Открыть png как бинарный файл

Обновлено: 06.07.2024

Привет! В этой статье я познакомлю вас с устройством изображений в формате PNG, а так же с механизмом работы алгоритма Deflate. Мы на примере рассмотрим все аспекты этой темы. Надеюсь, вам будет интересно.

Дисклеймер

Пример изображения

example-max-min

Долой длинные вступления. Начинаем!

Структура PNG

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

Любой файл PNG начинается со следующей последовательности байтов (в шестнадцатеричном представлении):

Наиболее важными для нас являются 9 и 10 байты. Давайте рассмотрим их чуть подробнее.

Color type

Используемая цветовая модель. Может принимать следующие значения:

Bits per pixel
Color option Channels BIts per channel
1 2 4 8 16
Grayscale 1 1 2 4 8 16
RGB 3 24 48
Indexed 1 1 2 4 8
Grayscale and alpha 2 16 32
RGBA 4 32 64

Глубина цвета

То, что я написал выше, очень важно запомнить или записать, так как эта информация обязательно пригодится нам при расшифровке чанка IDAT.

В нашем случае чанк IHDR является следующей строкой (в шестнадцатеричном представлении):

Что же это означает? Color type равняется 2, значит используется цветовая модель RGB, т.е. комбинация красного, зелёного и синего каналов. Глубина цвета равняется 16, а значит каждый из этих каналов закодирован парой байтов. Отсюда также несложно понять, что для кодирования одного пикселя изображения используется 48 бит или 6 байт. Идём дальше.

  1. Zlib header, содержащий информацию о методе, степени сжатия и некоторую служебную информацию.
  2. Последовательность блоков со сжатой информацией, следующих друг за другом.
  3. Последовательность битов, представляющих собой контрольную сумму несжатых данных, сгенерированную при помощи алгоритма Adler-32

Давайте немного подробнее.

Zlib header

Спецификация RFC 1950 говорит нам, что zlib header должен состоять из двух байтов. Первый байт – это Compression Method and Flags (CMF), который в свою очередь содержит:

  1. Compression Level (FLEVEL) – степень сжатия.
  2. Preset Dictionary (FDICT) – Если он равен 1, то за байтом FLG должен идти словарь. Если я правильно понял, то это просто последовательность несжатых байтов, которая необходима для кодирования/декодирования содержимого. Словарь полезно использовать при небольших размерах файла.
  3. Check bits for CMF and FLG (FCHECK) – число, которое дополняет CMF * 256 + FLG до такого значения, чтобы оно было кратно 31.

Схематично Zlib header можно изобразить так:

CMF FLG DICT (IF FDICT === 1)
CINFO CM FLEVEL FDICT FCHECK
0111 1000 11 0 11010

В нашем случае: 78 DA или в двоичном представлении 0111100011011010:

Нижними подчёркиваниями я обозначил любые значения, ибо они нам в данный момент безразличны.
Как видим, наше значение не попадает в первый промежуток. Идём дальше:

Таблица фиксированных кодов Хаффмана
Коды символов Длина Диапазон
0-143 8 от 00110000 до 10111111
144-255 9 от 110010000 до 111111111
256-279 7 от 0000000 до 0010111
280-287 8 от 11000000 до 11000111

Таблицы длин и обратных смещений

Тут тоже нет ничего сложного. Если код, который мы высчитали по предыдущей таблице попадает в промежуток от 257 до 285 (то есть, 3 и 4 случаи, исключая код 256, 286 и 287), то наше итоговое значение будет высчитываться по длине и обратному смещению. Но что же это? А это и есть результат работы Deflate. Название таблицы говорит само за себя. Когда мы натыкаемся на подобную команду, нужно отступить назад на количество битов, равное обратному смещению и взять количество битов, равное длине.

Таблица длин
Коды Доп. биты Диапазон длин
257-264 0 3-10
265-268 1 11-12, 17-18
269-272 2 19-22, 31-34
273-276 3 35-42, 59-66
277-280 4 67-82, 115-130
281-284 5 131-162, 227-257

