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

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

Пример картинки PNG
Чтобы сохранить каждый байт без искажений, сперва начинают с получения сырого DEFLATE-потока - не ZIP-архива и не любого другого контейнера, а именно чистых сжатых данных в формате DEFLATE. Для этого использовался XMLHttpRequest с явным указанием overrideMimeType('text/plain; charset=x-user-defined'). Этот прием заставлял браузер интерпретировать ответ как последовательность байтов, а не как текст в кодировке UTF-8, и тем самым предотвращал потерю данных.
Code (JavaScript) : Убрать нумерацию
- // Получаем сырые сжатые данные как строку с сохранением байтов
- function getDeflateData(url, callback) {
- var xhr = new XMLHttpRequest();
- xhr.open('GET', url, true);
- xhr.overrideMimeType('text/plain; charset=x-user-defined');
- xhr.onreadystatechange = function() {
- if (xhr.readyState === 4 && xhr.status === 200) {
- callback(xhr.responseText);
- }
- };
- xhr.send();
- }
Code (JavaScript) : Убрать нумерацию
- // Сырые DEFLATE-данные количество байтов, должно соответствовать
- // длине распакованных данных например, это будет строка "Hello, world!"
- // Это заранее сжатый DEFLATE-поток (без zlib-заголовка!)
- const deflateBytes = new Uint8Array([
- 0x78, 0x9C, 0xF3, 0x48, 0xCD, 0xC9, 0xC9, 0xD7, 0x51, 0x08, 0xCF,
- 0x2F, 0xCA, 0x49, 0x51, 0x04, 0x00, 0x1A, 0x0B, 0x04, 0x5D
- ]);
- // Собираем валидный PNG на 13х1 пикселов
- function buildPngFromDeflate(deflateData, width, height = 1) {
- const pngSignature = new Uint8Array([
- 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A
- ]);
- // IHDR
- const ihdrData = new Uint8Array([
- (width >> 24)
- & 0xFF, (width >> 16) & 0xFF, (width >> 8) & 0xFF, width & 0xFF,
- (height >> 24)
- & 0xFF, (height >> 16) & 0xFF, (height >> 8) & 0xFF, height & 0xFF,
- 8, // bit depth
- 0, // color type (grayscale)
- 0, // compression
- 0, // filter
- 0 // interlace
- ]);
- const ihdrChunk = new Uint8Array(12 + ihdrData.length);
- ihdrChunk.set([0, 0, 0, 13], 0); // длина данных
- ihdrChunk.set([73, 72, 68, 82], 4); // "IHDR"
- ihdrChunk.set(ihdrData, 8);
- ihdrChunk.set(crc32(new Uint8Array([
- 73, 72, 68, 82, ...ihdrData
- ])), 8 + ihdrData.length);
- // IDAT
- const idatLength = deflateData.length;
- const idatChunk = new Uint8Array(12 + idatLength);
- idatChunk.set([
- (idatLength >> 24) & 0xFF,
- (idatLength >> 16) & 0xFF,
- (idatLength >> 8) & 0xFF,
- idatLength & 0xFF,
- 73, 68, 65, 84 // "IDAT"
- ], 0);
- idatChunk.set(deflateData, 8);
- idatChunk.set(crc32(new Uint8Array([
- 73, 68, 65, 84, ...deflateData
- ])), 8 + idatLength);
- // IEND
- const iendChunk = new Uint8Array([
- 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130
- ]);
- // Собираем все вместе
- const total = pngSignature.length + ihdrChunk.length + idatChunk.length
- + iendChunk.length;
- const png = new Uint8Array(total);
- let offset = 0;
- [pngSignature, ihdrChunk, idatChunk, iendChunk].forEach(chunk => {
- png.set(chunk, offset);
- offset += chunk.length;
- });
- return png;
- }
- // Преобразуем в Data URI
- function uint8ToBase64(bytes) {
- let binary = '';
- for (let i = 0; i < bytes.length; i++) {
- binary += String.fromCharCode(bytes[i]);
- }
- return btoa(binary);
- }
Code (JavaScript) : Убрать нумерацию
- // Вспомогательная функция CRC32
- function crc32(data) {
- const table = [];
- for (let i = 0; i < 256; i++) {
- let c = i;
- for (let k = 0; k < 8; k++) {
- c = (c & 1) ? 0xEDB88320 ^ (c >>> 1) : c >>> 1;
- }
- table[i] = c;
- }
- let crc = 0xFFFFFFFF;
- for (let i = 0; i < data.length; i++) {
- crc = table[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8);
- }
- crc = (crc ^ 0xFFFFFFFF) >>> 0;
- return new Uint8Array([
- (crc >>> 24) & 0xFF,
- (crc >>> 16) & 0xFF,
- (crc >>> 8) & 0xFF,
- crc & 0xFF
- ]);
- }
Code (JavaScript) : Убрать нумерацию
- // Запуск основной функции
- (async () => {
- // Длина "Hello, world!"
- const width = 13;
- const pngBytes = buildPngFromDeflate(deflateBytes, width);
- const dataUrl = 'data:image/png;base64,' + uint8ToBase64(pngBytes);
- const img = new Image();
- img.onload = () => {
- const canvas = document.createElement('canvas');
- canvas.width = img.width;
- canvas.height = img.height;
- const ctx = canvas.getContext('2d');
- ctx.drawImage(img, 0, 0);
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
- const original = new TextDecoder().decode(
- new Uint8Array(imageData.data.buffer, 0, canvas.width)
- );
- // "Hello, world!"
- console.log('Распаковано:', original);
- };
- img.src = dataUrl;
- })();
Сейчас все гораздо проще. Современные браузеры предоставляют прямой доступ к бинарным данным и встроенные инструменты для работы с ними:
Code (JavaScript) : Убрать нумерацию
- fetch('file.zip')
- .then(response => response.arrayBuffer())
- .then(buffer => {
- // Используем JSZip или другие библиотеки
- return JSZip.loadAsync(buffer);
- })
- .then(zip => {
- // Доступ к файлам напрямую
- return zip.file("document.txt").async("string");
- });
Code (JavaScript) : Убрать нумерацию
- const decompressed = await new Response(
- compressedStream.pipeThrough(
- new DecompressionStream('deflate')
- )
- ).arrayBuffer();
Просмотров: 211 | Комментариев: 1
Метки: JavaScript
Комментарии
Отзывы посетителей сайта о статье
Добавить комментарий
Заполните форму для добавления комментария



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