На карте 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.

Немного изучив документацию, мы нашли несколько моментов, которые не позволили бы полноценно перевести наши продукты на эту библиотеку. 

  1. Минимальное требование к графическому API у библиотеки — OpenGL ES3. Мы сейчас кое-где ещё на OpenGL ES2 и только собираемся переезжать. 

  2. Библиотека находится на начальной стадии разработки и, судя по всему,  обновляется не очень регулярно.

Librsvg

Librsvg — библиотека для парсинга и растеризации SVG-изображений, используемая проектом GNOME. Её задача — растеризация SVG из популярной библиотеки Cairo. Отсюда и растут ноги у её недостатков.

  1. Сборка зависимостей. Пакетный менеджер Rust, cargo, не собирает эти зависимости сам, а на Windows их нужно собирать поштучно с помощью autoconf/make, с использованием либо MSYS, либо Cygwin. Сама сборка зависимостей — это добрая часть стека GTK (Cairo, GDK-Pixbuf, Pango, GLib).

  2. Размер библиотеки. Вместе с зависимостями она может достигать 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-файла, вычисление размера по умолчанию и растеризацию.

Для экспериментов собрали три набора данных по пять файлов в каждом:

  1. Изображения с проблемной растеризацией. Это логотипы разных организаций, на которых можно увидеть как прогресс, так и регресс в процессе растеризации. 

  2. Несколько случайно выбранных векторных изображений из стандартных ресурсов 2ГИС. Здесь нас больше всего интересовали изменения в производительности.

  3. Случайно выбранные векторные изображения из тестовой базы resvg. Тут нам хотелось узнать, насколько плохо с ними справится QtSvg.

Всё исследование разбили на шесть основных частей.

1. Скорость инициализации растеризатора

Инициализация растеризатора у QtSvg происходит за 84 нс, а у resvg — за 10 000 нс. В некоторых экспериментах эти цифры могли немного отличаться, но общая тенденция все равно сохранялась.

Несмотря на то, что разница огромна, она едва ли критична: инициализация выполняется редко, в идеальном случае — единственный раз за жизненный цикл приложения. А абсолютное время инициализации даже в плохом случае как resvg не выглядит огромным.

2. Парсинг SVG

Во втором эксперименте библиотека resvg показала хорошие результаты — не превосходные, но вполне конкурирующие с QtSvg. 

Набор/файл

QtSvg, мкс

resvg, мкс

разница, мкс

1/0

134

134

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, и даже оставлять свои отзывы. Пока жалоб на растеризацию не было ;)

Комментарии (6)


  1. A_HREF
    10.08.2021 14:42
    -2

    Еще можно Хромиумом растрировать, быстро и качественно, но проблема с размером.


    1. Amikko
      11.08.2021 00:31
      +1

      Хм, спасибо за идею! Погуглил, и оказалось, действительно так часто делают, например https://github.com/canhlinh/svg2png

      Просто какое-то время назад тоже была задача рендерить сложный наворочанный SVG во что-то растровое на Линуксе, и вменяемых библиотек, которые делали бы это без косяков, я так и нашёл. Если придётся к этой теме вернуться, попробую использовать такой подход с Хромом.


    1. rwscar Автор
      11.08.2021 12:34

      А его точно можно встроить в мобильное приложение?
      Ну и Chromium, наверное, образец «не самого компактного» ПО. Хотя его растеризация, конечно, хороша.


  1. Self_Perfection
    10.08.2021 19:51
    +4

    У вас противоречивые вещи написаны про скорость растеризации. Сначала в "Resvg vs QtSvg: начало" пишете

    Практически сразу оказалось, что растеризация у пропатченной resvg по сравнению с QtSvg проходит в шесть раз быстрее.

    А потом в "Эксперименты и сравнения / 4. Растеризация"

    Первый эксперимент, где нас не впечатлили результаты resvg. По всем показателям она была медленнее QtSvg, иногда — на порядок.

    Так быстрее или медленнее?


    1. rwscar Автор
      11.08.2021 16:02
      +1

      В экспериментальную часть статьи попали тайминги и картинки, сделанные новой версией resvg (0.12), а она медленнее, чем 0.11, которая и рассматривалась изначально как кандидатура для интеграции.
      Сделано так было потому, что данные о производительности версии 0.11 были недостаточно хорошо организованы, и их было мало. Я попробую перепрогнать тесты на изначальной версией и поправить статью.


  1. DiMaG
    11.08.2021 00:05
    +1

    Библиотека действительно быстрая и радует кол-вом поддерживаемых спецификаций. Но в каких-то местах всё еще слегка сыроватая. Например, у меня не рендерит тестовый SVG файл в PNG шире чем примерно 8000px в ширину (высота всего около 200px).
    7000px рендерит отлично, а вот 8000px уже пустая PNG-ха (не нулевой размер, а именно пустой canvas).

    Хотя вы вряд ли столкнетесь с такими размерами для логотипов и иконок на карте.