Код 285 обозначает длину 258

Таблица обратных смещений:

Таблица смещений
Коды Доп. биты Диапазон смещений
0-3 0 1-4
4,5 1 5-6, 7-8
6,7 2 9-12, 13-16
8,9 3 17-24, 25-32
10,11 4 33-48, 49-64
12,13 5 65-96, 97-128
14,15 6 129-192, 193-256
16,17 7 257-384, 385-512
18,19 8 513-768, 769-1024
20,21 9 1025-1536, 1537-2048
22,23 10 2049-3072, 3073-4096
24,25 11 4097-6144, 6145-8192
26,27 12 8193-12288, 12289-16384
28,29 13 16285-24576, 24577-32768

Логика здесь точно такая же. После того, как мы нашли значение длины, считываем еще 5 битов (от младшего к старшему, естественно), переводим в десятичное представление для удобства. Этот код находим в таблице, используем дополнительные биты при необходимости и находим обратное смещение. Поехали дальше, начнём разбирать наш код.

Приступаем

Для удобства переведём всю последовательность в двоичный код (можно написать программу для этого, можно сделать вручную, а можно воспользоваться калькулятором, которых в Интернете полно; лично я предпочитаю первый вариант). Итак, у нас получается код (не забывайте про нули, их здесь может и не быть, но каждый байт обязательно является двоичным восьмибитным числом):

01100010 01011000 10110001 1010100 1000001 10100001 10100000 11100000 11111111 11111111 11111111 11111111 11111111 11111111 1100111 10000000 1010000 10000 | 100001 01000000 00000000 00000000 00000000 11111111 11111111 11011100 01000111 00010000 11001111 1001001 1000101 1001110 1000100

Выглядит эффектно, правда? На самом деле, тут всё просто. Начинаем:

2. На этом этапе нужно брать минимум 7 битов, определять префикс. Далее уже переводить в число, либо считать смещения и длины. Что же мы стоим? Вперёд!

Таким же образом проделываем всю остальную работу. Мы помним, что для кодирования 1 пикселя в нашем случае используется 6 байтов. Считаем их всех, а потом ненадолго остановимся:

Символами \(\lceil\) \(\rceil\) обозначают потолок, то есть округление до целого в большую сторону.

Получилось, что R в нашем случае равен 168. Если мы проделаем то же самое с G и B-каналами, то получим, что цвет нашего первого пикселя в RGB-представлении равен (168, 32, 112). Теперь посмотрим на скриншот:

Скрин

Мы движемся в верном направлении. А это значит, что нет смысла останавливаться! Второй пиксель декодируем так:

Значение попадает в третий промежуток, давайте разбираться. По формуле получим следующий код:

Тем не менее, далее следует блок, который имеет нулевую длину. Честно признаюсь, я не до конца осознал целесообразность его нахождения здесь. Продолжим.

Судя по всему, начало нового блока? Ладно. Сжатия нет, блок последний.

Ну и наконец, в самом конце чанка IDAT следует четырёхбайтовая хеш-сумма несжатых данных, высчитанная по алгоритму Adler-32. В нашем случае, хеш-сумма в двоичном представлении выглядит так:

Adler-32

Последовательность (в десятичном представлении):

0 168 165 32 32 112 112 255 255 255 255 255 255 0 255 255 255 255 255 255 168 165 32 32 112 112

Я набросал на PHP простенький код:

$array = explode(' ', $uncompressed);

$start_1 = 1;
$start_2 = 0;

foreach($array as $num)

$temp_1 = $start_1 + trim($num);
$temp_2 = $start_2 + $temp_1;

$start_1 = $temp_1;
$start_2 = $temp_2;

$hash = sprintf("%016b", $temp_2 % 65521) . sprintf("%016b", $temp_1 % 65521);

Переменная $hash после выполнения кода содержит строку:

