Персональный сайт Олега Барабанова

Декодирование SVG в WebWorker в JavaScript

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

Как известно, в современных браузерах есть множество нативных способов декодирования изображений для последующего переноса на холст (Canvas API). Например, используя старый подход c событием onload:

image.onload => () => canvas.getContext('2d').drawImage(image, 0, 0)

или используя метод .decode()

canvas.getContext('2d').drawImage(await image.decode(), 0, 0)

или вообще используя глобальный метод createImageBitmap()

const imageBitmapPromise = createImageBitmap(image[, options])
const imageBitmapPromise = createImageBitmap(image, sx, sy, sw, sh[, options])

И впринципе все просто и понятно, но в случае с SVG не все так просто.

Проблема растеризации SVG в WebWorker

Если использовать вышеописанные методы в основном потоке - проблем нет, т.к. основной поток имеет доступ к DOM-модели. Но если вы попытаетесь применить их в воркере, то столкнетесь с проблемами, т.к. выяснится, что фактически DOM является программным интерфейсом для SVG, а как известно из спецификации, воркеры не имеют доступа к DOM. Проще говоря, вышеописанными способами декодировать SVG не получится.

Так что же можно сделать в таком случае?

Ну заново писать на JS всё декодирование и перевод в bitmap SVG - дело неблагодарное, т.к. SVG не самый легкий формат. А если вспомнить, что как и любой XML документ, SVG может иметь внутри себя подпространства имен и пр. - в общем лучше с этим не связываться.

Соответственно нам в любом случае нужен доступ к DOM модели. И если нам лучше не затрагивать попусту основной поток - попробуем использовать Iframe, внутри которого декодируем SVG и передадим как bitmap в WebWorker, используя для транспорта интерфейс MessageChannel.

Схема взаимодействия webworker с iframe через messagechannel

Т.е. логический порядок действий следующий:

  1. В главном потоке создаем новый экземпляр MessageChannel;
  2. После создаем незаметный iframe,с абсолютным позиционированием и пр., чтобы не было особого влияния на верстку;
  3. После загрузки iframe - пробрасываем в него port1 MessageChannel, а port2 пробрасываем в WebWorker;
  4. После, используя метод postMessage портов, мы можем передавать необходимые данные между воркером и фреймом.

Важно отметить, что при применении postMessage, передаваемые данные копируются, что в нашем случае не оптимально по скорости и памяти. Но Transferable объекты (OffscreenCanvas, ImageBirmap, ArrayBuffer и пр.) можно передать без копирования, используя аргумент transferList метода postMessage:

self.postMessage(imageBitmap, [imageBitmap])

Кстати, отмечу, что в настоящее время поддержка OffscreenCanvas или ImageBitmap практически полностью не поддерживаются в Safari и соответственно там придется городить отдельные костыли.

Причина необходимости декодирования SVG в отдельном потоке

Большое время может занять не столько декодирование SVG сколько время формирования растрового изображения. Если изображение имеет небольшое разрешение, то проблем нет. Но если планируемое изображение имеет такие размеры, как 7500x7500px, преобразование векторного изображения в растр может занимать продолжительное время и блокировать основной поток (а метод .decode() вообще может выбрасывать ошибку).

Работа iframe в отдельном потоке

Важног заметить, что по умолчанию iframe работает в том же потоке, что и главная страница, но последние версии браузеров запускают новый iframe уже в отдельном потоке в случае кросс-доменных запросов. Но при этом мы сталкиваемся с ограничениями CORS, которые в свою очередь будут блокировать декодированный ImageBitmap из Iframe в воркер из соображений безопасности. Помочь разобраться с этой проблемой может заголовок Access-Control-Allow-Origin, но к сожалению вынужден отметить, что нет гарантий, что определенный браузер запустит кроссдоменный Iframe в отдельном потоке, при разрешенном Access-Control-Allow-Origin. Тем не менее, есть способы (при определенных ситуациях) обойти и CORS в случае с SVG, но это тема отдельной статьи.

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

Выводы, касательно применения связки MessageChannel + Iframe + WebWorker

В общем можно сказать, что если нет возможности вывести iframe в другой домен - то в цепочке "главный поток + воркер + фрейм", можно убрать за ненадобностью фрейм и декодировать SVG напрямую в главном потоке. По остальным случаям необходимо проводить эксперименты (особенно с мобильными браузерами).

Тем не менее, там, где эта схема работает - появляется хоть какая-то возможность управления растеризацией SVG из воркера.

Так же публикую примитивные примеры кода, где можно увидеть, как работает декодирование с использованием описанного в статье метода: