Словник каталогу PDF має рівно один обов'язковий ключ навігації: /Pages. Цей ключ повинен вказувати на непрямий об'єкт типу /Pages, який, у свою чергу, містить масив /Kids та загальну кількість сторінок /Count. Якщо прибрати цей вказівник, жоден відповідний стандарту зчитувач не зможе знайти жодної сторінки у файлі. Стандарт ISO 32000-1 §7.7.2 однозначний у цьому питанні: каталог повинен містити запис /Pages, а об'єкт, на який він посилається, повинен мати тип /Pages. Файли, що порушують цю вимогу, не просто не відповідають стандарту; вони структурно зламані так, що більшість парсерів погано з ними справляються
Що насправді каже специфікація
Мінімальний відповідний стандарту PDF має принаймні три об'єкти. Об'єкт 1 є каталогом, об'єкт 2 є коренем Pages, а об'єкт 3 і далі є словниками окремих сторінок. Каталог вказує на корінь Pages, корінь Pages перелічує своїх дітей у /Kids, кожна сторінка містить зворотне посилання /Parent. Весь ланцюжок є двоспрямованим за задумом, тому парсер може почати з будь-якого кінця та перейти до будь-якої сторінки за час O(log n) для збалансованих дерев
% Minimal conforming structure (ISO 32000-1 §7.7.2)
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << >> >>
endobj
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj
Дерево Pages може бути вкладеним. Документ із тисячами сторінок зазвичай групує сторінки в проміжні об'єкти-вузли, які також мають тип /Pages, кожен зі своїм власним /Kids та /Count, що відображає піддерево під ним. Значення /Count кореневого вузла завжди дорівнює загальній кількості сторінок. Саме це число програми перегляду відображають у полі номера сторінки ще до того, як розберуть бодай одну сторінку, оскільки зчитування одного цілого числа з об'єкта 2 набагато дешевше, ніж обхід усього дерева
Як виглядає файл без Pages
Файли, в яких відсутній словник Pages, зазвичай походять від генераторів PDF, які записують об'єкти сторінок напряму, не збираючи їх у дерево, або ж виникають внаслідок пошкодження, яке видаляє кореневий вузол, залишаючи об'єкти-листки сторінок недоторканими. Каталог у такому файлі або взагалі не має ключа /Pages, або містить посилання на об'єкт, якого більше не існує в таблиці перехресних посилань
% Non-conforming: Catalog with no /Pages reference
1 0 obj
<< /Type /Catalog >>
endobj
% Page objects exist but are unreachable from the Catalog
5 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj
15 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 16 0 R /Resources << >> >>
endobj
25 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 26 0 R /Resources << >> >>
endobj
Парсер, який дотримується специфікації, прочитає каталог, спробує знайти /Pages, нічого не знайде (або знайде мертве посилання) і або видасть помилку, або повідомить про нуль сторінок. Чого він не повинен робити, так це продовжувати роботу, ніби файл має нуль сторінок, і мовчки повідомляти про успіх; це створює порожній вивід, який виглядає правильним для автоматизованих інструментів, але є помилковим для кожної людини, яка його відкриває
Чому парсери аварійно завершують роботу
Більшість парсерів PDF виділяють свою внутрішню таблицю сторінок під час завантаження на основі значення /Count з кореня Pages. Коли цей корінь відсутній, парсер або зчитує нуль, нічого не виділяє, а потім розіменовує нульовий вказівник при першому ж зверненні будь-якого коду до сторінки 1, або ж він зчитує сміття і виділяє абсолютно неправильний буфер. Жоден з цих результатів не є прийнятним. Порушення прав доступу за адресою 0x008E5D78, яке з'являється в журналах збоїв після обробки такого файлу, є саме цим: розіменуванням нульового вказівника всередині шляху доступу до сторінки, що спричинене відсутністю структури, яка, на думку парсера, мала б завжди бути там
Основне проєктне припущення є цілком розумним. Переважна більшість PDF-файлів у світі мають словник Pages. Парсери, які пропускають перевірку наявності задля збереження кількох інструкцій, не є безвідповідальними; вони оптимізують для найпоширенішого випадку. Файли, які карають таку оптимізацію, зустрічаються настільки рідко, що код у продакшені може ніколи не зіткнутися з ними, аж поки це не станеться, і в цей момент збій є і відтворюваним, і незрозумілим, якщо інженер не читав §7.7.2
Відновлення без дерева Pages
Якщо парсер повинен обробляти такі файли, а не відхиляти їх, відновлення йде за передбачуваним шляхом: просканувати кожен непрямий об'єкт у таблиці перехресних посилань, зібрати ті, що мають /Type /Page, та відсортувати їх за номером об'єкта. Порядок за номером об'єкта не обов'язково має збігатися з порядком читання за специфікацією, але на практиці генератори, що пропускають дерево Pages, зазвичай виводять сторінки послідовно, тому порядок за номером об'єкта частіше за все є правильним
Сама перевірка є дешевою. Перш ніж переходити за вказівником /Pages у каталозі, слід переконатися, що вказівник існує, що він посилається на реальний об'єкт, і що /Type знайденого об'єкта дорівнює /Pages. Якщо хоча б одна з цих трьох умов не виконується, слід переходити до лінійного сканування. Таке сканування працює повільніше, ніж обхід дерева для великих документів, оскільки воно зчитує заголовок кожного об'єкта, а не йде збалансованим шляхом, але воно працює, а для файлу, який вже є пошкодженим, коректність є важливішою за швидкість
Один крайовий випадок, який лінійне сканування не вирішує автоматично: впорядкування сторінок. Без масиву /Kids для визначення послідовності, "правильний" порядок є невизначеним за специфікацією. Порядок за номером об'єкта є прагматичним варіантом за замовчуванням; якщо файл достатньо важливий для ретельної обробки, перевірка того, чи об'єкти Page містять явний /StructParents або посилання на анотації, що мають на увазі певну послідовність читання, вартує додаткових зусиль
Наслідки для генераторів PDF
Для тих, хто пише генератор PDF, а не парсер, урок є дуже вузьким: завжди виводити корінь Pages перед закриттям файлу. Каталог без запису /Pages не є дійсним PDF за жодною ревізією специфікації. Генератори, які створюють об'єкти сторінок на льоту та збирають дерево на етапі фіналізації (підхід, який використовує більшість потокових авторів), працюють нормально за умови, що фіналізація дійсно виконується. Поширеним сценарієм збою є виняток або раннє повернення, яке перериває запис до завершення трейлера, залишаючи після себе файл, що відкривається в одних програмах перегляду (які мають евристику відновлення) і не працює в інших (які її не мають)
PDF/A та PDF/UA накладають додаткові обмеження на дерево сторінок понад те, що вимагає базова специфікація, але жоден з них не послаблює вимогу до /Pages. Валідатор, який перевіряє відповідність ISO 19005 або ISO 14289, виявить відсутній словник Pages як порушення базової специфікації ще до того, як він дійде до специфічних для профілю правил