Перейти к содержимому

Что на самом деле представляет собой PDF

Evidence: Standard-backed

PDF — это не просто описание страницы, оказавшееся в файле. Это небольшая графовая база данных с подключённым принтером. На этой странице описаны четыре части, которые есть в каждом PDF, — заголовок, тело, таблица перекрёстных ссылок, трейлер — и то, как NextPDF записывает их, чтобы читатель мог найти каждый объект без догадок.

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

Если относиться к PDF как к непрозрачному контейнеру, эти сбои выглядят случайными. Если знать объектную модель, каждый такой сбой выглядит ровно тем, чем и является: числом, которое не совпадает с позицией. Умение читать формат — это разница между “PDF повреждён” и “смещение объекта 14 устарело, потому что модуль записи измерил его до завершения вычисления длины потока”.

PDF состоит из четырёх частей в том порядке, в котором они идут в файле:

  1. Один заголовок — одна строка с указанием версии (%PDF-2.0).
  2. Одно тело — последовательность нумерованных косвенных объектов: словарей, потоков, массивов, чисел, строк, имён.
  3. Одна таблица перекрёстных ссылок (или, в PDF 2.0, поток перекрёстных ссылок) — соответствие номера объекта байтовому смещению, благодаря которому любой объект можно найти без сканирования файла.
  4. Один трейлер — небольшой словарь, который называет корневой объект документа и указывает, где начинается раздел перекрёстных ссылок.

Читатель читает PDF не от начала к концу. Сначала он читает последнюю строку, находит startxref, переходит к разделу перекрёстных ссылок и использует его как индекс по телу. Формат устроен так, чтобы читаться с конца. Один этот факт объясняет бо́льшую часть его устройства.

NextPDF строит PDF так, как формат читается: сначала объект, затем запись смещения; таблица записывается последней.

Номер каждому косвенному объекту выделяет единый реестр (src/Core/ObjectRegistry.php). Реестр выдаёт последовательные номера через allocate() и, после того как байты объекта записаны в выходной буфер, фиксирует байтовое смещение через register(). Смещения никогда не угадываются заранее. Они считываются из BinaryBuffer::getOffset() в момент, когда выводится заголовок объекта. Именно поэтому запись перекрёстной ссылки NextPDF не может разойтись с объектом, который она описывает: смещение — это ровно та позиция, в которой буфер фактически находился.

Когда тело готово, зависящая от версии стратегия сериализации (src/Writer/PdfSerializationStrategy.php) записывает раздел перекрёстных ссылок и трейлер:

  • Pdf20StreamStrategy выводит сжатый поток перекрёстных ссылок (/Type /XRef) — вариант по умолчанию для PDF 2.0.
  • Pdf17TableStrategy и Pdf14TableStrategy выводят традиционную 20-байтовую таблицу перекрёстных ссылок плюс отдельный словарь трейлера — это требуется профилями PDF/A, которые предписывают более старую структуру файла.

Стратегию выбирает профиль вывода; она не определяется автоматически. Какой бы она ни была, итоговые байты имеют одну и ту же форму: раздел перекрёстных ссылок, затем startxref, затем байтовое смещение, затем %%EOF. Именно этот хвост читатель находит первым.

  1. Step 1 of 4: ISO 32000-2 §7.5.5 — %%EOF and startxref at the file end
  2. Step 2 of 4: ISO 32000-2 §7.5.4 / §7.5.8 — the cross-reference section maps object number to offset
  3. Step 3 of 4: ISO 32000-2 §7.5.5 — the trailer names /Root, the document catalog
  4. Step 4 of 4: ISO 32000-2 §7.3.10 — each indirect object is reached at its recorded offset
Как читатель разрешает объект в файле NextPDF и какой пункт ISO 32000-2 определяет каждый шаг: он начинает с конца файла и движется внутрь.

Структура из четырёх частей — это не соглашение NextPDF; это раздел о структуре файла из Spec: ISO 32000-2, §7.5 . Стандарт определяет PDF как заголовок, тело из объектов, таблицу перекрёстных ссылок и трейлер и устанавливает, что читатель должен разбирать файл с конца. Последняя строка — это %%EOF, а две строки перед ней — ключевое слово startxref и байтовое смещение до раздела перекрёстных ссылок.

