Portable Network Javascript

Приукрасим забытое старое


Для начала — небольшая картинка в качестве эпиграфа. Продолжение — под катом.

image

На самом деле, сразу после неё следует строка



Да, это - не опечатка, я действительно имею в виду PNJ, а не PNG. Расшифровку сего извращенного термина см. в названии статьи. При следующих упоминаниях этого термина в данной статье имеется в виду обычное PNG-изображение.

Но зачем?


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

Сжимание кода в PNG - это типичный пример стеганографии (за исключением того, что в нашем случае здесь не слишком много секретности). Этот прием - давно не новость и иногда используется, например, в разных соревнованиях, где каждый байт - на вес золота.
Кроме того, для среднестатистических скриптов размером больше 100 kB статистически наблюдается неплохое уменьшение файла.

image

То же, но в числах:
Библиотека Размер min. JS (kB) Размер в PNG (kB)
jQuery 1.10.2 (minified) 93.107 57.042 -38.7%
jQueryUI 1.10.3 (custom, minified) 228.138 104.316 -54.3%
MooTools 1.4.5 (no-compat, YUI-compressed) 90.109 52.493 -41.7%
AngularJS 1.2.6 (minified) 100.023 62.730 -37.6%
EmberJS 1.2.1 (minified) 248.786 121.167 -51.3%


Как это работает?


Я, грубо говоря, изобрёл велосипед:
  • Из JS-кода генерируется PNG-изображение, данные которого заполняются соответствующими байтами из кода по очереди, т. е. в 1 пиксель зашивается 3 байта кода.
  • Изображение подгружается браузером
  • Специальная JS-библиотека считывает код из изображения на клиенте и выполняет его. Выполняется этот процесс с помощью рисования изображения на HTML5 canvas и считывания значений байтов, которые возвращает метод getImageData из canvas.getContext('2d').


Как видно, мы возлагаем все на плечи браузера и libpng (или что там у вас), которые и делают всё грязное дело, имплементируя таким способом кроссбраузерный механизм декомпрессии данных.

Кроме того, я заметил, что лучше всего сжимаются изображения ширины и высоты n/3 и 1 пикселей соответственно, поскольку, как я понимаю со своими недалёкими знаниями механизма компрессии PNG, пропадает необходимость сохранения данных о строке пикселя. Например, изображение в форме квадрата со стороной sqrt(n), хоть и выглядит более красиво, но весит почти также, как и JS-код.
И всё-таки, так выглядит jQuery v1.10.2 (minified) в виде квадратной стеганограммы:

image

Интеграция в реальный продукт


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

Пример страницы
<html>
    <head>
        <title>PNJ Test</title>
        <script type="image/javascript" src="jquery.png"></script>
        <script type="text/javascript" src="pnj.min.js"></script>
        <script type="text/javascript">
            pnj.ready(function() {
                $('.foo').html('Hello world!');
            });
        </script>
    </head>
    <body>
        <div class="foo">
            Loading...
        </div>
    </body>
</html>



Увы, обойтись без метода pnj.ready(...) не получилось, поскольку простого способа контроллировать ивенты а-ля *DOMReady я не придумал, а он может сработать до подгрузки всех PNJ-скриптов. Поэтому inline-код страницы приходится заворачивать в такой callback.

