С Новым Годом! С Новым Годом!
Blog. Just Blog

PNG-декодер браузера как нативный DEFLATE-распаковщик

Версия для печати Добавить в Избранное Отправить на E-Mail | Категория: Web-мастеру и не только | Автор: ManHunter
PNG-декодер браузера как нативный DEFLATE-распаковщик
PNG-декодер браузера как нативный DEFLATE-распаковщик

В эпоху до появления современных JavaScript-API - то есть в 2000-х и начале 2010-х - веб-разработчики сталкивались с серьезной проблемой: браузеры не умели работать с бинарными данными. XMLHttpRequest возвращал только строки, а любые попытки передать ZIP, изображение или исполняемый файл заканчивались искажением байтов. Но тогдашнее сообщество не сдавалось. Вместо этого оно придумало один из самых изобретательных хаков в истории веба: использовать PNG-декодер браузера как нативный DEFLATE-распаковщик.

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

Сам формат PNG устроен довольно просто - если говорить не о содержимом изображения, а о файловой структуре: он состоит из заголовка и последовательности блоков (чанков). В интернете доступно множество подробных описаний формата, и разобраться в нем несложно. Единственное техническое требование - каждый чанк должен сопровождаться контрольной суммой CRC32. Но и это не "ракетная наука": алгоритм CRC32 хорошо документирован и легко реализуется даже на чистом JavaScript или, например, на Ассемблере.

Пример картинки PNG
Пример картинки PNG

Чтобы сохранить каждый байт без искажений, сперва начинают с получения сырого DEFLATE-потока - не ZIP-архива и не любого другого контейнера, а именно чистых сжатых данных в формате DEFLATE. Для этого использовался XMLHttpRequest с явным указанием overrideMimeType('text/plain; charset=x-user-defined'). Этот прием заставлял браузер интерпретировать ответ как последовательность байтов, а не как текст в кодировке UTF-8, и тем самым предотвращал потерю данных.
  1. // Получаем сырые сжатые данные как строку с сохранением байтов
  2. function getDeflateData(urlcallback) {
  3.   var xhr = new XMLHttpRequest();
  4.   xhr.open('GET'urltrue);
  5.   xhr.overrideMimeType('text/plain; charset=x-user-defined');
  6.   xhr.onreadystatechange = function() {
  7.     if (xhr.readyState === && xhr.status === 200) {
  8.         callback(xhr.responseText);
  9.     }
  10.   };
  11.   xhr.send();
  12. }
Далее эти сжатые данные вручную упаковывались в валидный PNG-файл: формировались обязательные заголовки, создавались чанки (IHDR, IDAT, IEND), а сам DEFLATE-поток помещался внутрь чанка IDAT.
  1. // Сырые DEFLATE-данные количество байтов, должно соответствовать 
  2. // длине распакованных данных например, это будет строка "Hello, world!"
  3. // Это заранее сжатый DEFLATE-поток (без zlib-заголовка!)
  4. const deflateBytes = new Uint8Array([
  5.   0x780x9C0xF30x480xCD0xC90xC90xD70x510x080xCF,
  6.   0x2F0xCA0x490x510x040x000x1A0x0B0x040x5D
  7. ]);
  8.  
  9. // Собираем валидный PNG на 13х1 пикселов
  10. function buildPngFromDeflate(deflateDatawidthheight 1) {
  11.   const pngSignature = new Uint8Array([
  12.     0x890x500x4E0x470x0D0x0A0x1A0x0A
  13.   ]);
  14.  
  15.   // IHDR
  16.   const ihdrData = new Uint8Array([
  17.     (width >> 24)
  18.     0xFF, (width >> 16) & 0xFF, (width >> 8) & 0xFFwidth 0xFF,
  19.     (height >> 24)
  20.     0xFF, (height >> 16) & 0xFF, (height >> 8) & 0xFFheight 0xFF,
  21.     8// bit depth
  22.     0// color type (grayscale)
  23.     0// compression
  24.     0// filter
  25.     0  // interlace
  26.   ]);
  27.   const ihdrChunk = new Uint8Array(12 ihdrData.length);
  28.   ihdrChunk.set([00013], 0); // длина данных
  29.   ihdrChunk.set([73726882], 4); // "IHDR"
  30.   ihdrChunk.set(ihdrData8);
  31.   ihdrChunk.set(crc32(new Uint8Array([
  32.     73726882, ...ihdrData
  33.   ])), ihdrData.length);
  34.  
  35.   // IDAT
  36.   const idatLength deflateData.length;
  37.   const idatChunk = new Uint8Array(12 idatLength);
  38.   idatChunk.set([
  39.     (idatLength >> 24) & 0xFF,
  40.     (idatLength >> 16) & 0xFF,
  41.     (idatLength >> 8) & 0xFF,
  42.     idatLength 0xFF,
  43.     73686584 // "IDAT"
  44.   ], 0);
  45.   idatChunk.set(deflateData8);
  46.   idatChunk.set(crc32(new Uint8Array([
  47.     73686584, ...deflateData
  48.   ])), idatLength);
  49.  
  50.   // IEND
  51.   const iendChunk = new Uint8Array([
  52.     0000736978681746696130
  53.   ]);
  54.  
  55.   // Собираем все вместе
  56.   const total pngSignature.length ihdrChunk.length idatChunk.length
  57.     iendChunk.length;
  58.   const png = new Uint8Array(total);
  59.   let offset 0;
  60.   [pngSignatureihdrChunkidatChunkiendChunk].forEach(chunk => {
  61.     png.set(chunkoffset);
  62.     offset += chunk.length;
  63.   });
  64.   return png;
  65. }
  66.  
  67. // Преобразуем в Data URI
  68. function uint8ToBase64(bytes) {
  69.   let binary '';
  70.   for (let i 0bytes.lengthi++) {
  71.     binary += String.fromCharCode(bytes[i]);
  72.   }
  73.   return btoa(binary);
  74. }
