Объект номер 1 не является первой страницей. Этот единственный факт сбивает с толку больше кода для обработки PDF, чем любой другой аспект формата, и понимание причин требует заглянуть за пределы того, что показывает вам программа просмотра, и в граф объектов, который программа просмотра фактически читает
Файл PDF представляет собой набор пронумерованных косвенных объектов. Каждый объект имеет номер объекта и номер поколения, и другие объекты указывают на него с помощью ссылки, записанной как N G R: 3 0 R означает текущую версию объекта 3. Страницы находятся среди этих объектов, но порядок их отображения не имеет ничего общего с тем, где они расположены в файле, или с тем, какие номера они носят. Порядок отображения полностью определяется деревом /Pages, связанной структурой, корневым узлом которой является каталог документа. Если вы проигнорируете дерево и будете сканировать объекты по порядку номеров, вы соберете страницы в неправильном порядке для значительной части реальных файлов
Дерево страниц: что на самом деле устанавливает порядок
Каждый PDF-файл начинается с каталога документа (ISO 32000-2 §7.7.2). Каталог содержит запись /Pages, которая указывает на корневой узел дерева страниц. Этот корневой узел представляет собой словарь с /Type /Pages, массивом косвенных ссылок /Kids и значением /Count, указывающим общее количество конечных страниц под ним. Порядок отображения — это обход этого дерева в глубину слева направо, и точка
Минимальный трехстраничный файл делает это конкретным:
%PDF-1.7
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [20 0 R 4 0 R 9 0 R] /Count 3 >>
endobj
% Объект 4 хранится в файле третьим, но отображается как страница 2
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 5 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
% Объект 9 хранится четвертым, но является страницей 3
9 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 10 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
% Объект 20 хранится последним, но является страницей 1; Решает Kids[0], а не номер объекта
20 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 21 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
Массив /Kids читается как [20 0 R 4 0 R 9 0 R], поэтому объект 20 является страницей 1, объект 4 — страницей 2, а объект 9 — страницей 3. Нумерация объектов не имеет значения. Любой код, который перебирает объекты в числовом порядке и собирает объекты с /Type /Page, выдаст неправильную последовательность для этого файла
Почему генераторы создают непоследовательные макеты? Несколько причин. Библиотека, которая предварительно распределяет номера объектов для всех страниц перед записью их содержимого, будет нумеровать их в порядке создания, а затем записывать фактические байты в любом порядке, подходящем для сериализатора. Инструмент слияния, который сшивает документы вместе, перенумеровывает объекты из каждого исходного документа, чтобы избежать конфликтов; перенумерованные объекты страниц в конечном итоге разбрасываются по объединенной таблице объектов, в то время как новый корневой массив /Kids содержит правильную последовательность отображения. Инкрементные обновления добавляют новые объекты в конец файла со свежими номерами, поэтому страница, добавленная в качестве изменения, находится ближе к концу потока байтов, даже если она принадлежит позиции 1 в порядке отображения
Плоские деревья и вложенные поддеревья
Спецификация допускает две формы для дерева страниц. Простые генераторы создают плоскую структуру: один корневой узел /Pages, массив /Kids которого содержит только конечные объекты /Page. Это легко обойти: один уровень в глубину, один проход
В больших документах регулярно используется сбалансированное дерево. Массив /Kids корневого узла /Pages содержит промежуточные узлы /Pages, каждый из которых, в свою очередь, содержит собственный массив /Kids. /Count в каждом промежуточном узле сообщает об общем количестве конечных страниц в его поддереве, поэтому программа просмотра может пропустить целые поддеревья при переходе к странице по индексу без синтаксического анализа каждого объекта. 1000-страничный документ, структурированный как сбалансированное дерево с 10 страницами на конечный узел, может найти страницу 750 путем двоичного поиска с помощью трех или четырех поисков в словаре, вместо сканирования 750 записей /Kids
Последствие для кода обработки: вы не можете предполагать, что первый уровень /Kids содержит объекты /Page. Каждый дочерний элемент должен быть проверен. Если его /Type - /Pages, рекурсивно погрузитесь в него. Если его /Type - /Page, это лист. Остановка на первом уровне молча отбрасывает целые поддеревья в любом документе, где генератор решил использовать вложение. То, почему писатели в первую очередь выбирают глубокие деревья, от чего отказываются инструменты уплощения и как на практике происходит искажение /Count, рассматривается в нашей статье о форме дерева страниц, разветвлении и целостности /Count
Наследуемые атрибуты страницы
Дерево страниц также содержит механизм совместного использования ресурсов. Некоторые атрибуты страницы: /MediaBox, /CropBox, /Resources и /Rotate наследуются (ISO 32000-2 §7.7.3.4). Если словарь /Page пропускает один из них, читатель перемещается вверх по цепочке /Parent, пока не найдет атрибут или не достигнет корня. Размещение общего словаря шрифтов в корневом узле /Pages вместо копирования его на каждую конечную страницу может заметно уменьшить размер файла для документов, в которых повсеместно используются одни и те же шрифты
Правило наследования создает тонкость для кода, который считывает свойства страницы. Чтение /MediaBox напрямую из объекта /Page и обработка отсутствующего ключа как ошибки неверны; ключ может быть просто унаследован. Код, который правильно разрешает геометрию страницы, должен следовать по родительской цепочке. Ему также требуется защита от циклов: поврежденный файл может иметь ссылку /Parent, которая указывает обратно на уже посещенный узел, что приведет к бесконечному циклу без проверки на посещенные объекты
Таблица xref и потоки перекрестных ссылок
Поиск косвенных объектов осуществляется через таблицу перекрестных ссылок (или ее преемника, поток перекрестных ссылок, представленный в PDF 1.5). xref сопоставляет каждый номер объекта со смещением в байтах внутри файла. Соответствующее устройство чтения использует xref для перехода непосредственно к любому объекту; оно не сканирует файл последовательно. Этот дизайн с произвольным доступом - то, что делает возможным быстрый переход по страницам: программа просмотра читает каталог, разрешает ссылку /Pages через xref, читает корневой узел /Pages, разрешает запись /Kids и так далее, касаясь только нужных ей объектов
Инкрементные обновления добавляют новый раздел xref в конец файла с трейлером, который связывается с предыдущим. Объект, обновленный в редакции, получает новую запись в добавленном разделе xref; исходные байты остаются на месте, но заменяются. Именно так PDF-файлы с цифровой подписью остаются проверяемыми даже после добавления аннотаций или изменений заполнения форм: подписанный диапазон байтов никогда не затрагивается, а новое содержимое находится в добавленном разделе. Дерево страниц также может быть обновлено, поэтому добавление или удаление страниц в редакции создает новый корень /Pages с пересмотренным массивом /Kids, в то время как старый корневой объект все еще занимает свое первоначальное положение в файле. Линеаризованные (оптимизированные для Интернета) файлы добавляют поворот в компоновке байтов: объекты для страницы 1 физически перемещаются в начало файла, чтобы программа просмотра могла отобразить первую страницу, пока остальная часть все еще загружается, однако дерево страниц остается единственным авторитетом в отношении порядка — изменяются только смещения, записанные в xref
Что идет не так без обхода дерева
Режим отказа для подходов сканирования объектов тихий. Выходной документ выглядит правдоподобно: он имеет правильное количество страниц, и каждая страница содержит узнаваемый контент. Просто порядок неправильный, и неправильный таким образом, что это зависит от генератора, количества изменений и от того, были ли объединены какие-либо страницы из внешних источников. Тестовый корпус файлов, созданный одним инструментом, может пройти полностью; файлы из другого инструмента или рабочего процесса слияния потерпят неудачу. Эта непоследовательность является причиной того, что эвристические исправления никогда не держатся. Для ознакомления с именно этой ошибкой в реальном документе клиента — симптомом, ошибочным диагнозом и исправлением обхода — смотрите наше тематическое исследование отладки порядка страниц
Файлы с инкрементным обновлением особенно подвержены этому, поскольку страницы, добавленные или переставленные в более поздних версиях, имеют высокие номера объектов, в то время как порядок отображения управляется обновленным массивом /Kids. Сканирование, которое обрабатывает объекты в числовом порядке, поместит эти страницы с поздними номерами в конец, независимо от того, где им место по дереву
Исправление не сложное. Начните с каталога, разрешите ссылку /Pages, рекурсивно пройдите по массиву /Kids и выведите листья в том порядке, в котором вы их встречаете. Это порядок отображения по определению, независимо от номеров объектов, смещений в байтах или структуры файла. Большинство зрелых библиотек PDF предоставляют количество страниц и индексированное средство доступа к страницам, которые уже делают это правильно; риск заключается в коде, который обходит модель страниц библиотеки и касается слоя объектов напрямую
Одна структурная аномалия, с которой стоит разбираться явно: значение /Count на промежуточном узле /Pages может быть неправильным в поврежденных файлах. Доверие /Count для проверки границ, а затем остановка до полного обхода будет молча пропускать страницы, когда количество занижено. Использование /Count только в качестве подсказки производительности для предварительного выделения емкости или двоичного поиска и получение фактического количества из обхода является более безопасным шаблоном для важных документов