Evidence: Standard-backed

Любой косвенный объект задаётся номером объекта и номером поколения, разделёнными пробельным символом; за ними следует значение объекта, заключённое между ключевыми словами obj и endobj. Сочетание номера объекта и номера поколения однозначно идентифицирует объект; косвенная ссылка на него записывается как номер объекта, номер поколения и ключевое слово R. ObjectRegistry в NextPDF точно отражает это: последовательный номер, поколение 0 для вновь записанных объектов и зафиксированное смещение.

Начиная с PDF 1.5 объекты также могут находиться внутри потока объектов: там они хранятся без ключевых слов obj/endobj и должны иметь нулевое поколение. Поток перекрёстных ссылок (/Type /XRef, Spec: ISO 32000-2, §7.5.8 ) — это механизм PDF 2.0, который индексирует как обычные объекты, так и такие сжатые объекты. CrossReferenceStream в NextPDF формирует его с массивом ширин полей /W и сжатием FlateDecode.

Так выглядят минимальное тело PDF и его трейлер. Числа в разделе перекрёстных ссылок — это байтовые смещения. Они должны быть точно верными, поэтому NextPDF фиксирует их из буфера, а не вычисляет заранее.

%PDF-2.0
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000122 00000 n
trailer
<< /Size 4 /Root 1 0 R >>
startxref
196
%%EOF

Читатель разбирает это снизу: %%EOF, затем startxref 196, затем переходит к байту 196, где начинается xref, считывает, что объект 1 находится на байте 9, переходит по /Root 1 0 R к каталогу и оттуда обходит дерево страниц. Объект 0 всегда является началом списка свободных объектов с поколением 65535 — это особенность, унаследованная из самой ранней структуры формата; её нужно точно воспроизводить, потому что читатели её ожидают.

Распространённая ошибка — считать, будто PDF читается сверху вниз, как исходный код. Это не так. Объекты в теле могут идти в любом порядке. Номера объектов не обязаны идти в файле последовательно, и читатель на это не полагается. Единственный авторитетный индекс — это раздел перекрёстных ссылок, а единственный способ найти его — трейлер в конце. PDF с совершенно корректным телом и единственным неверным числом в startxref не читается. PDF с объектами, записанными в перемешанном порядке, но с корректной таблицей перекрёстных ссылок, исправен. Позиция ничего не значит; зафиксированная позиция — это всё.

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

NextPDF — это модуль записи. Описанное здесь поведение — это то, как он сериализует построенный им документ. Это не универсальный анализатор PDF и не инструмент восстановления. Он не обещает прочитать, восстановить или спасти произвольный сторонний файл с повреждённой таблицей перекрёстных ссылок. Гарантия узкая и намеренная: у файлов, которые NextPDF записывает, смещения совпадают, потому что они измерены, а не предсказаны.

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

Могут ли два объекта иметь один и тот же номер? В пределах одного раздела перекрёстных ссылок — нет. При инкрементных обновлениях файл может физически содержать несколько копий одного и того же номера объекта. Побеждает самая поздняя запись перекрёстной ссылки. Этому посвящена следующая страница.

Имеет ли значение порядок объектов в файле при выводе? Нет. NextPDF записывает объекты в детерминированном порядке для воспроизводимых сборок, но читатель разрешает всё через раздел перекрёстных ссылок, поэтому физический порядок не имеет смыслового значения.

  • Косвенный объект — нумерованный объект в теле, записываемый как N G obj … endobj, где N — это номер объекта, а G — номер поколения.
  • Косвенная ссылка — указатель на косвенный объект, записываемый как N G R.
  • Таблица перекрёстных ссылок (xref) — индекс от номера объекта к байтовому смещению. В PDF 2.0 это обычно поток перекрёстных ссылок (/Type /XRef) вместо классической текстовой таблицы по 20 байт на запись.
  • Трейлер — словарь в конце раздела перекрёстных ссылок, который называет /Root (каталог документа) и /Size и находится по смещению startxref.
  • Поток объектов — потоковый объект, который сам содержит другие косвенные объекты (сжатые вместе); у его участников нет obj/endobj, и поколение нулевое.
  • Каталог документа — объект, названный в /Root; точка входа в дерево страниц и ко всем остальным данным документа.