?

Log in

No account? Create an account

[icon] Анимация APNG с помощью canvas - Давид Мзареулян
View:Свежие записи.
View:Архив.
View:Друзья.
View:Личная информация.
View:Website (Мои фотографии).
View:Иероглиф. hiero.ru/David. RSS2LJ. Здешние теги.

Tags:, , , ,
Security:
Subject:Анимация APNG с помощью canvas
Time:09:19 pm
APNG — это такое расширение формата PNG, позволяющее делать анимацию. APNG лучше анимированного GIF-а примерно тем же, чем PNG лучше GIF-а простого — поддержка 8-битной прозрачности и 24-битного цвета. Картинка ниже — это как раз APNG:



Если шарики у вас в браузере крутятся, вам повезло — ваш браузер понимает APNG. Если же нет (как и у меня), то для таких невезунчиков как мы, я на днях написал небольшую библиотеку. О том, как она делалась — много букв под катом, интересных только веб-программистам. А для нетерпеливых — ссылка на саму библиотеку: https://github.com/davidmz/apng-canvas

Формат APNG и общая идея


APNG не слишком популярен как раз потому что его поддерживают не все браузеры. А точнее, его поддерживают и отображают только Firefox и Opera. Crome, Safari и IE вместо анимации показывают статичную картинку (благо формат обратно-совместим с обычным PNG). Замечу, что есть ещё одна попытка добавить анимацию в PNG — MNG, но этот формат гораздо сложнее и требует от декодера реализации внутри себя чуть ли не всего фотошопа.

Когда я только-только заинтересовался APNG (не так давно), мне сразу подумалось, что должен быть какой-то способ анимировать его программно, при помощи JS и какой-нибудь из нынешних технологий вроде canvas или SVG. Идея мне показалась настолько очевидной, что я даже не стал искать подобные реализации — наверняка их кто-то уже написал, если потребуется найду в гугле. Потребовалось. Я сунулся в гугл и не нашёл ничего. Максимум что нашлось — громоздкое решение на SVG, для которого кадры надо готовить из APNG-файла заранее. Ну, раз нет, надо писать самому:)

PNG-файл очень прост. Он состоит из 8-байтовой сигнатуры (признак того, что перед нами PNG-файл), за которой идёт последовательность блоков данных (chunks в терминологии стандарта). Каждый блок имеет следующую структуру (все числа — big endian):

Длина данных (L, м. б. нулём)int32
Тип блока4-байтовая строка
ДанныеL байт
CRCCRC32 от типа блока и данных


Кроме сигнатуры и блоков в PNG-файле ничего нет. Блоки бывают обязательные и необязательные, в последних обычно сидит всякая служебная информация и расширения формата (в т. ч. и APNG). Нас интересуют следующие обязательные блоки:

  • IHDR — всегда первый блок, содержит общую информацию о картинке, в том числе её размеры;
  • IDAT — собственно данные картинки. Блоков IDAT может быть несколько;
  • IEND — завершающий блок, вегда последний в файле.


Между IHDR и IDAT а также между IDAT и IEND могут встречаться и другие блоки, их мы рассматривать не будем.

Таким образом, структура типичного PNG-файла такова: IHDR IDAT+ IEND

Разработчики APNG поступили очень просто — они ввели дополнительные типы блоков, управляющие анимацией. Полное описание лучше прочесть на сайте Mozilla, а тут я опишу общие принципы:
  • acTL — блок, наличие которого говорит, что перед нами APNG-файл. Содержит число кадров и количество повторов анимации;
  • fcTL — блок, содержащий параметры конкретного кадра — режимы наложения, размеры кадра и его смещение (кадр может быть меньше чем вся картинка, что позволяет перерисовывать только изменяющуюся часть). После блока fcTL идут блоки с данными кадра — либо IDAT либо fdAT;
  • fdAT — блок, содержащий данные картинки кадра. Формат у него такой же как у IDAT, только добавлен номер кадра. Как и IDAT, таких блоков может быть несколько на кадр.


Последовательность блоков типичного APNG такова:

IHDR acTL [fcTL IDAT+] [fcTL fdAT+] [fcTL fdAT+] … IEND

или такова:

IHDR acTL IDAT+ [fcTL fdAT+] [fcTL fdAT+] [fcTL fdAT+] … IEND

Красным цветом я выделил блоки, характерные для APNG. Последовательности [fcTL IDAT+] или [fcTL fdAT+] образуют кадры анимации (я их выделил скобками для простоты восприятия). В первом варианте IDAT описывает первый кадр анимации, во втором — не участвует в анимации.