Для корректности формата к каждому чанку рассчитывалась контрольная сумма CRC32.
  1. // Вспомогательная функция CRC32
  2. function crc32(data) {
  3.   const table = [];
  4.   for (let i 0256i++) {
  5.     let c i;
  6.     for (let k 08k++) {
  7.       c = (1) ? 0xEDB88320 ^ (>>> 1) : >>> 1;
  8.     }
  9.     table[i] = c;
  10.   }
  11.   let crc 0xFFFFFFFF;
  12.   for (let i 0data.lengthi++) {
  13.     crc table[(crc data[i]) & 0xFF] ^ (crc >>> 8);
  14.   }
  15.   crc = (crc 0xFFFFFFFF) >>> 0;
  16.   return new Uint8Array([
  17.     (crc >>> 24) & 0xFF,
  18.     (crc >>> 16) & 0xFF,
  19.     (crc >>> 8) & 0xFF,
  20.     crc 0xFF
  21.   ]);
  22. }
Полученный бинарный поток кодировался в base64 и оборачивался в Data URI с префиксом data:image/png;base64,. Этот URI передавался элементу <img>. После загрузки изображение отрисовывалось на canvas, и вызов getImageData() возвращал уже распакованные байты, ведь браузер, декодируя PNG, автоматически применял нативную распаковку DEFLATE через встроенный zlib.
  1. // Запуск основной функции
  2. (async () => {
  3.   // Длина "Hello, world!"
  4.   const width 13;
  5.   const pngBytes buildPngFromDeflate(deflateByteswidth);
  6.   const dataUrl 'data:image/png;base64,' uint8ToBase64(pngBytes);
  7.  
  8.   const img = new Image();
  9.   img.onload = () => {
  10.     const canvas document.createElement('canvas');
  11.     canvas.width img.width;
  12.     canvas.height img.height;
  13.     const ctx canvas.getContext('2d');
  14.     ctx.drawImage(img00);
  15.  
  16.     const imageData ctx.getImageData(00canvas.widthcanvas.height);
  17.     const original = new TextDecoder().decode(
  18.       new Uint8Array(imageData.data.buffer0canvas.width)
  19.     );
  20.     // "Hello, world!"
  21.     console.log('Распаковано:'original);
  22.   };
  23.   img.src dataUrl;
  24. })();
Таким образом, PNG выступал не как изображение, а как обходной путь, транспортный контейнер, предоставлявший доступ к быстрому системному декодеру. В свое время условиях отсутствия бинарных API это был один из самых эффективных способов обрабатывать сжатые данные в браузере без медленных JavaScript-реализаций. Одним из ключевых преимуществ этого подхода была его кроссплатформенность: он работал везде, где поддерживался элемент canvas, включая устаревшие настольные и даже ранние мобильные браузеры. Не менее важной особенностью была полная независимость от внешних библиотек: для реализации не требовалось ни одного стороннего скрипта, использовались только встроенные, стандартные веб-API.

Сейчас все гораздо проще. Современные браузеры предоставляют прямой доступ к бинарным данным и встроенные инструменты для работы с ними:
  1. fetch('file.zip')
  2.   .then(response => response.arrayBuffer())
  3.   .then(buffer => {
  4.     // Используем JSZip или другие библиотеки
  5.     return JSZip.loadAsync(buffer);
  6.   })
  7.   .then(zip => {
  8.     // Доступ к файлам напрямую
  9.     return zip.file("document.txt").async("string");
  10.   });
А в некоторых случаях даже не нужны сторонние библиотеки, достаточно нативного Compression Streams API:
  1. const decompressed await new Response(
  2.   compressedStream.pipeThrough(
  3.     new DecompressionStream('deflate')
  4.   )
  5. ).arrayBuffer();
Больше не нужно выдумывать обходные пути через PNG, вручную рассчитывать CRC32 или превращать Canvas в DEFLATE-декодер. Все, что раньше требовало изобретательности, теперь это часть веб-стандарта. Но полезно знать, как такую задачу можно решить своими силами.

Поделиться ссылкой ВКонтакте
Просмотров: 211 | Комментариев: 1

Метки: JavaScript

Комментарии

Отзывы посетителей сайта о статье
Александр (26.12.2025 в 21:24):
Добавлю что Macromedia в своём Fireworks использовали PNG-файлы в качестве нативного формата и векторное описание проекта хранилось в этом же файле рядом с растровым изображением.
Правда, качество полученного растра по современным  меркам оставлял желать лучшего (сглаживание было неидеальным).

Добавить комментарий

Заполните форму для добавления комментария
Имя*:
Текст комментария (не более 2000 символов)*:

*Все поля обязательны для заполнения.
Комментарии, содержащие рекламу, ненормативную лексику, оскорбления и т.п., а также флуд и сообщения не по теме, будут удаляться. Нарушителям может быть заблокирован доступ к сайту.
Наверх
Powered by PCL's Speckled Band Engine 0.2 RC3
© ManHunter / PCL, 2008-2026
При использовании материалов ссылка на сайт обязательна
Время генерации: 0.07 сек. / MySQL: 2 (0.0025 сек.) / Память: 4.5 Mb
Наверх