Разбираем самый маленький PNG в мире

в 10:00, , рубрики: Deflate, PNG, ruvds_перевод, обработка изображений, сжатие изображений, структура png

Разбираем самый маленький PNG в мире - 1


Самый миниатюрный PNG в мире весит 67 байт и представляет собой один чёрный пиксель. Выше вы видите его в 200-кратном увеличении.

Красота, не так ли?

Состоит этот файл из четырёх частей:

  1. Сигнатура PNG, одинаковая во всех файлах этого формата: 8 байт.
  2. Метаданные изображения, включая его размеры: 25 байт.
  3. Данные пикселя: 22 байта.
  4. Маркер «конец изображения»: 12 байт.

Далее я опишу этот файл подробнее и постараюсь объяснить принцип работы формата PNG.

В качестве небольшой затравки скажу, что в конце предстоит неожиданный поворот. Хотя, надеюсь, вам и без того интересно побольше узнать о PNG.

▍ Часть 1: сигнатура PNG

Любой PNG-файл начинается с одинаковых 8 байт, которые в шестнадцатеричной кодировке выглядят так:

89 50 4E 47 0D 0A 1A 0A

Эта запись называется сигнатурой PNG. Попробуйте получить шестнадцатеричный дамп любого PNG, и вы увидите в его начале именно эти байты.

Декодеры PNG с помощью этой сигнатуры убеждаются, что считывают именно изображение PNG. Если они не обнаруживают в начале эту подпись, то отклоняют его. Данные могут оказаться по разным причинам повреждены (сталкивались когда-нибудь с файлом, имеющим неверное расширение?), и эта проверка помогает обработать подобные нюансы.1

Занятный факт: если вы декодируете эти байты в виде ASCII, то увидите буквы PNG:

.PNG....

Итак, это первые 8 байт. С одной частью разобрались. Вернёмся к нашему чеклисту:

  1. Сигнатура PNG.
  2. Метаданные изображения.
  3. Данные пикселей.
  4. Маркер «конец изображения».

Продолжим.

Часть 2: метаданные изображения

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

▍ Краткое знакомство с чанками

Помимо начальной сигнатуры, PNG состоят из чанков.

Чанк, в свою очередь, можно условно разделить на две логические части, представляющие его тип и байты данных. Типы – это элементы вроде «image header» (заголовок изображения) или «text metadata» (текстовые метаданные). При этом содержимое данных зависит от их типа – например, текстовые метаданные кодируется иначе, нежели заголовок изображения.

