[Перевод] Один пиксель вместо тысячи слов

one_pixel_image_comparison_hexdhump.png

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

Похоже, я был сильно неправ.

Что можно сделать с одним пикселем?
В ранние годы веба однопиксельные картинки часто использовались как костыли для вещей, которые сейчас делаются через CSS. Создание отступов, линий, прямоугольников, полупрозрачных фонов — много чего можно сделать, просто масштабируя пиксель до нужных размеров. Ещё одно использование пикселей, дожившее до наших дней — маячки, средства для отслеживания и аналитики.

В отзывчивом веб-дизайне однопиксельные картинки используются как временные заглушки в ожидании загрузки страницы. Большинство браузеров не поддерживают HTTP Client Hints, поэтому некоторые варианты с отзывчивыми изображениями ждут полной загрузки страницы, чтобы подсчитать актуальный размер картинок, а затем заменяют однопиксельные картинки нужными изображениями при помощи JavaScript.

8bd6345685a2e107ac2a13d940e1e742.png
Сломанная картинка

Есть и ещё одно применение однопиксельных картинок: их можно использовать в качестве картинок «по умолчанию». Если нужное изображение по каким-то причинам невозможно найти, в некоторых случаях лучше показать один прозрачный пиксель, чем выдавать »404 — Not Found», которая будет видна в браузерах как «сломанная картинка». Нужное изображение вы в любом случае не увидите, но профессиональнее будет не акцентировать на этом внимание, выдавая иконку «сломанной картинки».

Хорошо, значит, однопиксельные картинки бывают полезными. И как же наилучшим образом закодировать изображение размера 1×1?

Очевидно, что для форматов сжатия изображений это пограничный случай. Если изображение состоит из одного пикселя, сжимать тут особенно нечего. Несжатых данных тут будет содержаться от одного бита до четырёх байт — в зависимости от интерпретации: черно-белый (1 бит), оттенки серого (1 байт), оттенки серого с альфой (2 байта), RGB (3 байта), RGBA (4 байта).

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

Заголовки
Обычно для кодирования высоты и ширины используется четыре байта: два на число (если бы это был один байт, то максимальная размерность картинки была бы 255×255). Допустим, нужен ещё байт для задания типа цветопередачи (оттенки серого, RGB или RGBA). В таком минималистичном формате однопиксельная картинка занимала бы не менее 6 байт (для белого пикселя), а максимум — 9 байт (для полупрозрачного пикселя произвольного цвета).

Но в заголовках реальных форматов обычно содержится гораздо больше информации. Первые несколько байт любого формата содержат уникальный идентификатор нужный лишь для того, чтобы сообщить, что «Эй! Я — файл вот конкретно такого формата!». Эта последовательность байт также известна, как «волшебное число». К примеру, GIF всегда начинается с GIF87a или GIF89a, в зависимости от версии спецификаций, PNG — с 8-байтной последовательности, включающей PNG, у JPEG есть заголовок, содержащий строку JFIF или Exif, и т.д.

В заголовках может содержаться мета-информация. Это специфичные для данного формата данные, необходимые для раскодирования, определяющие, какой из подвидов формата используется. Некоторые из мета-данных не обязательно нужны для раскодирования, но тем не менее, используются для определения того, как показывать их на экране: цветовой профиль, ориентация, гамма, количество точек на пиксель. Это могут также быть производльные данные — комментарии, временные отметки, отметки об авторских правах, GPS-координаты. Это могут быть необязательные или обязательные данные, в зависимости от спецификации. Конечно, эти данные увеличивают объём файла. Давайте поэтому остановимся на минимальных файлах, откуда удалена вся необязательная информация — или мы будем тратить драгоценные байты на ерунду.

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

Однопиксельные, минимально возможные картинки, показывают, сколько «лишней» информации содержится в формате файла. Смотрим.

Вот шестнадцатеричный дамп 67-байтного PNG-файла с одним белым пикселем.