Замечания


  • Я не эксперементировал с другими форматами и другими типами палитры PNG
  • Код тестировался на работоспособность только в Firefox 26.0 и Chromium 31.0.1650.63
  • Для mime-типа скрипта, наверное, правильнее было бы использовать image/png или image/pnj, но хотелось заинтриговать читателя первой картинкой, поэтому осталось javascript.
  • Тэг с PNG-картинкой на самом деле подгружает и кеширует изображение в процессе загрузки страницы, но, поскольку декодирование происходит после DOMReady, приходится все равно пользовался методом pnj.ready(...).

  • Демо-страница: andrewdunai.com/misc/pnj-demo
    Генератор PNJ-изображений: andrewdunai.com/misc/pnj-demo/generate

    Дев-версия скрипта-декодера: andrewdunai.com/misc/pnj-demo/pnj.js
    Минифицированная версия: andrewdunai.com/misc/pnj-demo/pnj.min.js

    PNJ-вариант jQuery 1.10.2 (min): andrewdunai.com/misc/pnj-demo/jquery.png

    Спасибо за ваше внимание!
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 34

    +22
    Из графиков не понятна целесообразность — судя по графику JQuery в PNG занимает что-то межу 50 и 60 КБ, а gzip судя по closure-compiler.appspot.com/home должен занимать 32КБ
      +1
      Спасибо за отзыв! Я этот момент не учёл. А цифры действительно забыл добавить в статью, как только попаду домой — сразу отредактирую статью.
      +10
      Забавно, поучительно. Зануда mode on: практически все js, css и html и другие легко сжимаемые файлы отдаются с сервера уже сжатыми. То есть в настоящий момент нет необходимости даже минифицировать js и css, достаточно только комментарии вырезать. Даже не могу сообразить где ваш код пригодился бы.
        +3
        Согласен. Годится, по большому счету, лишь для забавы.
          +3
          Я бы написал об этом в статье, потому что мне показалось, что описанный в статье метод предлагается как решение проблемы сжатия скриптов.
          Очевидно, что он не самый лучший, ведь есть gzip, который:
          — поддерживается всеми браузерами
          — поддерживается всеми серверами
          — лучше сжимает

          А идея сама очень прикольная, респект
          +1
          Если хочется совсем уж адского сжатия, то таки минифицировать желательно. Удаляются лишние пробелы, переводы строк и прочие штуки.
          +5
          просто для истории :) – Compression using Canvas and PNG-embedded data, May 4, 2008
          0
          Напомнило habrahabr.ru/post/151538/
            +20
            Когда реализовывал такую систему (вынужденно, т.к. требовалось залить интерактивный рассказ на Самиздат, а засунуть туда JS можно только через img-эксплойт), столкнулся с её ненадёжностью.
            Во-первых, картинки частенько не прогружаются или прогружаются не полностью, особенно на мобильных устройствах с хреновым GPRS. Поэтому пришлось к изображению добавить контрольный столбец — самый правый столбик у меня содержал только синие (0,0,255) пиксели. Тогда перед декодированием можно проверить, прогрузилась ли картинка до конца или браузер срезал половину.
            Во-вторых, браузеры на движке WebKit отличаются редкостной параноидальностью, и когда пользователь сохраняет страничку, то JS запущенной локальной копии не имеет доступа к данным канваса (кто только придумал такое ограничение?).
            Наконец, оказалось, что слишком большие (>5Мб) картинки в процессе декодирования крашат Firefox for Android, причём он не просто падает, а уводит весь девайс в перезагрузку.
              +2
              Кстати, был опыт с Opera Turbo? Подозреваю, что пережатые картинки работать не будут вообще :)
                +1
                Opera Turbo пережимает только jpg изображения, png, gif остаются без пережатия, так было.
                +1
                Сделал в Gimp PNG'шку размером 2000x2000 и объёмом 7.8 МБайт с RGB-шумом, попробовал открыть на Nexus 7 с Android 4.3:

                Firefox 26.0.1: открыл, можно смотреть, масштабировать, полёт нормальный
                Chrome 31.0.1650.59: упал

                Разумеется, это ни о чём не говорит, просто интересный факт.
                +2
                Да-да-да, я то же самое однажды сделал, но на Хабр поленился запостить.
                Ссылка: animuchan.net/sukijs/ (см. исходники)
                  +1
                  Было уже: habrahabr.ru/post/102394/ и habrahabr.ru/post/31607/

                  Техника старая, обсосанная не раз. Используйте браузерный gzip и не парьтесь.
                    0
                    Кстати, Касперский может резать большой json, приходящий аяксом (что-то около 3 мегабайт). Причем режет по длине текста, т.к. приходит на самом деле 700-килобайтный gzip. Можно извратиться с мапами, а можно и попробовать скомпрессить так.
                      +7
                      Можно попробовать выкинул Касперский и не покупать его никогда, если он это действительно делает.
                        –3
                        Это сродни «давайте побивать камнями пользователей с 6 ослом/старой оперой/любителей на палмах»
                          +1
                          Нет, это как покарать производителя за плохой продукт.
                    +2
                    Парень, ты ненормальный. В хорошем смысле слова :) Именно такие и двигаю прогресс в программировании.
                      +1
                      А что на счет использования данного способа для обфускации js? Столкнулся недавно с этой проблемой и кроме как closure и обфускаторы вида !([]+[])+[], которые увеличивают длину кода в 100500 раз и прекрасно деобфусцируются одним нажатием кнопки, ничего не нашел.
                        0
                        С обфускацией тут ничего. Это не обфускация.
                        0
                        Если я не ошибаюсь — в один пиксель можно загнать четыре байта? Альфа канал же ещё…
                          0
                          Всё верно, ARGB, по байту на канал. Жаль, что в статье не описано почему именно 3 байта берётся.
                            0
                            Сжимается плохо, да и альфа полбайта только, если не ошибаюсь.
                              +2
                              Нельзя. К сожалению, canvas по-идиотски работает с альфа-каналом — в буфере хранятся не истинные цвета, а умноженные на значение альфа. В момент считывания браузер пытается восстановить их делением на альфа, что невозможно без потерь. Т.е. при считывании данных с канваса получим искажение цветов, если есть альфа-канал с переменными значениями.
                                0
                                Что-то я такого не наблюдал. Возвращаются все 4 компоненты из канваса. В псевдомассиве data данные лежат как RGBA.
                                  0
                                  Данные возвращаются, но с потерей точности, т.е. эти RGBA будут немного (незаметно для глаза) отличаться от исходных.
                                  Попробуйте закодировать текст в RGBA, засунуть этот массив в канвас, а потом сразу же снова считать и декодировать. При этом значительная часть символов заменяется «мусором» в результате потери точности.
                              +2
                              Сурово и бесполезно на практике, но очень интересно. Спасибо за статью!
                                0
                                А как насчет времени обработки js файлов с таким сжатием?
                                  +1
                                  Полезного вынес из статьи то, что EmberJS точно не буду использовать в обозримом будущем)

                                  Only users with full accounts can post comments. Log in, please.