Об'єкт номер 1 не є сторінкою 1. Цей єдиний факт збиває з пантелику більше коду для обробки PDF, ніж будь-який інший аспект формату, і щоб зрозуміти, чому, потрібно зазирнути за те, що показує вам програма перегляду, і подивитися на граф об'єктів, який програма перегляду насправді читає
Файл PDF — це колекція пронумерованих непрямих об'єктів (indirect objects). Кожен об'єкт має номер об'єкта та номер генерації (generation number), і інші об'єкти вказують на нього за допомогою посилання, записаного як N G R: 3 0 R означає поточну версію об'єкта 3. Сторінки є серед цих об'єктів, але їхня послідовність відображення не має нічого спільного з тим, де вони знаходяться у файлі, або які номери вони мають. Порядок відображення визначається виключно деревом /Pages — пов'язаною структурою, корінням якої є каталог документа. Якщо ви проігноруєте дерево і скануватимете об'єкти в числовому порядку, ви зберете сторінки в неправильному порядку для значної частини файлів у реальному світі
Дерево сторінок: що насправді встановлює порядок
Кожен PDF починається з каталогу документа (ISO 32000-2 §7.7.2). Каталог містить запис /Pages, який вказує на кореневий вузол дерева сторінок. Цей кореневий вузол є словником з /Type /Pages, масивом /Kids непрямих посилань та /Count, що вказує загальну кількість сторінок-листків під ним. Порядок відображення — це обхід цього дерева в глибину зліва направо (depth-first left-to-right), і крапка
Мінімальний файл з трьох сторінок робить це конкретним:
%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
% Object 4 is stored third in the file but is page 2 in display order
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
% Object 9 is stored fourth but is page 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
% Object 20 is stored last but is page 1; Kids[0] decides, not object number
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 містить правильну послідовність відображення. Поступові оновлення (incremental updates) додають нові об'єкти в кінці файлу з новими номерами, тому сторінка, додана як ревізія, знаходиться ближче до кінця потоку байтів, навіть якщо вона належить позиції 1 порядку відображення
Плоскі дерева та вкладені піддерева
Специфікація дозволяє дві форми для дерева сторінок. Прості генератори створюють плоску структуру: один кореневий вузол /Pages, масив /Kids якого містить лише об'єкти-листки /Page. Це легко обійти: один рівень углиб, один прохід
Натомість великі документи зазвичай використовують збалансоване дерево. Масив /Kids кореневого вузла /Pages містить проміжні вузли /Pages, кожен з яких своєю чергою має власний масив /Kids. /Count на кожному проміжному вузлі повідомляє про загальну кількість сторінок-листків у його піддереві, тому програма перегляду може пропускати цілі піддерева при переході на сторінку за індексом без аналізу кожного об'єкта. 1000-сторінковий документ, структурований як збалансоване дерево з 10 сторінками на листковий вузол, може знайти сторінку 750 за допомогою бінарного пошуку через три-чотири пошуки у словнику замість сканування 750 записів /Kids
Наслідок для коду обробки: ви не можете припускати, що перший рівень /Kids містить об'єкти /Page. Кожного нащадка потрібно перевіряти. Якщо його /Type — /Pages, здійснюйте рекурсію в нього. Якщо його /Type — /Page, це листок. Зупинка на першому рівні непомітно відкидає цілі піддерева в будь-якому документі, де генератор вирішив зробити вкладення. Чому розробники обирають глибокі дерева в першу чергу, від чого відмовляються інструменти сплощення (flattening tools) і як пошкодження /Count відіграє свою роль на практиці, розглядається в нашій супутній статті про форму дерева сторінок, розгалуження та цілісність /Count
Успадковані атрибути сторінки
Дерево сторінок також має механізм спільного використання ресурсів. Певні атрибути сторінки: /MediaBox, /CropBox, /Resources і /Rotate є успадкованими (ISO 32000-2 §7.7.3.4). Якщо у словнику /Page одного з них немає, програма зчитування (reader) підіймається ланцюжком /Parent, доки не знайде атрибут або не досягне кореня. Розміщення спільного словника шрифтів у кореневому вузлі /Pages замість копіювання його на кожну сторінку-листок може помітно зменшити розмір файлу для документів, у яких скрізь використовуються ті самі шрифти
Правило успадкування створює тонкість для коду, який зчитує властивості сторінки. Безпосереднє зчитування /MediaBox з об'єкта /Page та сприйняття відсутнього ключа як помилки є неправильним; ключ може бути просто успадкованим. Код, який правильно визначає геометрію сторінки, повинен слідувати за ланцюжком батьківських елементів. Він також потребує захисту від зациклення (cycle guard): пошкоджений файл може мати посилання /Parent, яке вказує назад на вже відвіданий вузол, що призведе до нескінченного циклу без перевірки відвіданих об'єктів
Таблиця xref та потоки перехресних посилань (cross-reference streams)
Пошук непрямих об'єктів відбувається через таблицю перехресних посилань (або її наступника — потік перехресних посилань, введений у PDF 1.5). Таблиця xref зіставляє кожен номер об'єкта зі зміщенням байтів (byte offset) у файлі. Сумісна програма зчитування (conforming reader) використовує xref, щоб перейти безпосередньо до будь-якого об'єкта; вона не сканує файл послідовно. Такий дизайн прямого доступу (random-access) робить можливим швидкий перехід по сторінках: програма перегляду зчитує каталог, дозволяє (resolves) посилання /Pages через xref, зчитує кореневий вузол /Pages, дозволяє запис /Kids і так далі, торкаючись лише тих об'єктів, які їй потрібні
Поступові оновлення (incremental updates) додають новий розділ xref у кінці файлу з трейлером (trailer), який з'єднується з попереднім. Об'єкт, оновлений у ревізії, отримує новий запис у доданому розділі xref; оригінальні байти залишаються на місці, але їх заміщують (superseded). Саме завдяки цьому PDF-файли з цифровими підписами залишаються придатними до перевірки навіть після додавання анотацій чи заповнення форм: підписаний діапазон байтів ніколи не зачіпається, а новий вміст живе в доданому розділі. Дерево сторінок також можна оновити, тому додавання або видалення сторінок у ревізії створює новий корінь /Pages з переглянутим масивом /Kids, тоді як старий кореневий об'єкт все ще займає своє початкове положення у файлі. Лінеаризовані (веб-оптимізовані) файли додають поворот до макета байтів: об'єкти для сторінки 1 фізично переміщуються на початок файлу, щоб програма перегляду могла відобразити першу сторінку, поки решта все ще завантажується, але дерево сторінок залишається єдиним авторитетом щодо порядку — змінюються лише зміщення, записані в xref
Що йде не так без обходу дерева
Режим збою для підходів сканування об'єктів є тихим. Вихідний документ виглядає правдоподібно: він має правильну кількість сторінок, і кожна сторінка містить упізнаваний вміст. Просто порядок є неправильним, причому неправильним у такий спосіб, який залежить від генератора, кількості ревізій і того, чи були будь-які сторінки об'єднані з зовнішніх джерел. Тестовий корпус файлів, створених одним інструментом, може пройти повністю; файли від іншого інструменту або робочого процесу об'єднання (merge workflow) зазнають невдачі. Ця невідповідність є причиною того, чому евристичні виправлення ніколи не працюють. Для покрокового розгляду саме цього збою в реальному документі клієнта — симптоми, неправильний діагноз та виправлення обходом — дивіться наше дослідження ситуації з налагодження порядку сторінок
Файли поступового оновлення особливо схильні до цього, оскільки сторінки, додані або перегруповані в пізніших ревізіях, мають високі номери об'єктів, тоді як порядок відображення контролюється оновленим масивом /Kids. Сканування, яке обробляє об'єкти в числовому порядку, розмістить ці пізніше пронумеровані сторінки в кінці незалежно від того, де вони належать згідно з деревом
Виправлення не є складним. Почніть з каталогу, знайдіть посилання /Pages, обійдіть масив /Kids рекурсивно і виводьте листки в тому порядку, у якому ви їх зустрічаєте. Це за визначенням і є порядок відображення, незалежно від номерів об'єктів, зміщень байтів чи структури файлу. Більшість зрілих бібліотек PDF надають кількість сторінок та індексований метод доступу до сторінок (page accessor), які вже роблять це правильно; ризик полягає в коді, який обходить модель сторінок бібліотеки й безпосередньо торкається шару об'єктів
Одна структурна аномалія, яку варто обробляти явно: значення /Count на проміжному вузлі /Pages може бути неправильним у пошкоджених (malformed) файлах. Довіра до /Count для перевірки меж і зупинка без повного обходу (full traversal) призведе до тихого пропускання сторінок, коли кількість занижена. Використання /Count лише як підказки продуктивності для попереднього виділення пам'яті (pre-allocation) або бінарного пошуку та визначення фактичної кількості з обходу — це безпечніший патерн для важливих документів