Статьи

Перевод: Мациевский Николай aka sunnybear
Опубликована: 16 июня 2008

Сжатие при помощи canvas и PNG-картинок

Примечание: ниже находится перевод статьи "Compression using Canvas and PNG-embedded data". Автор предлагает на суд читателей еще один способ загрузить в клиентском браузере JavaScript-библиотеку, передав при этом минимум данных. Для этого используется PNG-картинка и объект canvas. Мои комментарии далее курсивом.

Недавно у меня появилась идея, что можно хранить исходный Javascript-код в PNG-картинке, а затем получать его через метод getImageData() элемента canvas. К несчастью, сейчас это означает, что такой подход будет работать только в Firefox, Opera Beta и последних ночных сборках WebKit. Пока еще никто не указал мне, насколько gzip опережает данный метод по степени сжатия, я хочу сразу сказать что рассматриваемый метод никак не может быть практической альтернативой физическому сжатию. Чуть раньше сегодня я уже писал о сжатой версии в 8Кб скрипта Super Mario, для которого использовалась эта техника (подробнее можно прочитать в заметке про кодирование). Здесь я приведу лишь некоторые детали о действительном положении вещей.

Вышеприведенная картинка может выглядеть как белый шум, однако, она содержит 124 килобайта библиотеки Prototype, ужатой до 30 килобайт 8-bit PNG-картинки. PNG является графическим форматом без потерь качества, поэтому, теоретически, вы можете хранить данные любого типа как цветовые значения пикселей, например, это может быть Javascript-код, как это проделано выше. И этот формат также предлагает некоторые техники фильтрации для сжатия изображения, чем мы также воспользуемся, что уменьшить наш исходный код в размере. Мы также можем в любой момент извлечь наш код обратно в первоначальном виде.

Вначале нам нужно выбрать наилучший формат для представления наших данных графически. Это означает, что, с одной стороны, он должен лучше всего сжимать исходные данные, с другой стороны, делать это без потерь качества. В интернете у нас большого выбора нет, поэтому (так как JPEG представляет данные с потерями качества) наш выбор лежит между GIF и PNG.

Для PNG у нас есть две возможности: цветовая палитра в 24 бита и 8 бит. При использовании 24 бит и RGB цветов мы можем хранить в одном пикселе 3 байта данных, для 8-битной палитры только 1 байт на пиксель.

Небольшое тестирование в Photoshop показывает, что 8-битная картинка размером 300x100 с монохромным шумом сжимается до 5 Кб. При этом картинка 100x100 с 24-байтной палитрой с тем же самым шумом, примененным к каждому из трех каналов — R, G и A — сжимается до примерно 20 Кб. Обычный 8-битный GIF-файл получается немного больше в размере, чем 8-битный PNG-аналог, поэтому мы выбираем именно PNG. (При создании шума Photoshop не изменяет каждый пиксель, но и код не состоит из совершенно случайных участков, поэтому я считаю, что тестирование эквивалентно рассматриваемому случаю.)

Теперь нам нужно преобразовать наш JavaScript-файл в цветовые данные и сохранить его в виде PNG-файла. Для этой цели я набросал следующее, достаточно грубое, решение в виде PHP-скрипта, который читает исходный Javascript-файл, создает PNG-изображение и просто записывает в значение каждого пикселя числа от 0 255 — ASCII-код текущего символа скрипта (фактически, на 1 пиксель будет уходить 4 байта исходного файла: RGBA).

Здесь я столкнулся с небольшой проблемой: картинка создается из полноцветной (truecolor) палитры, тогда как нам нужно 8-битная индексированная, а PHP не умеет производить такое преобразование. Возможно, существует какой-то способ создавать индексированное изображение при помощи PHP/GD, однако, я не исследовал данный вариант, а использовал другой подход. Я просто открывал созданное изображение в чем-то типа Photoshop и преобразовывал его там в 8-битную палитру.

Итак, теперь у нас есть один JavaScript-файл, упакованный в сжатое PNG-изображение, и нам нужно лишь доставить его на клиент в первоначальном виде. При помощи элемента canvas мы можем достаточно просто рисовать изображение через метод drawImage(), а затем читать данные попиксельно с помощью getImageData(). Эти данные выдаются в виде большого массива значений, где каждый пиксель содержит 4 элемента (RGBA), поэтому нам лишь нужно взять эти 4 значения и склеить их вместе в виде строки, готовой к eval(). И на этом все.

Функция, которая читает из PNG-изображения данные, выглядит примерно следующим образом: pngdata.js.

Несколько тестовых результатов:

Результаты получились весьма впечатляющие даже для ужатых с помощью Packer (или любых других обфускаторов-минимизаторов) скриптов: мы можем урезать их первоначальный размер еще в два раза, используя описанный выше метод. Полученный PNG-файл может быть дальше уменьшен при помощи различных оптимизационных инструментов. Вы можете ознакомиться с комментариями по поводу других тестовых случаев и дополнительной информации (спасибо FreakCERS!).

Здесь вы можете самостоятельно «поиграться» с изображениями.

Естественно, у нас имеются некоторые издержки в виде дополнительного объема кода, которые позволяет читать данные и исполнять их, но весь его объем составляет не больше 300 байтов. Некоторые файлы не жмутся хорошо, например, такие, которые уже изначально хорошо сжаты. Я протестировал этот алгоритм на замечательном (и хорошо пожатом) 3D Tomb 2 от Mathieu 'p01' Henri, так как файл уже достаточно мал в размере и хорошо минимизирован, то все, что удалось получить после PNG-сжатия было потеряно из-за дополнительных издержек.

И, естественно, для больших скриптов будет существенная задержка в загрузке, потому что сначала изображение загружается в canvas, а затем попиксельно считывается. Чтение и анализ 69 Кб PNG, с помощью которой сжат 255 Кб dijit-all.js, занимает примерно 5–6 секунд (в FF2, Safari и другие браузеры справляются быстрее), что, в общем случае, совершенно неприемлемо. Распознавание 16 Кб PNG файла, полученного из 46 Кб dijit.js, занимает всего 500–1000 мс (целую секунду!), поэтому для более мелких файлов проблема бует не столь значительна.

В любом случае, поддержка метода getImageData для элемента canvas распространена не так широко, поэтому я предполагаю, что данный поход будет не более чем примером любознательности и еще одним способом так или иначе использовать canvas. Это означает, что все вышеописанное является лишь вещью в себе, и это не стоит использовать в приложениях, рассчитанных на конечных пользователей, когда существует более простые и действенные методы, например, gzip.

И еще раз, здесь выложен пример «распаковки» в действии. А здесь можно посмотреть, как в игре Mario используется эта техника.

Пара замечаний: во всех случаях, когда можно ограничиться встроенными средствами браузера для минимизации передаваемых данных, стоит воспользоваться ими. Это могут быть и особенности синтаксиса, позволяющие представить код в более компактном виде, и поддержка gzip/deflate на «аппаратном уровне». Все эти средства могут выполнить большинство требуемых операций гораздо более эффективно, чем их JavaScript-эмуляции. Поэтому, собственно, использовать JavaScript для ускорения процесса загрузки страницы категорически не рекомендуется: в большинстве случаев он только замедлит этот процесс.

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

Читать дальше

Все комментарии (habrahabr.ru)