Декодер, не понимающий APNG, просто игнорирует непонятные ему красные блоки и показывает статическую картинку IHDR IDAT+ IEND. Декодер APNG в свою очередь показывает анимацию, причём статическая картинка может быть первым её кадром, а может вообще не участвовать в показе.

Отсюда видно, как мы можем реализовать нужное нам поведение. Надо прочесть данные APNG-файла, разбить его на блоки, из этих блоков собрать нужное количество статичных PNG-кадров и в нужном порядке нарисовать их на canvas.

Что самое приятное — нам не надо понимать сам формат данных картинки (т. е. разбирать IDAT и fdAT). Согласно стандарту, все кадры имеют одинаковые параметры кодирования, одинаковую палитру и т. д. Поэтому нам нужно просто перетасовать уже имеющиеся блоки. Для каждого кадра надо взять взять его fdAT, поменять его тип на “IDAT”, выбросить номер кадра, пересчитать CRC — и у нас получится IDAT для новой картинки. Спереди к нему надо пристегнуть IHDR из исходного файла, сзади IEND — и вот у нас есть вполне корректный PNG-кадр, который поймёт canvas. Я упрощаю, конечно — надо ещё модифицировать размер картинки в IHDR и скопировать (без изменения) всякие вспомогательные блоки вроде палитры из исходной картинки, но это уже совсем простая задача.

Получение данных


Самое разветвлённое место во всём коде:) Нам надо получить двоичные данные APNG-картинки. Вопрос: как? Я не нашёл никаких способов добраться до данных тега img. Остаётся только старый добрый XMLHttpRequest (XHR далее).

Это сразу накладывает кучу ограничений, а именно: 1) скрипт не будет работать по протоколам отличным от http/https (т. е. не будет запускаться с локального диска) — невелика беда и 2) будут действовать кроссдоменные ограничения. Выражаясь по-русски, картинки должны находиться на том же домене, что и страница, их показывающая.

Chrome и Safari поддерживают т. н. Cross-Origin Resource Sharing — если сервер с картинкой выдаст HTTP-заголовок “Access-Control-Allow-Origin: *”, то XHR сможет принять от него данные даже находясь на другом домене. К сожалению, для IE9 это не работает. Точнее, он тоже поддерживает CORS, но умные ребята из Редмонда решили, что для кроссдоменных запросов нужно сделать специальный объект — XDomainRequest. Что им мешало внедрить нужный функционал в XHR — непонятно. А XDR, хотя и понимает CORS, но не даёт решительно никакой возможности получить двоичные данные ответа (у него нет поля responseBody, см. ниже). Так что для IE9 кроссдоменные картинки, похоже, показать не получится.

Я не зря всё время оговариваюсь, что нам нужны именно двоичные данные. Мы хотим получить строку с кодами символов от 0 до 255, соответствующим байтам APNG-файла. Как это сделано, можно посмотреть в коде функции loadBinary.

Проблема в том, что XHR, в общем, под двоичные данные не заточен. Он умеет получать текст, XML — и всё. На практике это означает, что мы получаем строку, в которой символы с кодами 0-127 отображаются корректно, а всё что выше заменяется на мусор с верхних этажей юникода. Мусор в том смысле, что восстановить исходные данные по нему невозможно.

Однако существует полулегальный хак (отражённый даже в мозилловской документации), который заключается в применении заклинания overrideMimeType('text/plain; charset=x-user-defined') перед вызовом XHR. Этот метод заставляет XHR считать, что данные придут в кодировке “x-user-defined”. Не знаю, что это за кодировка, но при этом байты с кодами 0xXX > 0x7F превращаются в символы с кодами 0xF7XX. Остаётся пробежаться по полученной строке и оставить от кода каждого символа только младший байт.

Это был метод номер раз. В Chrome мы можем использовать метод номер два: перед вызовом XHR установить его поле responseType в значение "arraybuffer". Тогда в ответ мы получим не текст, а ArrayBuffer, содержащий двоичные данные. Потом мы можем сделать из него Blob посредством BlobBuilder, который в свою очередь можем превратить в двоичную строку при помощи FileReader и его метода readAsBinaryString. Уф. Я вам скажу, что всё что относится к File API — это пока ещё во многом вуду. Но вся эта цепочка работает и, надеюсь, более оптимально, чем хак с “x-user-defined” (я сам скорости не мерял). Кстати, по спецификации responseType может быть также “blob”, что сэкономило бы нам один шаг цепочки, но в моём хроме он почему-то работать не захотел.

