Что на самом деле представляет собой PDF
ISO 32000-2 §7 Evidence: Standard-backed
PDF — это не просто описание страницы, оказавшееся в файле. Это небольшая графовая база данных с подключённым принтером. На этой странице описаны четыре части, которые есть в каждом PDF, — заголовок, тело, таблица перекрёстных ссылок, трейлер — и то, как NextPDF записывает их, чтобы читатель мог найти каждый объект без догадок.
Почему это важно
Заголовок раздела «Почему это важно»Большинство ошибок PDF — это не ошибки отрисовки. Это ошибки структуры: байтовое смещение, указывающее на один символ после нужного объекта, трейлер, указывающий неверный корневой объект, запись перекрёстной ссылки, не совпадающая с фактическим расположением объекта. Ни одна из них не меняет вид страницы до тех пор, пока читатель не пойдёт по другому пути в файле и не выйдет за его границы.
Если относиться к PDF как к непрозрачному контейнеру, эти сбои выглядят случайными. Если знать объектную модель, каждый такой сбой выглядит ровно тем, чем и является: числом, которое не совпадает с позицией. Умение читать формат — это разница между “PDF повреждён” и “смещение объекта 14 устарело, потому что модуль записи измерил его до завершения вычисления длины потока”.
Если коротко
Заголовок раздела «Если коротко»PDF состоит из четырёх частей в том порядке, в котором они идут в файле:
- Один заголовок — одна строка с указанием версии (
%PDF-2.0). - Одно тело — последовательность нумерованных косвенных объектов: словарей, потоков, массивов, чисел, строк, имён.
- Одна таблица перекрёстных ссылок (или, в PDF 2.0, поток перекрёстных ссылок) — соответствие номера объекта байтовому смещению, благодаря которому любой объект можно найти без сканирования файла.
- Один трейлер — небольшой словарь, который называет корневой объект документа и указывает, где начинается раздел перекрёстных ссылок.
Читатель читает PDF не от начала к концу. Сначала он читает последнюю строку, находит startxref, переходит к разделу перекрёстных ссылок и использует его как индекс по телу. Формат устроен так, чтобы читаться с конца. Один этот факт объясняет бо́льшую часть его устройства.
Как с этим работает NextPDF
Заголовок раздела «Как с этим работает NextPDF»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. Именно этот хвост читатель находит первым.
- Step 1 of 4: ISO 32000-2 §7.5.5 — %%EOF and startxref at the file end
- Step 2 of 4: ISO 32000-2 §7.5.4 / §7.5.8 — the cross-reference section maps object number to offset
- Step 3 of 4: ISO 32000-2 §7.5.5 — the trailer names /Root, the document catalog
- Step 4 of 4: ISO 32000-2 §7.3.10 — each indirect object is reached at its recorded offset
Что говорят факты
Заголовок раздела «Что говорят факты»Структура из четырёх частей — это не соглашение NextPDF; это раздел о структуре файла из Spec: ISO 32000-2, §7.5 ISO 32000-2 §7.5 . Стандарт определяет PDF как заголовок, тело из объектов, таблицу перекрёстных ссылок и трейлер и устанавливает, что читатель должен разбирать файл с конца. Последняя строка — это %%EOF, а две строки перед ней — ключевое слово startxref и байтовое смещение до раздела перекрёстных ссылок.
Любой косвенный объект задаётся номером объекта и номером поколения, разделёнными пробельным символом; за ними следует значение объекта, заключённое между ключевыми словами obj и endobj. Сочетание номера объекта и номера поколения однозначно идентифицирует объект; косвенная ссылка на него записывается как номер объекта, номер поколения и ключевое слово R. ObjectRegistry в NextPDF точно отражает это: последовательный номер, поколение 0 для вновь записанных объектов и зафиксированное смещение.
Начиная с PDF 1.5 объекты также могут находиться внутри потока объектов: там они хранятся без ключевых слов obj/endobj и должны иметь нулевое поколение. Поток перекрёстных ссылок (/Type /XRef,
Spec: ISO 32000-2, §7.5.8 ISO 32000-2 §7.5.8 ) — это механизм PDF 2.0,
который индексирует как обычные объекты, так и такие сжатые объекты.
CrossReferenceStream в NextPDF формирует его с массивом ширин полей /W и
сжатием FlateDecode.
Практический пример
Заголовок раздела «Практический пример»Так выглядят минимальное тело PDF и его трейлер. Числа в разделе перекрёстных ссылок — это байтовые смещения. Они должны быть точно верными, поэтому NextPDF фиксирует их из буфера, а не вычисляет заранее.
%PDF-2.01 0 obj<< /Type /Catalog /Pages 2 0 R >>endobj2 0 obj<< /Type /Pages /Kids [3 0 R] /Count 1 >>endobj3 0 obj<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>endobjxref0 40000000000 65535 f0000000009 00000 n0000000058 00000 n0000000122 00000 ntrailer<< /Size 4 /Root 1 0 R >>startxref196%%EOFЧитатель разбирает это снизу: %%EOF, затем startxref 196, затем переходит к байту 196, где начинается xref, считывает, что объект 1 находится на байте 9, переходит по /Root 1 0 R к каталогу и оттуда обходит дерево страниц. Объект 0 всегда является началом списка свободных объектов с поколением 65535 — это особенность, унаследованная из самой ранней структуры формата; её нужно точно воспроизводить, потому что читатели её ожидают.
Распространённое заблуждение
Заголовок раздела «Распространённое заблуждение»Распространённая ошибка — считать, будто PDF читается сверху вниз, как исходный код. Это не так. Объекты в теле могут идти в любом порядке. Номера объектов не обязаны идти в файле последовательно, и читатель на это не полагается. Единственный авторитетный индекс — это раздел перекрёстных ссылок, а единственный способ найти его — трейлер в конце. PDF с совершенно корректным телом и единственным неверным числом в startxref не читается. PDF с объектами, записанными в перемешанном порядке, но с корректной таблицей перекрёстных ссылок, исправен. Позиция ничего не значит; зафиксированная позиция — это всё.
Ограничения и границы
Заголовок раздела «Ограничения и границы»На этой странице описана структура файла, а не содержимое страницы. Как изображения попадают на страницу — потоки содержимого, графические операторы, вывод текста — это отдельная тема. Страница также не описывает, что происходит, когда файл изменяется после записи. Это задача инкрементных обновлений, при которых модуль записи добавляет второй раздел перекрёстных ссылок, а трейлер выстраивает цепочку назад.
NextPDF — это модуль записи. Описанное здесь поведение — это то, как он сериализует построенный им документ. Это не универсальный анализатор PDF и не инструмент восстановления. Он не обещает прочитать, восстановить или спасти произвольный сторонний файл с повреждённой таблицей перекрёстных ссылок. Гарантия узкая и намеренная: у файлов, которые NextPDF записывает, смещения совпадают, потому что они измерены, а не предсказаны.
Мини-FAQ
Заголовок раздела «Мини-FAQ»Зачем номера поколений, если новые файлы всегда используют 0? Номера поколений существуют для повторного использования объектов при обновлениях. В только что записанном файле каждый объект имеет поколение 0. Ненулевые поколения появляются только тогда, когда файл был инкрементно обновлён и номер объекта используется повторно.
Могут ли два объекта иметь один и тот же номер? В пределах одного раздела перекрёстных ссылок — нет. При инкрементных обновлениях файл может физически содержать несколько копий одного и того же номера объекта. Побеждает самая поздняя запись перекрёстной ссылки. Этому посвящена следующая страница.
Имеет ли значение порядок объектов в файле при выводе? Нет. NextPDF записывает объекты в детерминированном порядке для воспроизводимых сборок, но читатель разрешает всё через раздел перекрёстных ссылок, поэтому физический порядок не имеет смыслового значения.
Связанные документы
Заголовок раздела «Связанные документы»- Инкрементные обновления и почему они важны — что происходит, когда записанный PDF изменяется: добавленные разделы и трейлер, выстроенные в цепочку.
- Потоки и фильтры — как потоковые объекты тела сжимаются и кодируются.
- PDF 2.0: что изменилось — чем структура файла отличается между 1.7 и базовой версией 2.0, на которую ориентируется NextPDF.
Глоссарий
Заголовок раздела «Глоссарий»- Косвенный объект — нумерованный объект в теле, записываемый как
N G obj … endobj, гдеN— это номер объекта, аG— номер поколения. - Косвенная ссылка — указатель на косвенный объект, записываемый как
N G R. - Таблица перекрёстных ссылок (xref) — индекс от номера объекта к байтовому смещению. В PDF 2.0 это обычно поток перекрёстных ссылок (
/Type /XRef) вместо классической текстовой таблицы по 20 байт на запись. - Трейлер — словарь в конце раздела перекрёстных ссылок, который называет
/Root(каталог документа) и/Sizeи находится по смещениюstartxref. - Поток объектов — потоковый объект, который сам содержит другие косвенные объекты (сжатые вместе); у его участников нет
obj/endobj, и поколение нулевое. - Каталог документа — объект, названный в
/Root; точка входа в дерево страниц и ко всем остальным данным документа.