Далее. Эти две логические части описываются четырьмя полями, всегда расположенными в одном порядке:

  1. Length (длина): количество байтов в поле данных чанка (пункт #3 ниже). Кодируется в виде 4-байтового целого числа.2
  2. Chunk type (тип чанка): существуют разные типы чанков. Кодируется в виде 4-байтовой строки ASCII, например IHDR, то есть «image header», или tEXt, «text metadata».
  3. Data: данные чанка. Количество занимаемых ими байтов указывается в поле Length. Формат данных отличается в зависимости от типа чанка. Например, в чанке IHDR кодируются размеры изображения. Это поле может быть пустым, но обычно нет.
  4. Checksum: контрольная сумма для остальной части чанка, гарантирующая целостность данных. Размер 4 байта.

Как видите, каждый чанк имеет размер минимум 12 байт (4 для длины, 4 для типа и 4 для контрольной суммы).

Имейте ввиду, что поле Length – отражает размер поля «данных», а не всего чанка. Если вы хотите узнать весь его размер, просто прибавьте 12 – 4 байта для длины, 4 для типа и 4 для контрольной суммы.

Среди чанков возможны колебания в размере, но расположены они всегда в одном порядке. Например, метаданные изображения должны идти до данных пикселя. При достижении чанка «конец изображения» PNG заканчивается.

В нашем крохотном PNG будет всего три чанка из перечисленных.

▍ Заголовок изображения

Первым чанком каждого PNG, включая наш, идёт тип IHDR.
При этом каждый чанк начинается с указания длины содержащихся в нём данных.

Как мы вскоре увидим, IHDR всегда имеет 13 байт связанных с ним данных. В шестнадцатеричном виде 13 представляет значение 0D, кодируемое так:

00 00 00 0D

Далее идёт тип чанка, занимающий очередные четыре байта. IHDR кодируется так:

49 48 44 52

Это просто кодировка ASCII. Типы чанков состоят из букв ASCII, причём регистр имеет значение. Например, первая буква в верхнем регистре означает, что этот чанк является обязательным.

Далее идут данные. Общий размер данных IHDR составляет 13 байт, упорядоченных таким образом:

  • Первые восемь байт кодируют ширину и высоту изображения. Поскольку мы рассматриваем картинку 1х1, то эти байты выглядят так: 00 00 00 01 00 00 00 01.
  • Следующие два байта – это битовая глубина и цветовая модель.

Эти значения, пожалуй, являются самой запутанной частью нашего PNG.

Существует пять возможных цветовых моделей. У нас изображение чёрно-белое, поэтому мы используем greyscale (кодируется как 00). Если в изображении будет цвет, можно будет использовать тип truecolor (кодируется как 02). Есть ещё три типа. В данном случае они не актуальны, но для ознакомления можете почитать про них в спецификации PNG.

После выбора цветовой модели вам нужно выбрать битовую глубину. Битовая глубина зависит от цветовой модели, но обычно означает число битов на один цветовой канал изображения. Например, шестнадцатеричные цвета вроде #FE9802 имеют битовую глубину восемь – восемь бит для красного, восемь для зелёного и восемь для синего. Для нашего чёрно-белого изображения всё это лишнее…ему достаточно одного бита. Его пиксель может быть либо абсолютно чёрным (0), либо абсолютно белым (1) – в нашем случае он чёрный.

Если бы мы выбрали более «экспрессивную» цветовую модель и битовую глубину, то получили бы визуально такое же изображение 1х1, но файл стал бы больше, потому что на пиксель уже приходилось бы больше битов, которые по факту нам не нужны. Например, если использовать модель truecolor и 16 бит на канал, то каждый пиксель потребует 48 бит вместо одного – что совсем не нужно для кодирования «абсолютно чёрного».

При битовой глубине 1 и цветовой модели 0 мы кодируем эти два значения с помощью 00 01.

  • Следующий байт отражает метод сжатия. Сейчас во всех PNG он устанавливается равным 00. Присутствует он на случай, если позднее нужно будет добавить другой метод сжатия. Насколько мне известно, никто этого не делает.
  • То же касается метода фильтрации. Он всегда обозначен как 00.
  • Последней частью данных чанка является метод интерлейсинга. PNG поддерживают прогрессивное декодирование, которое позволяет рендерить изображения частично в процессе скачивания. Мы эту возможность использовать не будем, поэтому установим данный байт на 00.

Наконец, каждый чанк завершается контрольной суммой, состоящей из четырёх байтов. Для её получения используется популярная функция CRC32. Остальная часть чанка при этом представляет входные данные для этой функции. В нашем случае контрольной суммой будет:

37 6E F9 24

Вот чанк IHDR целиком:

Байты Содержимое
00 00 00 0D длина данных, 13 байт
49 48 44 52 IHDR в качестве ASCII
00 00 00 01 Ширина
00 00 00 01 Высота
01 Битовая глубина
00 Цветовая модель
00 Метод сжатия
00 Метод фильтрации
00 Метод интерлейсинга
37 6E F9 24 Контрольная сумма

Итак, с первым чанком покончено. Вернёмся к списку:

  1. Сигнатура PNG.
  2. Метаданные изображения.
  3. Данные пикселя.
  4. Маркер «конец изображения».

Осталось разобрать два.

Часть 3: чанк данных пикселя

Следующим идёт чанк IDAT, то есть «image data» (данные изображения). В нём кодируются фактические пиксели…в нашем случае один.

Напомню, что каждый чанк состоит из четырёх частей: длина данных, тип чанка, сами данные и контрольная сумма.

В этом чанке будет 10 байт данных, и чуть позже мы разберём каких именно. Закодируем эту длину:

00 00 00 0A

Следующим будет тип чанка:

49 44 41 54

Опять же, это просто ASCII-символы, и я привожу их шестнадцатеричную форму.

А теперь самое интересное: данные изображения.

▍ Первый шаг: несжатые пиксели

Данные изображения кодируются в серию строк сканирования, после чего сжимаются.

Строка сканирования представляет горизонтальную строку пикселей. Например, изображение 123х456 содержит 456 строк сканирования. В нашем случае присутствует всего одна строка, так как высота картинки — один пиксель.

Строки сканирования начинаются с типа фильтра, который может улучшать сжатие в зависимости от обрабатываемого изображения. Наше изображение настолько мало, что это значения не имеет, поэтому мы используем тип фильтра 0. 3

После применения фильтра каждый пиксель, в зависимости от битовой глубины, кодируется с помощью одного или более битов. В нашем случае нам нужен всего один бит на пиксель, так как битовая глубина изображения равна 1 – абсолютно чёрный или абсолютно белый.

Если данные нашего пикселя не выравниваются с границей байта – иными словами, не кратны 8 битам – мы заполняем оставшиеся строки сканирования нулями. В нашем случае так и есть, поэтому мы добавляем семь бит заполнения.

Всё вместе – нулевой байт начала строки сканирования, один нулевой бит и семь нулевых заполняющих битов – формируют нашу строку сканирования:

00 00

Теперь время заняться «сжатием» данных.

▍ Второй этап: «сжатие»

Далее мы будем сжимать данные строки сканирования…хотя, по факту не совсем.

Говоря точнее, мы прогоним их через алгоритм сжатия. В большинстве случаев такие алгоритмы выдают уменьшенную форму исходного материала – в этом и заключается весь смысл. Но иногда при «сжатии» крохотных входных данных на выходе мы получаем увеличенный результат. Происходит это вследствие небольших накладных издержек, и здесь, к сожалению, как раз такая ситуация. Но формат PNG вынуждает нас это делать.

Данные PNG-изображения кодируются в формате zlib с помощью алгоритма DEFLATE. DEFLATE также используется такими популярными форматами, как gzip и ZIP.

Я не буду подробно разбирать здесь принцип работы этого алгоритма (отчасти, потому что я не эксперт в этом вопросе4) и просто перейду к описанию нашего чанка данных:

  1. Заголовок zlib: 2 байта.
  2. Один сжатый блок DEFLATE, кодирующий два нуля5: 4 байта.
  3. Контрольная сумма zlib (она идёт отдельно от контрольной суммы чанка): 4 байта.

Подробнее о механизме DEFLATE можете прочесть в статье «An Explanation of the DEFLATE Algorithm». Также рекомендую infgen, полезный инструмент для инспектирования потоков DEFLATE.

Объединяя всё воедино, мы получаем те самые 10 байт данных:

78 01 63 60 00 00 00 02 00 01

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

Ладно, идём дальше. Теперь мы можем вычислить поле контрольной суммы PNG и закончить с этим чанком.

Байты Содержимое
00 00 00 0A длина данных, 10 байт
49 44 41 54 IDAT в качестве ASCII
78 01 Заголовок zlib
63 60 00 00 “Сжатый” блок DEFLATE
00 02 00 01 Контрольная сумма zlib
73 75 01 18 Контрольная сумма чанка

Остался последний чанк. Ещё раз взглянем напоследок на наш чеклист, прежде чем вычеркнуть и его.

  1. Сигнатура PNG.
  2. Метаданные изображения.
  3. Данные пикселя.
  4. Маркер «конец изображения».

▍ Часть 4: конец

В довольно поэтичном стиле изображения PNG завершаются также, как начинаются: небольшим числом константных байтов.

Последним идёт чанк IEND, «image trailer» (хвост изображения). Его нулевая длина кодируется 4 нулями:

00 00 00 00

Затем кодируется тип IEND:

49 45 4E 44

В этих чанках никаких данных нет, поэтому мы просто переходим к контрольной сумме. Поскольку всё остальное в этом чанке константно, контрольная сумма всегда одинакова:

AE 42 60 82

А вот хвостовой чанк целиком:

Байты Содержимое
00 00 00 00 длина данных, 0 байт
49 45 4E 44 IEND в качестве ASCII
AE 42 60 82 Контрольная сумма

На этом наш PNG заканчивается!

▍ Порадуемся проделанной работе

Ещё раз приведу наш пиксель в 200-кратном увеличении:

Разбираем самый маленький PNG в мире - 2

Красавец. Он начинается с классической сигнатуры PNG, за которой следуют фрагмент с метаданными, потом «сжатые» данные пикселя и, наконец, пустой чанк.

И это самый маленький PNG в мире!

…или нет?

▍ Неожиданный поворот: в мире есть и другие рекордсмены

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

До тех пор, пока нам будет удаваться закодировать все данные пикселей изображения в один байт, это изображение будет являться «самым маленьким PNG в мире».

Например, можно закодировать эту чёрную картинку размером 8х1, которая также весит 67 байт:

Разбираем самый маленький PNG в мире - 3

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

Напомню, что в нашем изначальном изображении 1х1 семь бит, по сути, тратились в пустую, заполняясь нулями. Можно это представить так:

Байты Содержимое
0 Чёрный пиксель
0000000 Заполнение

В приведённом же выше изображении 8х1 восемь чёрных пикселей могут кодироваться так:

Байты Содержимое
00000000 Восемь чёрных пикселей

Вместо того, чтобы добавлять дополнительные пиксели, вы можете также добавить цветность. Многие оттенки серого можно закодировать в один байт, и файл всё также сможет претендовать на звание «самый маленький». Например, этот серый пиксель 1х1 тоже имеет размер 67 байт:

Разбираем самый маленький PNG в мире - 4

Опять же, он просто использует весь доступный байт, чего не делает наш чёрно-белый пиксель.

Если вас заинтересовала эта тема, могу порекомендовать статью «The Biggest Smallest PNG» моего бывшего коллеги Джордана Роуза. В ней он разбирает самое большое изображение PNG размером 67 байт: чёрную строку размером 1х2064.

▍ Обобщение

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

Самые маленькие PNG используют минимальное число чанков и минимум данных.

Наши PNG состоят из четырёх частей:

  1. Константной сигнатуры PNG (8 байт).
  2. Чанка IHDR с метаданными (25 байт).
  3. Чанка IDAT с данными изображения (22 байта).
  4. Чанка IEND, хвоста изображения (12 байт).

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

Помимо этого, я создал Single Color Image, генерирующий монохроматические PNG произвольного размера. Например, вы можете сгенерировать с его помощью фиолетовый прямоугольник 12х34. Предполагается, что изображения будут иметь небольшой размер, но я ещё не реализовал самый сложный механизм сжатия, так что вам может потребоваться дополнительно прогонять результаты через PNG-компрессор для максимального уменьшения.

Наконец, у меня есть статья «Whats the Largest Possible PNG?» на тему самого крупного PNG, какой только можно создать. В теории эти файлы не имеют предельного размера, но есть ограничение по количеству пикселей, и многие декодеры тоже накладывают свои рамки.

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

▍ Сноски

  1. Если у вас есть какие-то данные, и вы не уверены, в каком они конкретно формате, взгляните на их первые 8 байт. Если ими окажется сигнатура PNG, то наверняка это PNG и есть. Подробнее читайте в спецификации MIME Sniffing.↩︎
  2. В частности, длина кодируется в виде 4-байтового целого числа с обратным порядком байтов. PNG также накладывают дополнительное ограничение: самый старший бит не используется, поэтому диапазон составляет от 0 до 231−1. Согласно спецификации, это делает формат «пригодным для языков, имеющих трудности с обработкой беззнаковых четырёхбайтовых значений». ↩︎
  3. В техническом смысле тип 0 тоже является типом фильтра, просто очень скучным. ↩︎
  4. Если вы эксперт, пожалуйста, свяжитесь со мной. Я хочу учиться! ↩︎
  5. Блок deflate состоит из 4 байт, или 32 бит. Первый бит указывает на то, что это последний блок DEFLATE. Следующие 2 указывают, что этот блок использует жёстко прописанные коды Хаффмана. Словарь в полезную нагрузку не включён. Следующие 8 бит кодируют буквальный нуль, как и ещё 8 бит за ними. Очередные 7 бит представляют маркер «конец блока». Последние 6 заполняют данные, выравнивая их по границе байта. ↩︎

Скидки, итоги розыгрышей и новости о спутнике RUVDS — в нашем Telegram-канале 🚀

Автор: Дмитрий Брайт

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js