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

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

В основата си един PDF файл е колекция от обекти, които сочат един към друг. Ако премахнете компресията, счетоводството на кръстосаните препратки и байтовите отмествания, това, което остава, е граф: малък набор от типизирани стойности, свързани чрез препратки, вкоренени в един-единствен обект, който програмата за преглед знае как да намери. Всичко, което един PDF може да изрази, от абзац текст до вграден шрифт и цифров подпис, е изградено от осем примитивни типа обекти и правилото, което позволява на един обект да препраща към друг. Научете ги и останалата част от формата ще се чете като композиция, а не като мистерия

Това е логическият слой на PDF, дефиниран в ISO 32000-1 точка 7.3, и той се намира едно ниво над физическото оформление на файла (заглавната част, тялото, таблицата с кръстосани препратки и трейлъра, което е отделна тема в техническия преглед на структурата на PDF файла). Логическият модел е това, което тези байтове означават след синтактичен анализ. Програмата за преглед чете файла отзад напред, за да намери трейлъра (trailer), следва го до корена и оттам документът се разгръща като обекти, препращащи към обекти. Това е частта, за която разсъждавате, когато отстранявате грешки в деформирана страница, пишете парсер или се доверявате на библиотека да сглоби документ

Осем типа обекти и нищо друго

PDF дефинира точно осем основни типа обекти. Всяка стойност в документ е един от тях, което поддържа формата управляем въпреки неговия обхват

Булевите стойности (Booleans) са ключовите думи true и false. Те включват и изключват флагове, като например дали дадена анотация се отпечатва

Числата (Numbers) се предлагат в два варианта, които спецификацията третира като един тип: цели числа (integers) като 42 и реални числа (reals) като 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 поставя всичко, което е твърде голямо или твърде бинарно, за да стои вътрешно (inline)

Осмият тип е null обектът, ключовата дума 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 метаданни, отметки и анотации

Диаграмата по-долу показва къде се намира тялото на обекта в заобикалящия го файл. Каталогът и дървото на страниците живеят вътре в това тяло като обикновени непреки обекти; заглавната част, таблицата с кръстосани препратки и трейлърът около тях са физическото скеле, което позволява на четеца да ги локализира

Диаграма на четирите физически секции на 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 препраща към един или повече потоци от съдържание (content streams), операторите за рисуване, които изобразяват текст и графики върху страницата. Неговият речник /Resources назовава шрифтовете, изображенията и цветовите пространства, на които разчитат тези оператори, като всеки запис е непряка препратка към обект, споделен между страниците. /MediaBox дава правоъгълника на страницата в точки (1/72 инча), а записи като /Rotate и /CropBox коригират начина, по който се представя

Това разделение на труда е целият модел в миниатюра. Речникът на страницата е структура: типизирани записи и препратки, които казват каква е страницата и с какво рисува. Потокът от съдържание е инструкции: отделен, компресируем блок данни (blob), който казва как да се рисува. Шриффът зад /F1 е споделен ресурс, дефиниран веднъж и сочен навсякъде, където се използва. Речник, поток и препратка си сътрудничат, за да рендират една страница, и същите модели се мащабират за целия документ. Операторите на потока от съдържание вътре в този блок данни са обхванати отделно за текст и шрифтове и за графики и визуални елементи

Защо този модел си струва да се знае

Повечето разработчици се сблъскват с обектния модел само когато нещо се счупи: дадена страница се рендира празна, защото нейната препратка /Contents виси в нищото (dangles), текстът излиза като кутии, защото никога не е бил вграден ресурс на шрифт, даден инструмент съобщава за /Count, който не съвпада със страниците, които може да намери. Всяко от тези е твърдение за графа, и директното четене на графа е по-добро от гадаенето. Осемте типа и правилото за препратки са достатъчно малък речник, който да държите в главата си, и след като видите PDF като обекти, сочещи към обекти, деформираните файлове престават да бъдат непрозрачни

Въпреки това, ръчното писане на модела рядко е правилното решение извън рамките на обучението. Поддържането на съгласуваност на отместванията на кръстосаните препратки, номерата на поколенията, броячите в дървото на страниците и дължините на потоците при редакции е вид счетоводство, с което трябва да се справя библиотека. В производството една зряла библиотека за разработка на PDF управлява графа на обектите, като ви оставя да мислите в страници и съдържание. Познаването на модела все още се отплаща: вие разбирате какво изгражда библиотеката отдолу и защо