Уберите описания страниц, и у вас останется тонкий слой структуры, который никто не печатает, но от которого зависит каждая программа чтения, индексатор и архивная система. Объект страницы ничего не знает о главе, к которой он принадлежит, об авторе, который ее написал, или о сноске со ссылкой в другое место. Эти знания находятся на уровень выше, в трех структурах, прикрепленных к каталогу документа: потоках метаданных, дереве оглавления и массивах аннотаций для каждой страницы. Их объединяет черта, из-за которой в них легко ошибиться. Ни одна из них не оставляет видимых меток на странице, поэтому файл может рендериться идеально, но при этом терять свои закладки, противоречить собственному полю автора или направлять ссылку на объект страницы, которого больше не существует
Это слой, который библиотека PDF предоставляет как свойства документа, API закладок и вызовы ссылок или аннотаций, и слой, который считывает поисковый робот, чтобы понять, о чем ваш документ. Объектная модель, лежащая в его основе, описана в пошаговом руководстве по структуре документа PDF. Здесь основное внимание уделяется строго тому, что привязано к каталогу
Все три структуры крепятся к каталогу. Полный каталог, связывающий их воедино, выглядит так:
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
/Outlines 3 0 R
/Names << /EmbeddedFiles 4 0 R >>
/Metadata 5 0 R
>>
endobj
Четыре записи, четыре независимые подсистемы. /Pages — это видимый документ; /Outlines — дерево закладок; /Metadata указывает на поток XMP; /Names обращается к общедокументному словарю имен, который, помимо прочего, содержит прикрепленные вложенные файлы. Каждая из них является необязательной, и программа чтения, не обнаружившая ни одной из них, все равно покажет страницы. Эта необязательность и является главной причиной того, почему навигационный слой первым начинает деградировать при редактировании файла инструментами, понимающими только страницы
Два хранилища метаданных, которые противоречат друг другу
PDF хранит метаданные документа в двух местах одновременно, и проблемы начинаются, когда они говорят разное. Исходный механизм — это словарь информации о документе, на который ссылается /Info в трейлере: плоский набор пар ключ-значение для /Title, /Author, /Subject, /Keywords, /Creator, /Producer и двух дат. Он прост, и каждая программа просмотра его читает. PDF 2.0 объявляет большую его часть устаревшей в пользу второго механизма — потока метаданных XMP
XMP — это автономный XML-документ, написанный на RDF, хранящийся как поток, к которому каталог обращается через /Metadata и который помечен как /Type /Metadata /Subtype /XML. В отличие от словаря Info, спрятанного внутри структуры объектов PDF, пакет XMP спроектирован так, чтобы его можно было извлекать и парсить отдельно с помощью инструментов, ничего не знающих о PDF. Вот типичный пакет:
5 0 obj
<< /Type /Metadata /Subtype /XML /Length 1235 >>
stream
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
<dc:title><rdf:Alt><rdf:li xml:lang="x-default">Quarterly Report</rdf:li></rdf:Alt></dc:title>
<dc:creator><rdf:Seq><rdf:li>A. Author</rdf:li></rdf:Seq></dc:creator>
<xmp:CreateDate>2026-06-16T10:46:27+08:00</xmp:CreateDate>
<xmp:CreatorTool>Reporting Service 4.2</xmp:CreatorTool>
<pdf:Producer>losLab PDF Library</pdf:Producer>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
endstream
endobj
Три детали в этом блоке определяют, переживут ли метаданные контакт с реальными инструментами. Инструкции обработки xpacket — это не декорация: они обрамляют пакет, чтобы экстрактор мог найти его внутри большего потока байтов, а программа записи, пропускающая закрывающий <?xpacket end="w"?>, создает файл, который нормально открывается, но спотыкается о строгие валидаторы. Типы данных свойств также имеют значение. dc:title — это языковая альтернатива, обернутая в rdf:Alt, в то время как dc:creator — это упорядоченный список, принимающий rdf:Seq; выдача любого из них как голого текстового узла — самая частая ошибка XMP, которую терпит большинство программ просмотра вплоть до той, которая ее не прощает. Префиксы пространств имен условны, но URI, к которым они привязываются, нормативны: парсер ориентируется на URI, а не на префикс
Жесткое правило при работе с двумя хранилищами — они должны совпадать. Если /Info говорит, что автор — один человек, а dc:creator называет другого, вы создали документ, который отвечает на один и тот же вопрос двояко, и то, какой ответ победит, зависит от того, какое поле считывает потребляющий инструмент. Библиотека обычно записывает оба поля за вас, но как только вы редактируете одно из них вручную или объединяете файлы от разных генераторов, они расходятся. Относитесь к словарю Info как к устаревшей совместимости, а к XMP — как к источнику истины, и регенерируйте оба из одного набора значений, а не патчите их независимо. Для PDF/A это становится требованием соответствия: ISO 19005 требует наличия XMP и запрещает любое свойство Info, противоречащее его аналогу в XMP
Дерево оглавления за панелью закладок
То, что программа просмотра показывает как панель закладок, в файле представляет собой двусвязное дерево словарей, называемое оглавлением документа. Каталог указывает на корневой словарь оглавления через /Outlines; корень указывает на свои первый и последний элементы верхнего уровня; и каждый элемент связан со своими соседями и своим родителем. Нигде нет никакого массива закладок. Вся структура восстанавливается путем следования по ссылкам, именно поэтому одна неработающая ссылка может заставить всю ветвь исчезнуть с панели без каких-либо ошибок
8 0 obj % the outline root
<< /Type /Outlines /Count 4 /First 9 0 R /Last 9 0 R >>
endobj
9 0 obj % top-level: a chapter
<< /Title (Chapter 1: Results)
/Parent 8 0 R /Count 2
/First 12 0 R /Last 15 0 R >>
endobj
12 0 obj % first child
<< /Title (Introduction)
/Parent 9 0 R /Next 15 0 R
/Dest [3 0 R /XYZ 72 720 0] >>
endobj
15 0 obj % second child, last sibling
<< /Title (Methodology)
/Parent 9 0 R /Prev 12 0 R
/Dest [3 0 R /Fit] >>
endobj
Прочтите ссылки, и инварианты станут очевидны. Каждый элемент указывает обратно на свой /Parent. Братья и сестры образуют цепочку через /Prev и /Next, причем первый элемент опускает /Prev, а последний опускает /Next. Родитель называет своих первого и последнего детей через /First и /Last, а дети между ними достижимы только путем прохода по цепочке братьев и сестер. Ошибитесь в одном, и сбой будет тихим: устаревший /Next обрывает главу, родитель, чей /Last не завершает цепочку, оставляет элементы осиротевшими, а программа просмотра рендерит то, до чего может добраться
Поле /Count несет в себе часть состояния, которая удивляет людей. На корневом и на любом развернутом элементе оно содержит количество видимых в данный момент потомков; на свернутом элементе это отрицательное число, величина которого равна количеству потомков, которые появились бы при разворачивании. Таким образом, /Count — это не фиксированный структурный факт о дереве, это сохраненное открытое или закрытое состояние панели, и генератор, жестко кодирующий его как положительную сумму, снова открывает каждую ветвь, которую автор намеревался оставить закрытой
Каждый элемент заслуживает своего места, указывая куда-то. /Title — это то, что показывает панель; /Dest — это то, куда ведет щелчок. Место назначения может быть встроено в элемент, как показано выше, или быть именем, которое разрешается через словарь имен документа, что является лучшим выбором, когда многие закладки и ссылки нацелены на одни и те же места, потому что вы исправляете перемещенную цель в одном месте. Библиотека обычно скрывает это дерево за дескриптором корня оглавления и методами, добавляющими дочерние записи; в HotPDF документ предоставляет OutlineRoot типа THPDFDocOutlineObject и связывает ссылки /Prev, /Next, /Parent и /Count за вас по мере добавления элементов. Этим стоит воспользоваться, потому что поддержание этих инвариантов вручную между правками — именно то место, где оглавления ломаются
Места назначения: грамматика перехода по клику
И закладки, и аннотации ссылок указывают на места назначения, а место назначения — это нечто большее, чем номер страницы. Это массив, который называет объект страницы, а затем указывает, с помощью глагола во втором слоте, как программа просмотра должна его кадрировать. Наиболее распространенным и наиболее часто неправильно используемым является /XYZ в формате [page /XYZ left top zoom]. Три его операнда независимы, и любой может быть null, что означает "оставить это так, как было у читателя". Таким образом, [page /XYZ null null null] переходит на страницу, не трогая позицию прокрутки или масштаб, что обычно и требуется от ссылки "перейти на страницу". Числа находятся в пользовательском пространстве по умолчанию, измеряемом от левого нижнего угла с возрастанием y вверх — в той же системе координат, которую использует содержимое страницы. Авторы, пришедшие из верстки для экранов, рефлекторно отмеряют от верха и отправляют читателя на неверный конец страницы
Семейство /Fit жертвует точным позиционированием ради устойчивости. [page /Fit] масштабирует всю страницу по окну, [page /FitH top] подгоняет страницу по ширине с заданным верхним краем, а [page /FitR l b r t] масштабирует прямоугольник, чтобы заполнить вид. Поскольку они вычисляют масштаб из геометрии страницы, а не из фиксированных координат, место назначения /Fit все равно делает разумные вещи после изменения размера страницы, в то время как место назначения /XYZ с жестко заданным масштабом может оставить читателя смотрящим на поля. Для оглавления /FitH с верхней координатой раздела стареет лучше, чем /XYZ с угаданным масштабом
Аннотации: все интерактивное, что не является содержимым страницы
Аннотация — это объект, который накладывается на страницу, не являясь частью ее потока содержимого. Ссылки, стикеры, выделения, виджеты форм, значки вложенных файлов, штампы: все это аннотации, перечисленные в массиве /Annots страницы, на которой они находятся. Удаление аннотации из этого массива удаляет ее со страницы, даже если основное содержимое не затронуто. В этом весь смысл: аннотации — это слой редактирования, отдельный от отметок, поверх которых они находятся
Каждая аннотация имеет общую небольшую основу. /Subtype называет тип, /Rect задает ее ограничивающую рамку в координатах страницы, а /Contents содержит текст, который также служит описанием для специальных возможностей (accessibility). Аннотация ссылки — случай, заслуживающий изучения, потому что она бывает двух форм: голое место назначения и действие
12 0 obj % link to a destination
<< /Type /Annot /Subtype /Link
/Rect [100 200 300 250]
/Border [0 0 0]
/Dest [5 0 R /XYZ null null null] >>
endobj
13 0 obj % link that runs an action
<< /Type /Annot /Subtype /Link
/Rect [50 50 200 100]
/Border [0 0 0]
/A << /Type /Action /S /URI /URI (https://www.example.com) >> >>
endobj
/Rect — это горячая точка; щелчок внутри нее отправляет читателя к месту назначения, повторно используя ту же грамматику, которую использует оглавление. /Border [0 0 0] делает реальную работу, подавляя уродливый прямоугольник по умолчанию, который программы просмотра рисуют вокруг ссылок. Вторая форма меняет голый /Dest на действие /A, чей подтип /S выбирает поведение: /GoTo в пределах этого файла, /GoToR для другого файла, /URI для веб-адреса, /Launch для запуска внешней программы. Это последнее заслуживает подозрения. /Launch, запускающий исполняемый файл, — это поведение, которое делает PDF вектором для вредоносных программ, поэтому соответствующие стандарту программы просмотра блокируют его или громко предупреждают, и ссылка не срабатывает для большинства читателей. Используйте /URI и /GoTo, а /Launch оставьте в покое
Аннотации разметки, такие как выделения и стикеры, а также аннотации фигур, такие как /Square, добавляют нюанс: их вид на экране не подразумевается их типом. Программа просмотра рендерит свою собственную версию, если только вы не закрепите внешний вид потоком внешнего вида, записью /AP, которая ссылается на форму XObject, содержащую операторы рисования. Пропустите ее, и одно и то же выделение может выглядеть по-разному в двух программах чтения или до и после редактирования в редакторе. Для всего, точный вид чего является частью документа, указывайте /AP. Вложенные файлы, к слову, повторно используют тот же механизм: встроенный файловый поток и словарь спецификации файла, представленные либо как аннотация /FileAttachment, либо через дерево имен /EmbeddedFiles в /Names каталога
Где ломается этот слой и как это отловить
Повторяющаяся ошибка во всем этом — висячая ссылка. Закладки перестают появляться, когда в каталоге нет записи /Outlines или цепочка братьев и сестер обрывается в середине дерева; метаданные игнорируются, когда потоку XMP не хватает маркировки /Type /Metadata /Subtype /XML или обертка xpacket деформирована. В каждом случае содержимое страницы в порядке, поэтому при беглом открытии все выглядит правильным, и дефект всплывает только на панели, которую никто не проверял
Две недорогие привычки улавливают большую часть этого. Откройте готовый файл в настоящей программе просмотра и прощелкайте панель закладок и выборку ссылок, что проверит граф ссылок так же, как это сделает читатель. Затем считайте метаданные отдельным инструментом и убедитесь, что словарь Info и XMP согласуются, — то единственное разногласие, которое не выявит никакое количество кликов. Генерируйте этот слой через библиотеку, которая берет на себя ведение учета ссылок, и большинство из этих ловушек никогда не откроется. Компонент HotPDF Component для Delphi и C++Builder предоставляет структуры оглавления, аннотаций и метаданных через API уровня документа, поэтому вы описываете иерархию закладок и ссылки, а он сам связывает ссылки. Что касается объектной модели, к которой прикрепляются эти структуры, в техническом обзоре структуры файла PDF рассматриваются каталог и таблица перекрестных ссылок, от которых они зависят