Техническая статья

Логическая объектная модель PDF: типы, ссылки, структура

По своей сути файл PDF — это набор объектов, которые указывают друг на друга. Уберите сжатие, учет перекрестных ссылок и смещения байтов, и останется граф: небольшой набор типизированных значений, связанных друг с другом ссылками, укорененных в одном объекте, который программа для чтения знает, как найти. Все, что может выразить PDF, от абзаца текста до встроенного шрифта и цифровой подписи, построено из восьми примитивных типов объектов и правила, которое позволяет одному объекту ссылаться на другой. Изучите их, и остальная часть формата будет читаться как композиция, а не как тайна

Это логический уровень PDF, определенный в ISO 32000-1, пункт 7.3, и он находится на один уровень выше макета физического файла (заголовка, тела, таблицы перекрестных ссылок и трейлера, что является отдельной темой в статье Технический обзор структуры файла 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, происходит от того, как комбинируются эти восемь типов

Прямые значения, непрямые объекты и ссылки

Любой из этих восьми типов может появляться двумя способами. Прямой объект записывается на месте, как 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 эффективным и редактируемым. Один шрифт, изображение или цветовое пространство могут быть определены один раз и на них могут ссылаться с сотни страниц. Небольшое изменение может быть добавлено в качестве новой версии, которая заменяет один объект, а не переписывает файл целиком. Таблица перекрестных ссылок (cross-reference table) — это индекс, который превращает номер объекта в смещение байта, поэтому программа для чтения переходит прямо к 12 0 obj без сканирования, но это физическая оптимизация. Логически все, что вам нужно знать, это то, что 12 0 R означает "объект, идентифицированный как 12 0"

Каталог: с чего начинается каждый документ

Разрешение ссылок должно откуда-то начинаться, и это где-то — это запись трейлера /Root, которая указывает на каталог документа (document catalog): корень графа объектов, словарь с /Type /Catalog. Программа для чтения достигает его первым, потому что трейлер находится первым, и оттуда к любой другой части документа можно добраться по ссылкам

Каталог содержит только две строго обязательные записи: его /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                                  % страница-лист
<< /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 — это общий ресурс, определенный один раз и указываемый везде, где он используется. Словарь, поток и ссылка сотрудничают для визуализации одной страницы, и те же самые шаблоны масштабируются на весь документ. Операторы потока содержимого внутри этого блока данных рассматриваются отдельно для текста и шрифтов и для графики и визуальных элементов

Почему стоит знать эту модель

Большинство разработчиков знакомятся с объектной моделью только тогда, когда что-то ломается: страница отображается пустой, потому что её ссылка /Contents зависла (dangles), текст выводится в виде прямоугольников, потому что ресурс шрифта так и не был встроен, инструмент сообщает о /Count, которое не совпадает с количеством страниц, которые он может найти. Каждое из этих утверждений относится к графу, и чтение графа напрямую лучше, чем угадывание. Восемь типов и правило ссылки — это достаточно небольшой словарный запас, чтобы держать его в голове, и как только вы увидите PDF как объекты, указывающие на объекты, неправильно сформированные файлы перестанут быть непрозрачными

Тем не менее, написание модели вручную редко является правильным решением за пределами обучения. Сохранение согласованности смещений перекрестных ссылок, номеров поколений, счетчиков дерева страниц и длин потоков в процессе редактирования — это та бухгалтерия, для которой существует библиотека. В производстве зрелая библиотека разработки PDF управляет графом объектов, оставляя вас думать о страницах и контенте. Знание модели все равно окупается: вы понимаете, что библиотека строит под капотом и почему