Кроссплатформенная растеризация SVG — сравниваем библиотеки и экспериментируем
На карте 2ГИС очень много картинок — те же знаки дорожного движения и логотипы компаний. Графические API, которые в наши карты предоставляют Android и iOS, обычно не могут рисовать векторную графику напрямую, поэтому нам приходится её растеризовать. А так как мы заранее не знаем нужный размер картинки и не можем её растеризовать до сборки ресурсов, используем растеризаторы.
И если для 2ГИС на Android и iOS мы можем использовать платформенные решения, то затаскивать их в Mobile SDK было бы, мягко говоря, не очень правильно.
При внедрении поддержки векторных изображений в мобильный 2ГИС мы не нашли нормальное кроссплатформенное решение, поэтому использовали разные растеризаторы для Android и iOS. На Android — это QtSvg, потому что приложение уже использует библиотеку Qt. На iOS нашли избыточную по функциональности, но подходящую нам библиотеку Macaw.
В случае SDK такой подход обычно не срабатывает: вряд ли мы сможем интегрировать Qt в нашу SDK только ради QtSvg, а написанную на Swift библиотеку Macaw невозможно использовать на Android. Да и вообще — иметь два различных растеризатора неудобно, поэтому мы начали искать единый инструмент, который сможет работать на наших платформах.
Поиски растеризатора
Стандарт SVG довольно объёмный — только официальная документация версии SVG 1.1 составляет более 800 страниц, а SVG Tiny для смартфонов — ещё 500 страниц. И создание собственного растеризатора — нетривиальная техническая задача.
Поэтому мы решили всё-таки посмотреть уже существующие решения, а уже потом, если не найдём, пытаться создать собственный растеризатор.
Сначала мы собрали пожелания к единому растеризатору:
умение парсить и растеризовать SVG,
открытость,
бесплатность,
кроссплатформенность,
легковесность (минимум дополнительных зависимостей),
простая интеграция с CMake-проектом,
статическая компоновка.
Под эти качества подошли три библиотеки — pathfinder, librsvg и resvg. Мы проверили работу каждой.
Pathfinder
Pathfinder — библиотека для растеризации векторных изображений с GPU-ускорением. Она является частью проекта Servo — браузерного движка, написанного на Rust.
Немного изучив документацию, мы нашли несколько моментов, которые не позволили бы полноценно перевести наши продукты на эту библиотеку.
Минимальное требование к графическому API у библиотеки — OpenGL ES3. Мы сейчас кое-где ещё на OpenGL ES2 и только собираемся переезжать.
Библиотека находится на начальной стадии разработки и, судя по всему, обновляется не очень регулярно.
Librsvg
Librsvg — библиотека для парсинга и растеризации SVG-изображений, используемая проектом GNOME. Её задача — растеризация SVG из популярной библиотеки Cairo. Отсюда и растут ноги у её недостатков.
Сборка зависимостей. Пакетный менеджер Rust, cargo, не собирает эти зависимости сам, а на Windows их нужно собирать поштучно с помощью autoconf/make, с использованием либо MSYS, либо Cygwin. Сама сборка зависимостей — это добрая часть стека GTK (Cairo, GDK-Pixbuf, Pango, GLib).
Размер библиотеки. Вместе с зависимостями она может достигать 40 МБ. А максимальный размер приложений в App Store и Google Play, которые можно скачивать не через Wi-Fi, — 100 и 200 МБ. Трата от 20 до 40% места только на растеризацию SVG выглядит как серьёзный удар по конкурентоспособности SDK.
Librsvg нам точно не подходит — несмотря на качество этой библиотеки, она слишком большая для нашей задачи.
Resvg
Resvg оказалась наиболее подходящим для нас вариантом. Она легко компилируется на настольных платформах, без проблем интегрируется с существующим Qt-кодом благодаря совместимым интерфейсам и имеет не такой большой список зависимостей, как предыдущие варианты. При этом она стабильно развивается — это видно по вкладке releases на Гитхабе.
Один минус мы всё же нашли — конфликт версий зависимостей между нашим кодом и кодом resvg. В зависимостях и у resvg, и у нашей SDK есть библиотека поддержки двунаправленного текста и диакритических знаков Harfbuzz — она нужна для корректного отображения текста при растеризации SVG. Но версии Harfbuzz у нас и в resvg были разные, и это мешало нам интегрировать библиотеку статически: не получалось ни избавиться от второй сборки harfbuzz, ни скомпановаться с двумя версиями сразу.
Оказалось, что проблема в итоге не очень большая и такой конфликт всё-таки можно решить, используя одну из версий зависимости. Мы решили тестово интегрировать resvg в Android-версию SDK и посмотреть, как она покажет себя в деле — пусть даже и с динамической компоновкой.
Вооружившись статьёй об интеграции Rust-библиотек в Android-приложение и описанием системы сборки Rust, взялись за работу.
Первые сложности
Мы достаточно быстро нашли у resvg в зависимостях пакеты, для сборки которых необходим C++. Более того, по коду сборочных скриптов стало понятно, что разработчики библиотеки не планируют собирать её под мобильные ОС. Поэтому нам пришлось самостоятельно дописать код для сборочных скриптов — например, для интеграции с CMake мы создали скрипт на Python, который для сборки библиотеки вызывает пакетный менеджер Rust.
Написанный нами скрипт решал достаточно сложную техническую задачу. Для кросс-компиляции Rust-кода раньше требовалось передавать через окружение пути к платформенным инструментам — компилятору C++, архиватору и компоновщику, —, а CMake сам по себе с этим не справлялся. Про возможность прописывания путей в ~/.cargo/config мы знали, но подкидывание путей через окружение казалось нам самым удобным решением: это позволяет пробросить пути напрямую из CMake, упростив таким образом конфигурацию машин CI и разработчиков.
Resvg vs QtSvg: начало
После модификации resvg мы начали проводить испытания — сравнивали скорость растеризации и внешний вид изображений с QtSvg. Практически сразу оказалось, что растеризация у пропатченной resvg по сравнению с QtSvg проходит в шесть раз быстрее.
Позже, после обновления resvg, заметили регресс производительности. Скорость стала уже, конечно, не в шесть раз больше, но качество не пострадало.
Во время тестирования resvg мы нашли небольшую проблему — в версии библиотеки на Android не загружались системные шрифты, потому что зависимость resvg-fontdb не поддерживает Android и iOS, хотя их поддержку возможно добавить буквально парой строк кода. Несмотря на это, мы не стали отказываться от resvg — библиотека позволяет работать с собственными шрифтами, которые мы чаще всего и используем.
Эксперименты и сравнения
Перед испытаниями мы написали адаптер, чтобы использовать resvg в этих наших случаях, и небольшой бенчмарк, который тестировал отдельные функции растеризатора — инициализацию растеризатора, парсинг svg-файла, вычисление размера по умолчанию и растеризацию.
Для экспериментов собрали три набора данных по пять файлов в каждом:
Изображения с проблемной растеризацией. Это логотипы разных организаций, на которых можно увидеть как прогресс, так и регресс в процессе растеризации.
Несколько случайно выбранных векторных изображений из стандартных ресурсов 2ГИС. Здесь нас больше всего интересовали изменения в производительности.
Случайно выбранные векторные изображения из тестовой базы resvg. Тут нам хотелось узнать, насколько плохо с ними справится QtSvg.
Всё исследование разбили на шесть основных частей.
1. Скорость инициализации растеризатора
Инициализация растеризатора у QtSvg происходит за 84 нс, а у resvg — за 10 000 нс. В некоторых экспериментах эти цифры могли немного отличаться, но общая тенденция все равно сохранялась.
Несмотря на то, что разница огромна, она едва ли критична: инициализация выполняется редко, в идеальном случае — единственный раз за жизненный цикл приложения. А абсолютное время инициализации даже в плохом случае как resvg не выглядит огромным.
2. Парсинг SVG
Во втором эксперименте библиотека resvg показала хорошие результаты — не превосходные, но вполне конкурирующие с QtSvg.
Набор/файл | QtSvg, мкс | resvg, мкс | разница, мкс |
1/0 | 134 | 134 | 0 |
1/1 | 225 | 220 | -5 |
½ | 134 | 122 | -12 |
⅓ | 103 | 109 | +6 |
¼ | 339 | 398 | +59 |
2/0 | 109 | 87 | -22 |
2/1 | 131 | 84 | -47 |
2/2 | 112 | 82 | -30 |
2/3 | 188 | 110 | -78 |
2/4 | 100 | 90 | -10 |
3/0 | 78 | 68 | -10 |
3/1 | 80 | 146 | +66 |
3/2 | 203 | 148 | -55 |
3/3 | 75 | 138 | +63 |
¾ | 82 | 241 | 159 |
3. Вычисление размера по умолчанию
Здесь resvg тоже выигрывает у QtSvg. Судя по результатам, resvg использует какой-то алгоритм с константной сложностью.
Набор/файл | QtSvg, мкс | resvg, мкс | разница, мкс |
1/0 | 12 | 2 | -10 |
1/1 | 14 | 2 | -12 |
½ | 13 | 2 | -11 |
⅓ | 13 | 2 | -11 |
¼ | 12 | 2 | -10 |
2/0 | 9 | 2 | -7 |
2/1 | 8 | 2 | -6 |
2/2 | 8 | 2 | -6 |
2/3 | 9 | 2 | -7 |
2/4 | 8 | 2 | -6 |
3/0 | 9 | 2 | -7 |
3/1 | 10 | 2 | -8 |
3/2 | 9 | 2 | -7 |
3/3 | 9 | 2 | -7 |
¾ | 10 | 2 | -8 |
4. Растеризация
Первый эксперимент, где нас не впечатлили результаты resvg. По всем показателям она была медленнее QtSvg, иногда — на порядок. Мы не знаем, с чем это может быть связано — может быть, с бóльшим набором поддерживаемых возможностей resvg или с небольшим возрастом библиотеки.
Набор/файл | QtSvg, мкс | resvg, мкс | разница, мкс |
1/0 | 216 | 264 | 48 |
1/1 | 2041 | 14 788 | 12 747 |
½ | 1343 | 12 500 | 11 157 |
⅓ | 1153 | 14 160 | 13 007 |
¼ | 365 | 941 | 576 |
2/0 | 293 | 703 | 410 |
2/1 | 276 | 680 | 404 |
2/2 | 58 | 93 | 35 |
2/3 | 110 | 286 | 176 |
2/4 | 319 | 732 | 413 |
3/0 | 106 | 893 | 787 |
3/1 | 242 | 351 | 109 |
3/2 | 234 | 452 | 218 |
3/3 | 341 | 371 | 30 |
¾ | 594 | 1081 | 487 |
5. Регресс и прогресс на проблемных логотипах
Какого-то сильного отличия между библиотеками в качестве обработки логотипов мы не нашли. Ошибка растеризации, хоть и разная, наблюдается только на одном из изображений.
6. Ошибки растеризации у QtSvg
В этом эксперименте выяснили, что растеризация у QtSvg не идеальна и имеет слабые стороны там, где resvg нормально справляется.
Результаты битвы resvg с QtSvg
Спустя шесть экспериментов мы пришли к выводу, что библиотека resvg хоть и не идеальна для нашего проекта, мы всё же попробуем использовать её в работе нашей Android-версии SDK вместо QtSvg.
Важная ремарка — все наши эксперименты проводились для дальнейшей интеграции resvg с Android. Когда мы добрались до iOS-версии, уже вышла новая версия resvg 0.12, в которой пакеты зависимостей были переписаны с С++ на Rust. Это упростило интеграцию с нашим кодом — количество путей к инструментам для передачи в cargo уменьшилось и появилась статическая компоновка, потому что второй экземпляр Harfbuzz исчез. Мы этим оперативно воспользовались.
Сейчас мы внедрили библиотеку resvg в Mobile SDK и заканчиваем её интеграцию в Android- и iOS-приложения 2ГИС. Наши партнёры по экосистеме уже могут работать с тестовой версией приложений, в которых используется resvg, и даже оставлять свои отзывы. Пока жалоб на растеризацию не было ;)