[Перевод] Один пиксель вместо тысячи слов
Пару месяцев назад, отдыхая от реализации новых возможностей вроде q_auto и g_auto, я прикалывался в нашем командном чате по поводу того, как различные форматы хранения изображений будут сжимать однопиксельную картинку. В ответ Orly, редактор блога, попросил меня написать пост об этом. Я сказал: «Конечно, почему бы и нет. Но это будет очень короткий пост. Ведь что можно рассказать про один пиксель».
Похоже, я был сильно неправ.
Что можно сделать с одним пикселем?В ранние годы веба однопиксельные картинки часто использовались как костыли для вещей, которые сейчас делаются через CSS. Создание отступов, линий, прямоугольников, полупрозрачных фонов — много чего можно сделать, просто масштабируя пиксель до нужных размеров. Ещё одно использование пикселей, дожившее до наших дней — маячки, средства для отслеживания и аналитики.
В отзывчивом веб-дизайне однопиксельные картинки используются как временные заглушки в ожидании загрузки страницы. Большинство браузеров не поддерживают HTTP Client Hints, поэтому некоторые варианты с отзывчивыми изображениями ждут полной загрузки страницы, чтобы подсчитать актуальный размер картинок, а затем заменяют однопиксельные картинки нужными изображениями при помощи JavaScript.
Сломанная картинка
Есть и ещё одно применение однопиксельных картинок: их можно использовать в качестве картинок «по умолчанию». Если нужное изображение по каким-то причинам невозможно найти, в некоторых случаях лучше показать один прозрачный пиксель, чем выдавать »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. Я, конечно, не могу забыть о нём, являясь главным автором бесплатного формата изображений без потери качества (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).
Вот сравнительная табличка с оптимальными размерами файлов для разных однопиксельных картинок:
Может показаться странным, что формат без сжатия выигрывает у форматов со сжатием. Но если подумать, однопиксельные картинки — это наихудший вариант для сжатия изображений. Весь файл состоит из заголовка и дополнительной информации, и в нём очень мало данных. А очень мало данных нельзя сжать, поскольку сжатие основано на предсказуемости, и как можно предсказать единственный пиксель?
Комментарии (2)
22 июля 2016 в 11:52
+2↑
↓
Пока читал текст до «сломанной картинки» нервничал, что она все не загружается и внутренне возмущался, что автор добавил битую ссылку22 июля 2016 в 11:55
+4↑
↓
Я сам нервничал каждый раз.