И, наконец, метод номер три для нашего любимого IE9. Надо ли говорить, что он не поддерживает не то что responseType, но и overrideMimeType? Другого от него мы и не ожидали. Но данные получать как-то надо. Оказывается, что в IE-шном XHR есть замечательное поле responseBody, которое содержит именно «сырые» данные ответа. Казалось бы, ура, но есть нюанс — это поле надоступно из JS. Точнее, оно в объекте видно, но у него нет типа и нет свойств, и добраться до его данных не представляется возможным.

Красоту и изящество решения этой проблемы, думаю, оценят все, кому приходилось писать под этот недобраузер. Решение заключается в создании VBScript-а (!!!), в котором (сюрприз!) поле responseBody внезапно оказывается «корректного» типа, и конвертации его в нормальную строку. Вот этот скрипт:
Function APNGIEBinaryToBinStr(Binary)
   APNGIEBinaryToBinStr = CStr(Binary)
End Function


Я понятия не имею, что такое CStr и какую уличную магию оно делает, но если скормить этой функции responseBody, то на выходе мы получим искомую строку. Слава Microsoft! Никогда не знаешь, через какое технологическое отверстие придётся побеждать очередную «особенность» IE…

Анимация


Ну а дальше всё просто. Получив данные, мы их разбиваем на блоки и собираем из них картинки для кадров. Тут тоже есть момент: как эти данные преобразовать в собственно объекты Image (которые понимает canvas)? Слава богу, что у нас есть data: URL, который как раз для этого и придуман. Почему-то везде используется data: URL в кодировке base64 (для чего приходится тянуть с собой JS-реализацию кодера), тогда как в браузерах есть всеми давно забытый, но вполне рабочий и нативный метод escape. Помните такого? Он очень своеобразно работает с юникодными текстами, а вот для нашей строки с кодами 0-255 он идеален. И data: URL собирается так:

"data:image/png," + escape(binString)

UPDATE 17/06/11 Я поторопился с похоронами base64 — как выяснилось, браузер в Android-е не понимает data:url в виде escaped-строки. Не то чтобы Андроид был сильно важен в этом контексте, но всё-таки неаккуратно.

К счастью, во всех браузерах кроме IE есть замечательный метод btoa. Это не что иное как встроенный в браузер base64-энкодер. Он в реальной жизни не очень применим, потому что не понимает юникода (хотя это обходится), но зато нашу бинарную строку он конвертит на ура. Поэтому в IE мы оставляем escape, а в остальных браузерах используем btoa.



Дальше у нас есть набор Image-объектов, представляющих конкретные кадры, и остаётся только по setTimeout их рисовать на canvas. Всё!

Заключение


Удивительно, что после формирования массива картинок анимация практически не грузит процессор. Если верить диспетчеру задач Хрома, то вкладка со страницей http://davidmz.github.com/apng-canvas/, на которой крутятся четыре анимации суммарным весом больше 3 Мб отъедает от 1 до 3% CPU. Интересно что та же страница в Firefox-е (который поддерживает APNG нативно) кушает около 8% CPU. Страница в Opera (тоже нативная поддержка APNG) ест в районе 1% CPU, а в IE9 — 6-8%.

Основное время (заметное на больших картинках) уходит на получение данных и формирование картинок. По-хорошему, в Crome надо использовать worker-ы и держать данные в ArrayBuffer-е. Я пока не стал этого делать, т. к. сейчас код для всех браузеров един (кроме загрузки данных), а так пришлось бы делать две разные ветки — для IE и для Chrome (и с Safari ещё отдельный вопрос). Но в перспективе — стоило бы.


Лицензия Creative Commons
Этот текст доступен по лицензии Creative Commons Attribution 3.0 Непортированная.
Комментарии: написать Previous Entry Поделиться Next Entry


jerom
Link:(Link)
Time:2011-06-16 06:08 pm
А для ff нельзя померить cpu usage js реализации?
(Ответить) (Thread)


david_m
Link:(Link)
Time:2011-06-16 06:52 pm
Померил. Получилось примерно то же самое — 8-10%.
(Ответить) (Parent) (Thread)


blacklion
Link:(Link)
Time:2011-06-16 07:37 pm
Хм. Даже странно — canvas модная есть, APNG нету. Это кто так отличился?
(Ответить) (Thread)


david_m
Link:(Link)
Time:2011-06-16 07:44 pm
В смысле - кто? Хром, Сафари и IE.
(Ответить) (Parent) (Thread)


valshooter
Link:(Link)
Time:2011-06-16 08:24 pm
Снимаю шляпу
(Ответить) (Thread)


_sirano_
Link:(Link)
Time:2011-06-16 08:33 pm
Эх. Я вот столкнулся как-то с тем, что какой-то из вполне олдовых браузеров ту самую 8-битную прозрачность не поддерживает, а тут APNG...
(Ответить) (Thread)

