Пару месяцев назад, отдыхая от реализации новых возможностей вроде q_auto и g_auto, я прикалывался в нашем командном чате по поводу того, как различные форматы хранения изображений будут сжимать однопиксельную картинку. В ответ Orly, редактор блога, попросила меня написать пост об этом. Я сказал: «Конечно, почему бы и нет. Но это будет очень короткий пост. Ведь что можно рассказать про один пиксель».
Похоже, я был сильно неправ.
Что можно сделать с одним пикселем?
В ранние годы веба однопиксельные картинки часто использовались как костыли для вещей, которые сейчас делаются через CSS. Создание отступов, линий, прямоугольников, полупрозрачных фонов – много чего можно сделать, просто масштабируя пиксель до нужных размеров. Ещё одно использование пикселей, дожившее до наших дней – маячки, средства для отслеживания и аналитики.
В отзывчивом веб-дизайне однопиксельные картинки используются как временные заглушки в ожидании загрузки страницы. Большинство браузеров не поддерживают HTTP Client Hints, поэтому некоторые варианты с отзывчивыми изображениями ждут полной загрузки страницы, чтобы подсчитать актуальный размер картинок, а затем заменяют однопиксельные картинки нужными изображениями при помощи JavaScript.
Сломанная картинка
Есть и ещё одно применение однопиксельных картинок: их можно использовать в качестве картинок «по умолчанию». Если нужное изображение по каким-то причинам невозможно найти, в некоторых случаях лучше показать один прозрачный пиксель, чем выдавать «404 — Not Found», которая будет видна в браузерах как «сломанная картинка». Нужное изображение вы в любом случае не увидите, но профессиональнее будет не акцентировать на этом внимание, выдавая иконку «сломанной картинки».
Хорошо, значит, однопиксельные картинки бывают полезными. И как же наилучшим образом закодировать изображение размера 1х1?
Очевидно, что для форматов сжатия изображений это пограничный случай. Если изображение состоит из одного пикселя, сжимать тут особенно нечего. Несжатых данных тут будет содержаться от одного бита до четырёх байт – в зависимости от интерпретации: черно-белый (1 бит), оттенки серого (1 байт), оттенки серого с альфой (2 байта), RGB (3 байта), RGBA (4 байта).
Но нельзя закодировать только лишь данные – в любом формате изображений нужно задать интерпретацию данных. По меньшей мере, нужно знать высоту и ширину изображения и количество бит на пиксель.
Заголовки
Обычно для кодирования высоты и ширины используется четыре байта: два на число (если бы это был один байт, то максимальная размерность картинки была бы 255x255). Допустим, нужен ещё байт для задания типа цветопередачи (оттенки серого, 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» значит один байт на канал. Следующие четыре байта – размерность картинки, 0x0001 на 0x0001. Следующие 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).
Вот сравнительная табличка с оптимальными размерами файлов для разных однопиксельных картинок:
Может показаться странным, что формат без сжатия выигрывает у форматов со сжатием. Но если подумать, однопиксельные картинки – это наихудший вариант для сжатия изображений. Весь файл состоит из заголовка и дополнительной информации, и в нём очень мало данных. А очень мало данных нельзя сжать, поскольку сжатие основано на предсказуемости, и как можно предсказать единственный пиксель?
Комментарии (34)
dmitry_ch
22.07.2016 12:57+7«Женщина из ничего может сделать шляпку, салат и трагедию»
А хороший автор — даже из пикселя, увлекшись, напишет отличный пост.
P.S. Ну что сделает с таким постом «редактор ТМ» — тоже понятно )
Temmokan
22.07.2016 13:17+4Можно вспомнить и про
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">
stas404
22.07.2016 16:12+1Что туда в src только не пробовал пихать народ в свое время.
Начиная с "about:blank
" и "#
", заканчивая всякими "//:0
" и "file://
", варианты в сочетании сopacity:0
, чтобы не было видно «битой картинки» и т.д.
Веселое было времечко.
Isildur
22.07.2016 13:20+8Странно, но забыли классику — формат BMP, у меня он занял 68 байт.
ruikarikun
22.07.2016 16:41+11Вы, вероятно, выбрали двухцветный BMP, в котором 8 байт тратится на палитру, а сам файл округляется до длины, кратной 4 байтам. В данном случае выгоднее использовать полноцветный, 32-битный или 24-битный BMP без палитры. У меня такой BMP занимает уже 58 байт, даже с поддержкой полупрозрачности.
Но и это ещё не всё! Современный софт использует заголовки BMP версии 3 и выше, потому что только они поддерживают 32-битный цвет, но для 24бит/пиксель можно также использовать и BMP старой версии, с BITMAPINFO версии BITMAPCOREHEADER (12 байт), а не BITMAPINFOHEADER (40 байт). Так можно сэкономить ещё 28 байт.
Итого, вот BMP 24bpp 1х1, размером 30 байт:
42 4D 1E 00 00 00 00 00 00 00 1A 00 00 00 0C 00
00 00 01 00 01 00 01 00 18 00 FF FF FF 00
DeadKnight
22.07.2016 13:26+7Напомнило старую умозрительную задачу о том, как при помощи палки длинной 1 м и ножа записать большой текст одним движением.
Решение сводилось к тому. что все буквы переводились в числа, соединялись, в начало добавлялся «0.» и получалась дробь. По итогу ножом делалась метка на палке в месте соответствующем этой дроби.dmitry_ch
22.07.2016 14:41+3Точность метода (точнее, возможность точного восстановления текста) вызывает глубокое сомнение.
У ножа, грубо говоря, и то толщина есть. У линейки — точность нанесения рисок (но мы, скажем, возьмем ту же линейку и для восстановления), и ширина самих рисок. Установить линейку можно так же, точно в «начале палки», или с еле заметным сдвигом. Сдвиг может быть и при попытке поставить метку ножом. Ну и, конечно, нужно где-то хранить число «букв» в записанной фразе, а то «точность измерения» — штука такая, можно много знаков после запятой писать )
Ну и как точно отмерять 0.5789432687357483905 м. с такими допущениями… А уж, в сравнении с ним, 0.5789432687357483905574832967589432675439679443214 насколько труднее окажется к воспроизведению? )
А если говорить о числе записанных символов, то это будет как китайская флешка с безлимитной емкостью — писать-то можно, а вот прочесть не получится.RomanArzumanyan
22.07.2016 15:31Чисто из занудства — штангенциркуль даёт точность измерения порядка 0.01 мм. Итого получаем 100к различимых рисок на палке. Если применить ещё какой-нибудь препроцессинг (типа кодов Голомба для сжатия видео), то получим вполне неплохой текст.
dmitry_ch
22.07.2016 15:36+2Не сравнивайте точность измерения и точность, с которой вы считаете данные сами. Хотя бы потому, что риски не в заранее заданных точностью штангенциркуля (!!) точках будут стоять, а в произвольном месте палки будет зарубка ножом. «Аналоговая» такая запись, теплая-ламповая :)
Это сокрее вопрос вероятностный, и, скорее всего, первые цифры (не буквы текста, а именно цифры считанного значения) будут точны с высокой вероятностью, то каждая следующая цифра будет все менее вероятно истинной. И что 20-й знак после запятой у вас 4, а не 5 — это уж вопрос веры окажется.RomanArzumanyan
22.07.2016 16:01Закодировал число, отмерил его на штангенциркуле, закрутил гайку. Приложил к палке, царапнул палку лапкой штангенциркуля. Даже нож не нужен. Если декодирование производится тем же самым штангелем, то можно схлопотать ошибку в последнем знаке — согласен.
Пример из жизни — при помощи такого же инструмента (штангель + глубиномер) в домашних условиях юстируется рабочий отрезок фотоаппарата. Точность измерений там похожая — до долей миллиметров. Процесс исчерпывающе описан в книге Майзенберга.dmitry_ch
22.07.2016 16:09Это у вас штангель метровой длины?
Усугубить легко: считывать тем же штангелем — это хорошая хохма, но давайте тогда и показания прямо на нем не сбрасывать, а? Прямо установили 12 см 37.14 мм, риску на палке нарисовали для проформы (но не сильно, чтобы не сдвинуть показания), и быстрее записали свои «0.123714» на бумажку, пока штангель не «ушел» и пока число в голове. ) Тепличные условия — они много гипотез потвердили, но мы-то все же понимаем, что такое практика, правда?
А если серьезно, как вы штангелем с точностью 0.01 мм отметите хотя бы 50 знаков после запятой на деревянном бруске, так, чтобы кто-то (лучше другой человек, но пусть даже вы сами) же через время (скажем — через день), даже этим же, а лучше другим штангелем прочитаете их с приличной точностью (ну, скажем, чтобы только последний символ текста, а не цифровой знак, как вы предположили) был неуверенно считан?RomanArzumanyan
22.07.2016 16:1950 знаков после запятой — никак. 100к различимых значений дают диапазон величин от 0 до 1 с точностью до 0.00001, а это всего 5 знаков. На палке метровой длины это соответствует точности 0.01 мм, которую даёт измерительный прибор. Читайте внимательно.
dmitry_ch
22.07.2016 17:02Вы уже определитесь, вы сами берете точность 0.01 мм, из нее вычисляете зачем-то число различимых рисок в виде 100К, затем из 100К делаете вывод про точность в 0.00001 (чего?). На отрезке в метр, оно, наверное, и неплохо, но а) как штангелем мерять метровую палку, б) как вы нанесете вполне себе физическим маркером (ножом, он есть в условии задачи) риску так, чтобы удалось ее отличить от (ваше число) 100К точно таких же на отрезве в метр? Риска ваша должна быть, тогда, шириной 0.01 мм, и вы должны с похожей точностью потом ее положение считать.
Хорошо, так сколько знаков «на палке» можно записать, чтобы их потом можно было бы прочитать? 5, как я вашу мысль понял? Т.е. на метре палки смогли записать 5 букв? Там в задаче про «большой текст» речь идет )RomanArzumanyan
22.07.2016 17:52Объясняю:
- Штангель даёт точность измерения 10^-5 м. На метровой палке это 10^5 различимых положений риски.
- Одному концу палки ставим в соответствие число 0.00000, второму — число 0.99999. Любая риска на палке представима в виде десятичной дроби: 0.12375 — риска на расстоянии 12.375 см от начала палки.
- Кодируем входной текст арифметическим кодером. Получаем десятичную дробь, например 0.32974. Делаем риску на расстоянии 32.974 см от начала палки (или обрезаем палку, что гораздо проще, т. к. не нужно будет потом искать риску; кроме того, это не нарушает условий исходной задачи).
Длина текста, который можно закодировать, записит от распределения вероятностей символов алфавита.
Метровый штангель в природе есть, например ШЦ-1000. Это, конечно, дичь (и стоит около 12к), но он существует.dmitry_ch
22.07.2016 18:08Отпилить нельзя, там условие как раз «при помощи палки длинной 1 м и ножа записать большой текст одним движением» — вы не отпилите палку одним движением ножа )
Нож у вас какой ширины? Я бы предложил как вариант 0,3-0.5 мм взять. Раз так, что можно о «точности ножа» говорить, а не о точности штангеля.
По сути, к тому и прошли: «большой текст» одной меткой записать трудно.Hardwar
24.07.2016 20:14+1Если идти в практический аспект, не нарушающий условия задачи, то текст можно записать прописью, «в одно движение».
Denai
22.07.2016 13:27-5PNG с одним белым пикселем не обязательно будет 67 байт занимать, скорее всего он будет значительно больше. Например paint.net сохраняет такую картинку в файл размером 156 байт, mspaint выдаёт 119 байт, 67 становится уже после обработки.
Nixx
22.07.2016 13:51Отличный пост! Уже несколько месяцев не могу разобраться с одной проблемой, и тут вы :) Может кто поможет разобраться?
Есть железка, в которую можно по UDP загружать и скачивать изображения. Самое странное, при скачивании изменяется формат загруженного изображения, в итоге приходят такие данные:
Первый файл:
HEX bytes (24):
fefefefefefefefe00000000000fd2003ac800403ac80040
Известно: размер изображения 1920х1080, цвет полностью черный (#000000)
Второй файл:
HEX bytes (24):
fefefefefefefefe00000000000fd2003ac660fa3acefcfa
Известно: размер изображения 1920х1080, цвет полностью красный (?#?FF0000?)
Примерные форматы: rec 709, YUV, 4:2:2
Fedcomp
23.07.2016 09:38+2многие веб сервисы пересоздают картинку при загрузке чтобы избавиться от эксплойтов, rarjpg и прочего.
SabMakc
22.07.2016 14:05WBMP будет еще меньше. Всего 5 байт: 00 00 01 01 00.
Правда, не уверен в поддержке формата браузерами.Gitkan
22.07.2016 16:22Проверил в Firefox и Chrome — не открывают, предлагают скачать и сохранить как любой другой неизвестный бинарник.
zemavo
23.07.2016 11:49Как раз хотел про него написать, вспомнив старые телефоны. Интересно, а что бы SVG показал?
Tutanhomon
22.07.2016 17:07Я, правда, не знаю как у вас там в браузерах, но у нас в играх не используют однопиксельные изображения, потому что видеокарта все равно будет одинаково производительна как на 32, так и на 1 пиксельной картинке(если не ошибаюсь, она все равно будет растянута до 32х32, для оптимизации памяти)
Кроме того, для PVRTC компрессии минимальный размер — 4х4.
Вроде же современные браузеры работают с GPU…
Допускаю конечно, что глупость сморозил, но все же :)
GreenBee
Пока читал текст до «сломанной картинки» нервничал, что она все не загружается и внутренне возмущался, что автор добавил битую ссылку
SLY_G
Я сам нервничал каждый раз.
iassasin
Ничего страшного не произойдет, это читерная картинка. Даже если не загрузится, браузер подставит корректное изображение.