PDF-файл - це, по суті, набір об'єктів, які вказують один на одного. Якщо відкинути стиснення, облік перехресних посилань і зміщення в байтах, залишиться граф: невеликий набір типізованих значень, з'єднаних між собою посиланнями, з коренем у єдиному об'єкті, який зчитувач знає, як знайти. Усе, що може виразити PDF, від абзацу тексту до вбудованого шрифту чи цифрового підпису, будується з восьми примітивних типів об'єктів і правила, яке дозволяє одному об'єкту посилатися на інший. Вивчіть їх, і решта формату читатиметься як композиція, а не загадка
Це логічний рівень PDF, визначений у пункті 7.3 стандарту ISO 32000-1, і він лежить на один рівень вище фізичної структури файлу (заголовок, тіло, таблиця перехресних посилань і трейлер, які є окремою темою в технічному огляді структури PDF-файлу). Логічна модель - це те, що означають ці байти після розбору. Програма перегляду читає файл з кінця, щоб знайти трейлер, слідує за ним до кореня, і звідти документ розгортається як об'єкти, що посилаються на об'єкти. Саме про цю частину ви міркуєте, коли налагоджуєте пошкоджену сторінку, пишете парсер або довіряєте бібліотеці збірку документа
Вісім типів об'єктів і нічого більше
PDF визначає рівно вісім базових типів об'єктів. Кожне значення в документі є одним із них, і саме це зберігає зрозумілість формату попри його масштабність
Булеві значення (Booleans) представлені ключовими словами true та false. Вони вмикають і вимикають прапорці, наприклад, те, чи друкується анотація
Числа (Numbers) бувають двох видів, які специфікація розглядає як один тип: цілі числа, як 42, та дійсні числа, як 3.14 або -0.002. У PDF немає експоненційного запису, тому ви ніколи не побачите 1e6 у відповідному файлі. Координати, розміри шрифтів і кути повороту - все це числа
Рядки (Strings) містять послідовності байтів, записані або в круглих дужках (Hello), або в кутових дужках у шістнадцятковому форматі <48656C6C6F>. Обидва записи кодують однаковий вміст; шістнадцятковий формат є запасним виходом для байтів, які незручно записувати в круглих дужках. Рядки містять текст, але в першу чергу це байти, що стає важливим у той момент, коли ви працюєте з будь-чим поза межами ASCII
Імена (Names) - це атомарні токени, що починаються з косої риски: /Type, /Pages, /MediaBox. Ім'я - це не рядок; це ідентифікатор, який використовується як ключ словника або перелічуване значення, і два імена є рівними лише тоді, коли вони збігаються байт у байт. Коса риска - це синтаксис, а не частина імені. Це часто спантеличує новачків, які розглядають /Times-Roman і рядок (Times-Roman) як взаємозамінні; формат так не вважає
Масиви (Arrays) - це впорядковані, різнорідні списки у квадратних дужках: [0 0 612 792] - це прямокутник сторінки, і масив може вільно змішувати типи, включно з посиланнями на інші об'єкти. Словники (Dictionaries) - це "робочі конячки". Записаний між << та >>, словник відображає ключі-імена на значення будь-якого типу, і майже кожна значуща структура в PDF (сторінка, каталог, шрифт, анотація) є словником із ключем /Type, який оголошує, чим вона є
Потоки (Streams) - це словники, за якими слідує хвіст сирих байтів між ключовими словами stream та endstream. Словник описує байти (їхню довжину та будь-які фільтри, як-от FlateDecode, що їх стискають), а байти несуть громіздке корисне навантаження: інструкції щодо вмісту сторінки, вбудовані програми шрифтів, зображення. Потік - це місце, куди PDF поміщає все, що є надто великим або надто бінарним, щоб розмістити його безпосередньо в коді
Восьмий тип - це нульовий об'єкт (null object), ключове слово null. Це реальне значення, яке відрізняється від відсутності ключа. Запис словника зі значенням null обробляється так, ніби він відсутній, а посилання, яке вказує на неіснуючий об'єкт, також дає null, а не помилку. Така поблажлива поведінка є навмисною: вона дозволяє пошкодженому файлу деградувати замість відмови відкриватися. Дев'ятого типу немає; все, що виражає PDF, походить від того, як поєднуються ці вісім типів
Прямі значення, непрямі об'єкти та посилання
Будь-який із цих восьми типів може з'являтися у два способи. Прямий (direct) об'єкт записується на місці, як-от 612 всередині масиву MediaBox. Непрямому (indirect) об'єкту надається ідентичність, щоб інші об'єкти могли на нього вказувати: два цілих числа (номер об'єкта та номер генерації), що обгортають визначення в obj та endobj:
12 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj
Це об'єкт 12, генерація 0, словник шрифту. Будь-де в іншому місці файлу інший об'єкт посилається на нього за допомогою непрямого посилання (indirect reference): ті самі два числа, за якими йде ключове слово R, 12 0 R. Посилання є вказівником. Коли словник ресурсів сторінки містить /Font << /F1 12 0 R >>, він називає об'єкт 12 шрифтом, що стоїть за ім'ям ресурсу /F1, не копіюючи визначення шрифту на сторінку
Номер генерації існує для видалення та повторного використання. Коли об'єкт звільняється і його слот використовується повторно, генерація збільшується, щоб застаріле 12 0 R не могло вказувати на нового орендаря слота 12. Щойно записані файли майже всі мають генерацію 0, але сильно відредагованого файлу може мати більші номери, і парсер, який ігнорує генерацію, врешті-решт прочитає неправильний об'єкт
Саме непряма адресація (indirection) робить PDF ефективним і придатним для редагування. Один шрифт, зображення або колірний простір можна визначити один раз і посилатися на них із сотні сторінок. Невелику зміну можна додати як нову ревізію, яка замінює один об'єкт, а не перезаписує весь файл. Таблиця перехресних посилань - це індекс, який перетворює номер об'єкта на зміщення в байтах, завдяки чому зчитувач переходить безпосередньо до 12 0 obj без сканування, але це фізична оптимізація. Логічно вам потрібно знати лише те, що 12 0 R означає "об'єкт, ідентифікований як 12 0"
Каталог: звідки починається кожен документ
Розв'язання посилань має з чогось починатися, і цим місцем є запис трейлера /Root, який вказує на каталог документа (document catalog): корінь графа об'єктів, словник із /Type /Catalog. Зчитувач потрапляє туди першим, оскільки трейлер знаходиться першим, і звідти кожна інша частина документа є доступною завдяки переходу за посиланнями
Каталог містить лише два суворо обов'язкові записи: свій /Type та /Pages, непряме посилання на корінь дерева сторінок. Решта є необов'язковими і описують поведінку всього документа, а не вміст: /Outlines вказує на дерево закладок, /Names зберігає дерева імен, ключами яких є рядки, /Metadata посилається на потік метаданих XMP, а /PageMode та /PageLayout пропонують програмі перегляду, як слід відкривати документ. Жоден із них не потрібен для рендерингу сторінки; вони налаштовують досвід роботи навколо сторінок. Структури закладок, метаданих та анотацій, що прикріплені до каталогу, розглядаються у статті про метадані, закладки та анотації PDF
На діаграмі нижче показано, де тіло об'єктів знаходиться в навколишньому файлі. Каталог і дерево сторінок живуть усередині цього тіла як звичайні непрямі об'єкти; заголовок, таблиця перехресних посилань і трейлер навколо них - це фізичні риштування, які дозволяють зчитувачу їх знаходити

Дерево сторінок: збалансована ієрархія сторінок
Від /Pages документ розгалужується в дерево сторінок, де вибір PDF на користь графа, а не плаского списку, виправдовує себе. Сторінки зберігаються не як проста послідовність; вони звисають із дерева, внутрішні вузли якого є вузлами дерева сторінок (/Type /Pages), а листки є об'єктами сторінок (/Type /Page). Внутрішній вузол перелічує своїх дітей у масиві /Kids і записує в /Count, скільки сторінок-листків знаходиться під ним. Кожен вузол, окрім кореня, містить посилання /Parent, що веде назад угору, тому дерево можна обійти в будь-якому напрямку
2 0 obj % root of the page tree
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>
endobj
3 0 obj % a leaf page
<< /Type /Page /Parent 2 0 R
/MediaBox [0 0 612 792]
/Resources << /Font << /F1 12 0 R >> >>
/Contents 5 0 R >>
endobj
4 0 obj % an interior node grouping two more pages
<< /Type /Pages /Parent 2 0 R /Kids [6 0 R 7 0 R] /Count 2 >>
endobj
Тут об'єкт 2 є коренем із трьома сторінками під ним: сторінкою-листком 3, плюс двома додатковими, доступними через внутрішній вузол 4. Значення /Count кореня (3) має дорівнювати загальній кількості листків під ним, і лічильник, який не узгоджується з фактичною структурою, є типовою причиною несправності відредагованого вручну файлу. Сенс дерева полягає в локальності доступу. Зчитувач, відкриваючи сторінку 900 тисячосторінкового документа, не обходить 900 об'єктів; він спускається на кілька вузлів, оскільки добре сформоване дерево залишається неглибоким і збалансованим. Побудова такого дерева вручну є досить клопіткою, щоб захотіти побачити цей процес від початку до кінця, що і зроблено в покроковому посібнику зі створення PDF-документа з нуля
Дерево відпрацьовує своє друге призначення завдяки успадкуванню (inheritance). Кілька атрибутів сторінки, такі як /Resources, /MediaBox, /CropBox та /Rotate, можна встановити на внутрішньому вузлі і не вказувати на окремих сторінках, які потім успадкують значення найближчого предка. Встановіть /MediaBox один раз у корені, і кожен листок отримає однаковий розмір сторінки без його повторення; сторінка, яка має відрізнятися, оголошує свій власний. Це єдине місце в об'єктній моделі, де значення залежить від позиції об'єкта в дереві, а не лише від його власного вмісту
Що насправді містить сторінка-листок
Об'єкт сторінки є точкою з'єднання між структурною моделлю та видимим вмістом. Його запис /Contents посилається на один або кілька потоків вмісту, тобто операторів малювання, які наносять текст і графіку на сторінку. Його словник /Resources називає шрифти, зображення та колірні простори, на які покладаються ці оператори, де кожен запис є непрямим посиланням на об'єкт, спільний для кількох сторінок. /MediaBox задає прямокутник сторінки в пунктах (1/72 дюйма), а такі записи, як /Rotate та /CropBox, налаштовують спосіб її представлення
Цей розподіл праці і є всією моделлю в мініатюрі. Словник сторінки - це структура: типізовані записи та посилання, які кажуть, чим є сторінка і за допомогою чого вона малюється. Потік вмісту - це інструкції: окремий блок, що стискається і каже, як малювати. Шрифт, який стоїть за /F1, є спільним ресурсом, що визначається один раз і на який вказують там, де він використовується. Словник, потік і посилання співпрацюють для рендерингу однієї сторінки, і ті самі шаблони масштабуються на весь документ. Оператори потоку вмісту всередині цього блоку розглядаються окремо для тексту та шрифтів та для графіки та візуальних елементів
Чому цю модель варто знати
Більшість розробників стикається з об'єктною моделлю лише тоді, коли щось ламається: сторінка рендериться порожньою через те, що посилання /Contents нікуди не веде, текст виводиться у вигляді квадратів, оскільки ресурс шрифту так і не був вбудований, інструмент повідомляє /Count, який не збігається з кількістю знайдених сторінок. Кожна з цих проблем є твердженням про граф, і безпосереднє читання графа краще, ніж вгадування. Вісім типів і правило посилань - це достатньо малий словник, щоб тримати його в голові, і щойно ви побачите PDF як об'єкти, що вказують на об'єкти, пошкоджені файли перестануть бути чимось незрозумілим
Проте написання моделі власноруч рідко є правильним рішенням поза межами навчання. Підтримка зміщень перехресних посилань, номерів генерацій, лічильників дерева сторінок і довжин потоків в актуальному стані під час редагування - це та бухгалтерія, для якої й існують бібліотеки. На практиці надійна бібліотека для розробки PDF керує графом об'єктів, залишаючи вам можливість думати категоріями сторінок і вмісту. Знання моделі все одно окупається: ви розумієте, що саме будує бібліотека під капотом, і чому