00000000  89 50 4e 47 0d 0a 1a 0a  00 00 00 0d 49 48 44 52  |.PNG........IHDR|
00000010  00 00 00 01 00 00 00 01  01 00 00 00 00 37 6e f9  |.............7n.|
00000020  24 00 00 00 0a 49 44 41  54 78 01 63 68 00 00 00  |$....IDATx.ch...|
00000030  82 00 81 4c 17 d7 df 00  00 00 00 49 45 4e 44 ae  |...L.......IEND.|
00000040  42 60 82                                          |B`.|

Файл состоит из 8-байтного «волшебного числа» PNG, за которым следует отрезок заголовка IHDR из 13 байт, отрезок с данными об изображении IDAT с 10 байтами «сжатых» данных, и отметка об окончании IEND. Каждый отрезок данных начинается с 4-байтного отрезка с длиной и 4-байтного отрезка-идентификатора, и заканчивается контрольной суммой из 4 байт. Эти три отрезка данных обязательны, так что они в любом случае отъедают 36 байт у 67-байтного файла.

Чёрный пиксель тоже занимает 67 байт, прозрачный — 68, а произвольный цвет RGBA займёт от 67 до 70 байт.

Заголовок у JPEG длиннее. Минимальный однопиксельный JPEG занимает 141 байт, и он не бывает прозрачным, т.к. JPEG не поддерживает альфа-канал.

В смысле заголовков GIF самый компактный из трёх универсальных форматов. Белый пиксель можно закодировать в GIF 35 байтами:

00000000  47 49 46 38 37 61 01 00  01 00 80 01 00 00 00 00  |GIF87a..........|
00000010  ff ff ff 2c 00 00 00 00  01 00 01 00 00 02 02 4c  |...,...........L|
00000020  01 00 3b                                          |..;|

а прозрачный — 43:

00000000  47 49 46 38 39 61 01 00  01 00 80 01 00 00 00 00  |GIF89a..........|
00000010  ff ff ff 21 f9 04 01 0a  00 01 00 2c 00 00 00 00  |...!.......,....|
00000020  01 00 01 00 00 02 02 4c  01 00 3b                 |.......L..;|

Для всех перечисленных форматов можно изготовить и файлы поменьше, которые будут показываться в большинстве браузеров, но они будут сделаны с нарушением спецификаций, так что декодер изображений может в любой момент пожаловаться на то, что файл битый (и будет прав), и показать иконку «сломанной картинки» –, а мы именно её и пытаемся избежать.

Так какой же наилучший формат однопиксельной картинки для веба? Есть варианты. Если пиксель непрозрачный, то GIF. Если прозрачный — тоже GIF. Если полупрозрачный, то PNG, поскольку у GIF прозрачность задаётся только как «да» или «нет».

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

Что же насчёт более экзотических форматов?
Используя формат WebP, выбирайте его версию без потерь качества. Однопиксельная картинка без потери качества в формате WebP занимает от 34 до 38 байт. С потерей — от 44 до 104 байт, в зависимости от наличия альфа-канала. К примеру, вот полностью прозрачный пиксель в 34-байтном WebP без потери качества:
00000000  52 49 46 46 1a 00 00 00  57 45 42 50 56 50 38 4c  |RIFF....WEBPVP8L|
00000010  0d 00 00 00 2f 00 00 00  10 07 10 11 11 88 88 fe  |..../...........|
00000020  07 00                                             |..|

а вот тот же пиксель с потерей качества (по умолчанию) WebP, занимающий 82 байта:

00000000  52 49 46 46 4a 00 00 00  57 45 42 50 56 50 38 58  |RIFFJ...WEBPVP8X|
00000010  0a 00 00 00 10 00 00 00  00 00 00 00 00 00 41 4c  |..............AL|
00000020  50 48 0b 00 00 00 01 07  10 11 11 88 88 fe 07 00  |PH..............|
00000030  00 00 56 50 38 20 18 00  00 00 30 01 00 9d 01 2a  |..VP8 ....0....*|
00000040  01 00 01 00 02 00 34 25  a4 00 03 70 00 fe fb fd  |......4%...p....|
00000050  50 00                                             |P.|

Разница в том, что WebP с потерей качества и прозрачностью хранится как две картинки в одном файле-контейнере: одна картинка с потерей качества, хранящая данные для RGB, и другая, без потери, с данными альфа-канала.

BPG
У формата BPG также есть режимы с потерей из без потери качества, и для него действует обратная закономерность. BPG с потерей хранит 1 пиксель в 31 байте — наименьший показатель из всех:
00000000  42 50 47 fb 00 00 01 01  00 03 92 47 40 44 01 c1  |BPG........G@D..|
00000010  71 81 12 00 00 01 26 01  af c0 b6 20 bc b6 fc     |q.....&.... ...|

BPG без потерь качества занимает 59 байт. Прозрачный пиксель займёт 57 байт в BPG
с потерями и 113 байт в BPG без потерь. Интересно, что в случае с одним белым пикселем BPG выиграет у WebP (31 байт против 38), а с одним прозрачным пикселем WebP выигрывает у BPG (34 байта против 57).

FLIF

А ещё есть FLIF. Я, конечно, не могу забыть о нём, являясь главным автором бесплатного формата изображений без потери качества (Free Lossless Image Format). Вот 15-байтный FLIF для одного белого пикселя:

00000000  46 4c 49 46 31 31 00 01  00 01 18 44 c6 19 c3     |FLIF11.....D...|

А вот 14-байтный для чёрного:

00000000  46 4c 49 46 31 31 00 01  00 01 1e 18 b7 ff        |FLIF11........|

Чёрный пиксель получился меньше, потому что ноль сжимается лучше, чем 255. Заголовок простой: первые 4 байта всегда «FLIF», следующий — человеко-читаемое обозначение цвета и интерлейсинга. В нашем случае это »1», что значит, один канал для цвета (оттенки серого). Следующий байт — глубина цвета.»1» значит один байт на канал. Следующие четыре байта — размерность картинки, 0×0001 на 0×0001. Следующие 4 или 5 — сжатые данные.

Полностью прозрачный пиксель тоже занимает 14 байт в FLIF:

00000000  46 4c 49 46 34 31 00 01  00 01 4f fd 72 80        |FLIF41....O.r.|

В этом случае у нас 4 цветовых канала (RGBA) вместо одного. Можно было бы ожидать, что раздел с данными будет длиннее (всё-таки каналов в четыре раза больше), но это не так: поскольку значение альфа равно нулю (пиксель прозрачный), значения RGB считаются неважными, и их просто не включают в файл.

Для произвольного цвета RGBA файл FLIF может занять до 20 байт.

Хорошо, значит FLIF лидер в категории «один пиксель» в соревновании на кодирование изображений. Если бы ещё это было какое-то важное соревнование:)

Но тем не менее, FLIF не будет лидером. Помните упомянутый мною минималистичный формат? Тот, который закодирует один пиксель в размер от 6 до 9 байт? Такого формата нет, поэтому он в счёт не идёт. Но есть существующий формат, который довольно близко подходит к этому.

Он называется Portable Bitmap format (PBM), и представляет собою несжатый формат изображений из 1980-х. Вот как можно было бы закодировать один белый пиксель в PBM всего 8-ю байтами:

00000000  50 31 0a 31 20 31 0a 30                           |P1.1 1.0|

Да тут и шестнадцатиричный дамп не нужен, этот формат человеко-читаемый. Его можно открыть в текстовом редакторе.

P1
1 1
0

Первая линия (P1) обозначает, что картинка двухцветная. Не оттенки серого, а только два цвета — чёрный (цифра 1) и белый (0). Вторая линия — размерность картинки. А затем идёт разделённый пробелами список чисел, одно число на пиксель. В нашем случае 0.

Если вам нужно что-то другое, кроме чёрного и белого, можно использовать формат PGM для представления одного пикселя любого цвета всего 12-ю байтами, или PPM размером 14 байт. Это всегда меньше, чем соответствующий FLIF (или любой другой формат со сжатием).

В традиционном семействе форматов PNM (PBM, PGM и PPM) не поддерживается прозрачность. Существует дополнение PNM под названием Portable Arbitrary Map (PAM), где есть прозрачность. Но для нас он не подходит из-за многословности. Самый маленький из файлов PAM, представляющий прозрачный пиксель, такой:

P7
WIDTH 1
HEIGHT 1
DEPTH 4
MAXVAL 1
TUPLTYPE RGB_ALPHA
ENDHDR
\0\0\0\0

На последней строке идёт четыре нулевых байта. Всего получается 67 байт. Можно было бы использовать оттенки серого с альфа-каналом вместо RGBA, это бы сберегло два байта в секции данных. Но получится файл из 71 байта, поскольку нужно будет сменить TUPLTYPE с RGB_ALPHA на GRAYSCALE_ALPHA. Кроме того, программе обработки может не понравится MAXVAL 1, и придётся поменять его на MAXVAL 255 (ещё два байта).

В общем, для однопиксельных изображений без прозрачности, самым маленьким будет PNM (от 8 до 14 байт для PNM против от 14 до 18 для FLIF), а с прозрачностью самым мелким будет FLIF (от 14 до 20 байт для FLIF против от 67 до 69 байт для PAM).

Вот сравнительная табличка с оптимальными размерами файлов для разных однопиксельных картинок:

d69b94eda9f95bb54295ed2ac70c22ce.jpg

Может показаться странным, что формат без сжатия выигрывает у форматов со сжатием. Но если подумать, однопиксельные картинки — это наихудший вариант для сжатия изображений. Весь файл состоит из заголовка и дополнительной информации, и в нём очень мало данных. А очень мало данных нельзя сжать, поскольку сжатие основано на предсказуемости, и как можно предсказать единственный пиксель?

Комментарии (2)

  • 22 июля 2016 в 11:52

    +2

    Пока читал текст до «сломанной картинки» нервничал, что она все не загружается и внутренне возмущался, что автор добавил битую ссылку
    • 22 июля 2016 в 11:55

      +4

      Я сам нервничал каждый раз.

© Habrahabr.ru