Найдите 10 отличий. Хеш-суммы совпадают, а поэтому с полной уверенностью заявляю, что мы сделали всё верно! Со следующего байта уже начинается чанк IEND, с IDAT же мы наконец-то закончили.

Стоит признать, что это было довольно просто. В пункте про алгоритм Deflate мы также рассмотрим декодирование строки (да, для примера возьмем обычную строку) с динамическими кодами Хаффмана.

Обязательный чанк, который должен следовать после всех остальных. Символизирует окончание PNG-документа и не содержит какой-либо информации.

Deflate

Алгоритм Хаффмана

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

2. Сортируем его по частоте и создаем очередь. Приоритетом является частота. При этом, элементы с самым низким приоритетом выстраиваются в самом начале:

3. Теперь создаем бинарное дерево. Алгоритм такой: Берем первые два элемента и формируем из них мини-дерево, где листьями будут наши элементы. В таблицу записываем сумму приоритетов, помещая его в нужное место:

Таким же образом делаем со всеми остальными. В итоге получается такое дерево:

Теперь нужно заменить нашу последовательность полученными кодами и выстроить таблицу, чтобы интерпретатор мог без проблем её декодировать. В принципе, это всё.

Ближе к теме

Я уже выше писал, но напомню еще раз. Данные состоят из блоков, следующих друг за другом последовательно. Каждый из них содержит заголовок, состоящий из 3 битов (RFC 1950):
1 бит – BFINAL. Устанавливается в 1, если данный блок последний.
2 бита – BTYPE. Содержит информацию о типе сжатия:

  1. 00 – без сжатия
  2. 01 – сжатие с фиксированными кодами Хаффмана
  3. 10 – сжатие с динамическими кодами Хаффмана
  4. 11 – зарезервированное значение. Установка его вызовет ошибку.

Для урока я взял простую последовательность, которая не включает в себя длины и смещения. Тем не менее, работать с подобными данными мы уже умеем, так как это было рассмотрено выше, а метод сжатия значения не имеет, ибо всё происходит точно так же. Также уточню, что строка ниже не содержит заголовка zlib и контрольной суммы. Мы рассматриваем только Deflate.

Пример Deflate с динамическими кодами Хаффмана

Предположим, у нас есть следующая последовательность:

05 A0 61 0B 00 00 18 86 2E 68 8F EA D9 3D AE C7 B4 9B 10 E8 FF 40 21 07 14 56 5A DA

В двоичном представлении:

00000101 10100000 01100001 00001011 00000000 00000000 00011000 10000110 00101110 01101000 10001111 11101010 11011001 00111101 10101110 11000111 10110100 10011011 00010000 11101000 11111111 01000000 00100001 00000111 00010100 01010110 01011010 11011010

Давайте ненадолго остановимся и рассмотрим всё это. Блок с динамическими кодами состоит из следующих частей (исключая заголовок и описание длин):

Внимательность: в первой части мы получаем лишь длины, которые в последствии пригодятся при декодировании. Мы получаем длины тем же способом, что я описал выше.

2. Далее идёт последовательность из HLIT + 257 команд и значений закодированных символов, которые можно найти, исходя из их позиции в потоке данных (на примере я объясню этот момент). В предыдущем блоке мы получили их длины. Теперь же мы сможем сопоставить распакованное значение и длину кода, которым оно закодировано. Используя приведённый выше алгоритм, зная длины кодов и их значения, мы без проблем найдем сами коды.

3. Последним идёт блок со сжатыми данными. На этом этапе мы уже знаем коды и их значения (или длины и смещения). Поэтому без проблем можем декодировать строку.

Если у вас каша в голове, то не переживайте, сейчас на примере мы всё разберём. Поехали!

Итак, берём HCLEN + 4 (15) трёхбитовых значений:

Отбросим все значение, где длина равна 0 и переведём в десятичную систему:

Теперь восстанавливаем коды длин:

Затем у нас следуют 257 длин и команд. Здесь мы и применяем наши длины символов и команд. Давайте я покажу как найти значение.

Читайте также: