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

Пояснення метаданих, структури та анотацій PDF

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

Це той рівень, який 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 містить текст, який одночасно служить доступним описом. Варто вивчити анотацію посилання, оскільки вона буває двох форм: просте місце призначення та дія

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 для Delphi та C++Builder відкриває структури контурів, анотацій і метаданих через API на рівні документа, тому ви описуєте ієрархію закладок і посилання, а він прокладає посилання. Щодо об'єктної моделі, до якої прикріплюються ці структури, то в технічному огляді структури файлу PDF розглядається каталог і таблиця перехресних посилань, від яких вони залежать