Технічна стаття

Логічна модель об'єктів PDF: типи, посилання та структура

PDF-файл - це, по суті, набір об'єктів, які вказують один на одного. Якщо відкинути стиснення, облік перехресних посилань і зміщення в байтах, залишиться граф: невеликий набір типізованих значень, з'єднаних між собою посиланнями, з коренем у єдиному об'єкті, який зчитувач знає, як знайти. Усе, що може виразити PDF, від абзацу тексту до вбудованого шрифту чи цифрового підпису, будується з восьми примітивних типів об'єктів і правила, яке дозволяє одному об'єкту посилатися на інший. Вивчіть їх, і решта формату читатиметься як композиція, а не загадка

Це логічний рівень PDF, визначений у пункті 7.3 стандарту ISO 32000-1, і він лежить на один рівень вище фізичної структури файлу (заголовок, тіло, таблиця перехресних посилань і трейлер, які є окремою темою в технічному огляді структури PDF-файлу). Логічна модель - це те, що означають ці байти після розбору. Програма перегляду читає файл з кінця, щоб знайти трейлер, слідує за ним до кореня, і звідти документ розгортається як об'єкти, що посилаються на об'єкти. Саме про цю частину ви міркуєте, коли налагоджуєте пошкоджену сторінку, пишете парсер або довіряєте бібліотеці збірку документа

Вісім типів об'єктів і нічого більше

PDF визначає рівно вісім базових типів об'єктів. Кожне значення в документі є одним із них, і саме це зберігає зрозумілість формату попри його масштабність

Карткова сітка PDF восьми типів об'єктів PDF з прикладним синтаксисом для boolean, number, string, name, array, dictionary, stream і null
Вісім примітивних типів творять увесь 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. Зчитувач потрапляє туди першим, оскільки трейлер знаходиться першим, і звідти кожна інша частина документа є доступною завдяки переходу за посиланнями

Анатомія непрямого посилання PDF: об'єкт шрифту дванадцять визначений один раз і досягається зі словника ресурсів сторінки через нотацію 12 0 R
Об'єкт здобуває ідентичність через свої номери об'єкта й покоління, а споживачі досягають його де завгодно, записуючи відповідне посилання R замість копії

Каталог містить лише два суворо обов'язкові записи: свій /Type та /Pages, непряме посилання на корінь дерева сторінок. Решта є необов'язковими і описують поведінку всього документа, а не вміст: /Outlines вказує на дерево закладок, /Names зберігає дерева імен, ключами яких є рядки, /Metadata посилається на потік метаданих XMP, а /PageMode та /PageLayout пропонують програмі перегляду, як слід відкривати документ. Жоден із них не потрібен для рендерингу сторінки; вони налаштовують досвід роботи навколо сторінок. Структури закладок, метаданих та анотацій, що прикріплені до каталогу, розглядаються у статті про метадані, закладки та анотації PDF

На діаграмі нижче показано, де тіло об'єктів знаходиться в навколишньому файлі. Каталог і дерево сторінок живуть усередині цього тіла як звичайні непрямі об'єкти; заголовок, таблиця перехресних посилань і трейлер навколо них - це фізичні риштування, які дозволяють зчитувачу їх знаходити

Діаграма чотирьох фізичних розділів файлу PDF: заголовок версії, тіло, що містить об'єкти документа, включно з каталогом і деревом сторінок, таблиця перехресних посилань зі зміщеннями об'єктів та трейлер, що вказує на корінь

Дерево сторінок: збалансована ієрархія сторінок

Від /Pages документ розгалужується в дерево сторінок, де вибір PDF на користь графа, а не плаского списку, виправдовує себе. Сторінки зберігаються не як проста послідовність; вони звисають із дерева, внутрішні вузли якого є вузлами дерева сторінок (/Type /Pages), а листки є об'єктами сторінок (/Type /Page). Внутрішній вузол перелічує своїх дітей у масиві /Kids і записує в /Count, скільки сторінок-листків знаходиться під ним. Кожен вузол, окрім кореня, містить посилання /Parent, що веде назад угору, тому дерево можна обійти в будь-якому напрямку

2 0 obj                                  % корінь дерева сторінок
<< /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                                  % внутрішній вузол, що групує ще дві сторінки
<< /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, є спільним ресурсом, що визначається один раз і на який вказують там, де він використовується. Словник, потік і посилання співпрацюють для рендерингу однієї сторінки, і ті самі шаблони масштабуються на весь документ. Оператори потоку вмісту всередині цього блоку розглядаються окремо для тексту та шрифтів та для графіки та візуальних елементів

Діаграма дерева сторінок PDF: корінний вузол pages, проміжний вузол, листкові сторінки з посиланнями на батька та зв'язки з потоком вмісту і спільними ресурсами шрифтів
Листові сторінки посилаються на батька, потік вмісту та спільні ресурси, тоді як корінь відстежує підсумки в /Count і може передавати вниз атрибути на кшталт /MediaBox кожному нащадку

Чому цю модель варто знати

Більшість розробників стикається з об'єктною моделлю лише тоді, коли щось ламається: сторінка рендериться порожньою через те, що посилання /Contents нікуди не веде, текст виводиться у вигляді квадратів, оскільки ресурс шрифту так і не був вбудований, інструмент повідомляє /Count, який не збігається з кількістю знайдених сторінок. Кожна з цих проблем є твердженням про граф, і безпосереднє читання графа краще, ніж вгадування. Вісім типів і правило посилань - це достатньо малий словник, щоб тримати його в голові, і щойно ви побачите PDF як об'єкти, що вказують на об'єкти, пошкоджені файли перестануть бути чимось незрозумілим

Проте написання моделі власноруч рідко є правильним рішенням поза межами навчання. Підтримка зміщень перехресних посилань, номерів генерацій, лічильників дерева сторінок і довжин потоків в актуальному стані під час редагування - це та бухгалтерія, для якої й існують бібліотеки. На практиці надійна бібліотека для розробки PDF керує графом об'єктів, залишаючи вам можливість думати категоріями сторінок і вмісту. Знання моделі все одно окупається: ви розумієте, що саме будує бібліотека під капотом, і чому