Декодирование 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
.
Т.е. логический порядок действий следующий:
- В главном потоке создаем новый экземпляр
MessageChannel
; - После создаем незаметный iframe, с абсолютным позиционированием и пр., чтобы не было особого влияния на верстку;
- После загрузки
iframe
- пробрасываем в негоport1
MessageChannel
, аport2
пробрасываем вWebWorker
; - После, используя метод
postMessage
портов, мы можем передавать необходимые данные между воркером и фреймом.
Важно отметить, что при применении postMessage, передаваемые данные копируются, что в нашем случае не оптимально по скорости и памяти. Но Transferable объекты (OffscreenCanvas
, ImageBitmap
, 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 из воркера.
Так же публикую примитивные примеры кода, где можно увидеть, как работает декодирование с использованием описанного в статье метода: