© Георгиевский Анатолий, 31.08.2003

О том, как я пишу программки: 00000000

Об этом здоровому человеку лучше вовсе не думать

Я писал программку, которая переводит формат HTML в PDF и, между прочим, мне надо было разобраться, как строится формат PDF. И уже совсем частная задача, как картинку из файла в формате PNG встроить в PDF.

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

10: Создавать документы в формате PDF с текстами, рамками и русскими шрифтами я научился за 1.5 дня и 9 кБайт исходного кода, полдня из которых я потратил на то, чтобы понять, что перед потоком DEFLATE (алгоритм упаковки данных) [RFC 1951] надо вставить два лишних байта "хЪ", чтобы Adobe его понял. В моем распоряжении была документация по формату PDF (980 страниц английского текста) [PDF Reference] и несколько готовых файлов PDF. Естественно, документацию я практически не читал - это же не реально. Я просто писал программку.

20: Вставлять картинки PNG в PDF я научился за 3 часа. Дело было утром, и мне совсем не хотелось идти на работу и читать PNG (Portable Network Graphics) Specification [RFC 2083].

21: Я, как водится, открыл файл PDF для просмотра в бинарном виде и стал изучать, как там это у других получается.
Если дальше по тексту я начну приводить выдержки из бинарного потока данных, не пугайтесь, для меня они тоже ничего осмысленного до поры не представляли. Я, как и любой неискушенный читатель, не знал к тому времени, как строится формат PNG. В таких затруднительных ситуациях я сам могу придумать структуру объекта, исходя из того, как бы я это реализовал, будь я на их месте.
В формате PDF файл картинки PNG вставляется не целиком, а по частям. Отдельно вставляется палитра цветов, отдельно вставляется запакованный DEFLATE поток данных, отдельно вставляется описание параметров картинки (размеры, цветность и пр.). Я немного огорчился, это означало, что мне придется разбирать формат PNG, которого я не знал.

22: Я открыл другой PDF, посмотрел как вставлены картинки JPEG в PDF. Примерно также: описание параметров картинки и поток данных, запакованных каким-то непонятным алгоритмом DCT. Открываю документацию по PDF и нахожу по ключевому слову ссылку "Adobe Technical Note #5116, Supporting the DCT Filters in PostScript Level 2". Закрываю документацию. Тупик, казалось бы, ибо никуда я сегодня не пойду и читать "Technical Note #5116" в мои планы не входило.

23: Открываю отдельно файл с первой попавшейся картинкой JPEG. Сравниваю поток данных в примере PDF и в файле JPEG. Оказывается, шапка, первые несколько символов в потоке, упакованном DCT фильтром, и в файле JPEG совпадают. Там встречаются одинаковые последовательности символов, вроде "JFIF" или "Adobe".
Предположение: Картинка JPEG вставляется как есть целиком, а описание картинки содержит фильтр DCT как указание на этот самый формат JPEG.

24: Я бы на месте этих умников и с PNG поступил бы так же. Только с первого взгляда PNG совсем не похож на поток DEFLATE, да и где взять информацию для палитры и цветности, не понятно.

25: Ищу знакомые два байта "хЪ", с которых начинается поток DEFLATE, в файле PNG и, что удивительно, нахожу. Стало быть, формат PNG состоит из частей, в одной из которых содержится поток DEFLATE, как его Adobe понимает.

26: Как теперь выделить поток из файла PNG? Перед заветными байтами "хЪ" стоит "IDAT", видимо разделитель полей, перед "IDAT" - несколько байтов hex:"00 00 00 60", которые ничего мне не говорят. У всех файлов PNG, которые я просматривал, первые два-три байта в последовательности hex:"00", а остальные не совпадают. Похоже на число 32-битное. Только не понятно, что оно обозначает: то ли длина, то ли смещение. На смещение от начала файла не похоже, ничего осмысленного не следует. На длину тоже не похоже, ничего осмысленного не следует, хотя, надо отметить, число явно меньше сегмента данных, даже если считать вместе с ключевым словом "IDAT". Формат PNG было бы удобно разгребать, если бы где-то в самом начале стояла бы ссылка на начало поля данных, а поле данных начиналось бы с длины поля. Или оба этих числа были бы расположены в начале файла.

27: Смотрю файл PNG с начала. Начинается с текстового комментария, строка:"%PNG". На новой строке hex:"1A 0A" - ничего мне не говорит, во всех файлах PNG нахожу тоже, значит ничего содержательного, небось такая же вспомогательная абстракция как и "хЪ" перед потоком DEFLATE. Дальше hex:"00 00 00 0D" и строка:"IHDR" - видимо, начало заголовка "HeaDeR". С конца файла нахожу запись hex:"00 00 00 00", строка:"IEND" и несколько байт hex:"AE 42 60 82". Очевидно, "IEND" - является таким же разделителем, но вот смысл оставшихся байтов мне не понятен.

28: А где взять палитру? Ищу по файлу осмысленную фразу, нахожу "PLTE" от слова "PaLeTtE". Перед ним тоже какое-то 32-битное число hex:"00 00 00 21". И опять теряюсь в догадках, что оно означает. Цветов у меня максимум 16, шапка на этом не кончается, так что число hex:"00 00 00 21" остается загадкой. Я бы вписал длину палитры, количество цветов, сразу после "PLTE" или где-нибудь в шапке, заранее.

29: Открываю черно-белую картинку PNG. Все также, только после слова "PLTE" все байты повторяются по три. Значит в палитре сохранены последовательности RGB, которых ровно на hex:"21" = 33 -байт, т.е 11 цветов. Предположение: перед словом "PLTE" стоит 32-битная длина поля палитры. После него четыре непонятных байта, опять 32-битное число и начинается другое поле, например "IDAT".

2A: Смотрю с начала файла, пытаюсь разобраться, что можно вытянуть из заголовка ещё. Сразу после слова "IHDR" два 32-битных числа, которые совершенно очевидно означают ширину и высоту изображения, далее hex:"08 03 00 00 00", непонятные четыре байта, после чего следуют уже другие поля, например "gAMA", "tEXt", "PLTE", "IDAT" и "IEND". Поля "gAMA" и "tEXt" присутствуют только в картинках подготовленных с помощью приложений Adobe, т.е они опциональные.

2B: А что будет, если сохранить картинку в формате PNG c 24 бит на точку, TrueColor, или 8 бит/точку, 4 ...? Для PNG-24 поле "PLTE" отсутствует, оно и понятно. Сразу после ширины и высоты идет последовательность hex:"08 06 00 00 00". Для PNG-4 поле "PLTE" строится также. После ширины и высоты в заголовке идет последовательность hex:"04 03 00 00 00". Предположение: первый байт в последовательности означает число бит, глубина цветности на компоненту, второй байт "03" - для картинки с палитрой, "02" - для картинки TrueColor 24бит, а "00" - GrayScale. Видимо бывают и другие варианты, но это мне проверить не удалось. Скорее всего, разработчики формата оставили какие-либо возможности для расширения. Тогда, разумно предположить, что глубина цветности может быть больше 8 бит, для сканеров это, очевидно, необходимо. А картинки могут содержать информацию о прозрачности, тогда число "04" во втором байте должно обозначать GrayScale+alpha или RGB+alpha, это можно предположить исходя из того, что наличие альфа-канала удобно кодировать одним битом, старшим, чтобы выполнялись битовые операции. Еще для пущей универсальности можно было бы закодировать какой-нибудь CMYK, например как "01" и "05". Думаю, что гоняться за универсальностью бессмысленно, потому, пока мне не покажут хоть одну картинку с кодами отличными от "03" и "02", не буду раздувать код. Предположение: последние три байта в последовательности ничего не означают и зарезервированы для дальнейшего использования.

2C: Какова общая структура файла или как быстро разобрать поля и загрузить картинку? Очевидно, никакой таблицы ссылок на сегменты в файле не содержится, надо грузить всё подряд В такой последовательности: строка:"\x89PNG\r\n", hex:"1A 0A", (длина сегмента:32 бит, имя сегмента:4 символа, 4 непонятных байта)*. То, что в скобках, может повторяться несколько раз для сегментов "IHDR", "gAMA", "tEXt", "PLTE", "IDAT" и "IEND" и, возможно, любые другие имена, если кому потребуются, все равно их проигнорируют. Обязательными сегментами являются "IHDR", "IDAT" и "IEND".

2D: Что содержится в 4-х непонятых байтах? Не длина и не смещение, т.к. все четыре обычно заполнены. Формат разрабатывался для передачи по ненадежным линиям, т.е с потерей данных, оттого его и прозвали "Portable Network Graphics" - это тоже предположение, - значит, имеет смысл реализовать контроль целостности данных, вставить куда-нибудь контрольную сумму, например, по стандартному алгоритму CRC32 или MD5 [RFC 1321]. MD5 не может быть использован, у него длина 128 бит, остается CRC32.

2E: Если в конце каждого сегмента стоит контрольная сумма CRC32, к чему именно она применяется: ко всему файлу, к данным, ко всей последовательности (длина, имя, данные) или как-то иначе? Логичное предположение: только к данным. Проверяю для последнего сегмента "IEND", у которого длина нулевая. CRC32('')=0. Ну, ошибся. Ведь я же не сказал, что знаю, как работает алгоритм CRC32. Другое предположение: к (имени, данным), чтобы имя не исказилось в процессе передачи данных.
Проверяю: CRC32("IEND")=hex:"AE 42 60 82", что и требовалось доказать.

2F: Засекаю время: сочинение на тему писал ровно столько же, сколько разрабатывал саму тему, около 3-х часов с перерывом на обед. [см. результат]

Теперь, если кто не верит в силу мысли или каких-либо других причин, может меня проверить [RFC 2083]. Я себе обычно доверяю настолько, что не утруждаю себя чтением первоисточников.

30: Ну, думаю, если я такой умный, почему бы мне не разобрать с той же легкостью формат JPEG. Всего ничего, надо вытащить данные о картинке (ширину, высоту, глубину цветности и пр.), которые мне необходимы для встраивания файла JPEG в PDF.

31: Надо ли говорить, что никакой документации под рукой нет? Открываю подряд несколько файлов JPEG в бинарном виде изучаю последовательности символов вокруг шестнадцатеричного значения высоты и ширины картинки. К сожалению не могу объяснить, что произошло за те пятнадцать минут, что я изучал внутренности файлов JPEG, могу только представить готовый результат созерцания.

32: Файл состоит из 16-ти битных дескрипторов сегментов и соответствующих им полей, после дескриптора следует 16-битная длина сегмента и соответствующие данные. Файл начинается с подписи hex:"FFD8" и заканчивается дескриптором hex:"FFD9". Интересующий меня сегмент параметров называется hex:"FFC0" или hex:"FFC2". В одном файле может быть несколько сегментов с одинаковым дескриптором. Видимо, в сегменты с одинаковым именем должны сшиваться, но меня это не касается. Мне нужно добраться до моего дескриптора, положение которого в файле, сторго говоря, произвольное. То, что здесь называется дескрипторами, я, было, принял за контрольную разность, но одумался, потому что их значения от файла к файлу не меняются и с длиной никак не коррелируют. Интересующий меня сегмент последовательно содержит байт глубины цветности, 16-битную высоту и ширину изображения и количество компонент цвета 1 для GrayScale или 3 для TrueColor. [см. результат]

(31 августа 2003 г.)