solovei95
Link:(Link)
Time:2011-06-17 06:32 am
Я очень прошу - сделай библиотеку, которая импортирует данные того или иного кадра APNG. Назвать библиотеку "apng-hacker".
(Ответить) (Thread)


david_m
Link:(Link)
Time:2011-06-17 06:52 am
Функция parsePNGData именно это и делает. Она, правда, недоступна снаружи, но ничто не мешает её вытащить. На входе — бинарная строка APNG-файла, в deferred передаётся распарсенная структура.
(Ответить) (Parent) (Thread)

(Удалённый комментарий)

david_m
Subject:Re: Библиотека
Link:(Link)
Time:2011-06-17 06:57 am
Делайте:)

Ещё раз, у меня в коде всё _уже_ разбирается. Вы вполне можете достать оттуда функцию парсинга и применить у себя.
(Ответить) (Parent) (Thread)


arkanoid
Link:(Link)
Time:2011-06-17 07:34 am
Но вот какая штука: 3% cpu это на самом деле тоже ОЧЕНЬ много. 30 с небольшим вкладок -- и все, машинка молотит только на это дело. А еще если вспомнить, что процессоры бывают и куда как послабее..
(Ответить) (Thread)


david_m
Link:(Link)
Time:2011-06-17 08:16 am
Всё-таки 30 вкладок с мегабайтными анимациями — это не очень типичный кейс:) А лёгкие картинки анимируются практически незаметно для проца. Потом, тот же FF отъедает куда больше даже на «родной» реализации APNG.

(Ответить) (Parent) (Thread)


egorfine
Link:(Link)
Time:2011-06-17 08:47 am
Суперкруто!

Багрепорт: в Сафари не работает, жалуется на то, что TypeError: 'undefined' is not a constructor (evaluating 'new (l.BlobBuilder||l.WebKitBlobBuilder)').
(Ответить) (Thread)


david_m
Link:(Link)
Time:2011-06-17 09:17 am
Спасибо! Но у меня работает в 5.0.5. Какая версия Сафари у тебя?
(Ответить) (Развернуть) (Parent) (Thread)


Игорь Алексеев
Link:(Link)
Time:2011-10-07 08:02 am
Спасибо огромное!
Если я верно понял apng лучше просто последовательности png тем что хранит разницу?
А Вы её обрабатываете или всегда перерисовываете весь кадр?
Или это уже дело браузера и его реализации canvas?
(Ответить) (Thread)


david_m
Link:(Link)
Time:2011-10-07 09:28 am
Обрабатываю, конечно.
(Ответить) (Parent) (Thread)


bolk
Link:(Link)
Time:2011-10-09 08:28 am
Спасибо, вдохновился добавить нормальную поддержку MJPEG в «Оперу»: https://github.com/bolknote/UserJS/blob/master/mjpeg.js

Кстати, больше всего бился с извлечением бинарных данных, в процессе перебора идей наугад придумал-таки способ.
(Ответить) (Thread)


david_m
Link:(Link)
Time:2011-10-09 08:54 am
Респект:) Кстати, не лучше ли будет создать регексп-объекты заранее — они тогда, наверное, скомпилируются и будут потом быстрее работать?
(Ответить) (Развернуть) (Parent) (Thread)

Дмитрий Яркин
Link:(Link)
Time:2012-09-07 11:27 am
Большое спасибо! Ваша статья, ссылки и код скрипта помогли мне понять как сделать APNG из PNG. Я доработал расширение ScreenShoter для Opera (https://addons.opera.com/en/extensions/details/screenshoter/?display=en), и теперь оно может захватывать движущиеся картинки и сохранять их как APNG.
(Ответить) (Thread)


Dmitry Do
Link:(Link)
Time:2013-12-27 06:40 am
Здравствуйте, Давид.
А посмотрите, пожалуйста, в IE 11 не работает.
http://pix.academ.org/img/2013/12/27/07af6ca95c6ab0023eecbb2c1404ef71.jpg (http://pix.academ.org/img/2013/12/27/07af6ca95c6ab0023eecbb2c1404ef71.jpg)
(Ответить) (Thread)


david_m
Link:(Link)
Time:2014-01-13 03:24 pm
Извините, потерял Ваш комментарий.

Да, в самом деле не работает… Придётся вернуться к этой библиотечке. Есть вероятность, что в IE11 уже можно и без VBScript…
(Ответить) (Parent) (Thread)

[icon] Анимация APNG с помощью canvas - Давид Мзареулян
View:Свежие записи.
View:Архив.
View:Друзья.
View:Личная информация.
View:Website (Мои фотографии).
View:Иероглиф. hiero.ru/David. RSS2LJ. Здешние теги.