На карте 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 |
1/2 |
134 |
122 |
-12 |
1/3 |
103 |
109 |
+6 |
1/4 |
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 |
3/4 |
82 |
241 |
159 |
3. Вычисление размера по умолчанию
Здесь resvg тоже выигрывает у QtSvg. Судя по результатам, resvg использует какой-то алгоритм с константной сложностью.
Набор/файл |
QtSvg, мкс |
resvg, мкс |
разница, мкс |
1/0 |
12 |
2 |
-10 |
1/1 |
14 |
2 |
-12 |
1/2 |
13 |
2 |
-11 |
1/3 |
13 |
2 |
-11 |
1/4 |
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 |
3/4 |
10 |
2 |
-8 |
4. Растеризация
Первый эксперимент, где нас не впечатлили результаты resvg. По всем показателям она была медленнее QtSvg, иногда — на порядок. Мы не знаем, с чем это может быть связано — может быть, с бóльшим набором поддерживаемых возможностей resvg или с небольшим возрастом библиотеки.
Набор/файл |
QtSvg, мкс |
resvg, мкс |
разница, мкс |
1/0 |
216 |
264 |
48 |
1/1 |
2041 |
14 788 |
12 747 |
1/2 |
1343 |
12 500 |
11 157 |
1/3 |
1153 |
14 160 |
13 007 |
1/4 |
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 |
3/4 |
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, и даже оставлять свои отзывы. Пока жалоб на растеризацию не было ;)
Автор: Филипп