Technical Article

Štruktúra súboru PDF: Hlavička, telo, xref a trailer

Prehliadač PDF nezačína čítať súbor od začiatku. Začína od konca. Posledných niekoľko bajtov obsahuje adresu všetkého ostatného a parser, ktorý tomuto poradiu nerozumie, nesprávne interpretuje formát už od prvého riadku. Najužitočnejším spôsobom, ako spoznať štruktúru PDF na disku, je preto učiť sa ju rovnako ako prehliadač: najprv koniec, potom skok dozadu na mapu a následne načítanie objektov, na ktoré táto mapa ukazuje.

Samotné bajty sú v textovom editore ľahko čitateľné, pokiaľ nie je nič skomprimované. Minimálny jednostránkový dokument, ktorý vykresľuje reťazec „Hello, World!“, sa zmestí do necelých päťsto bajtov a sú v ňom viditeľné všetky štrukturálne prvky formátu. Tu je celý súbor s vyznačenými štyrmi časťami:

%PDF-1.0                          % Header
%âãÏÓ

1 0 obj                           % Body: the object sequence
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj

2 0 obj
<<
/Rotate 0
/Parent 1 0 R
/Resources 3 0 R
/MediaBox [0 0 612 792]
/Contents [4 0 R]
/Type /Page
>>
endobj

3 0 obj
<< /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >>
endobj

4 0 obj
<< /Length 65 >>
stream
1. 0. 0. 1. 50. 700. cm BT
  /F0 36. Tf
  (Hello, World!) Tj
ET
endstream
endobj

5 0 obj
<< /Pages 1 0 R /Type /Catalog >>
endobj

xref                              % Cross-reference table
0 6
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n

trailer                           % Trailer
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF

Štyri časti, vždy v tomto poradí za sebou v súbore: hlavička, telo objektov, tabuľka krížových odkazov (cross-reference table) a trailer. Háčik je v tom, že ich čítate takmer v opačnom poradí. Norma ISO 32000-2 §7.5.1 popisuje rovnakú štvorčastnú štruktúru a dôvod prístupu od konca na začiatok je čisto praktický: prehliadač, ktorý skočí priamo na potrebný objekt, je oveľa rýchlejší ako ten, ktorý skenuje každý bajt od vrchu. A tento náhodný prístup (random access) je presne to, čo poskytujú trailer a tabuľka krížových odkazov.

Hlavička má dva riadky a na tom druhom záleží

Prvý riadok je %PDF-1.0. Znak percenta z neho z hľadiska syntaxe robí komentár, no prehliadače ho berú ako podpis súboru (file signature) a zisťujú z neho verziu. Spracovanie verzií je v praxi voľné. Prehliadač navrhnutý pre PDF 2.0 bez problémov otvorí súbor deklarujúci verziu 1.0 a väčšina prehliadačov sa pokúsi otvoriť aj súbor, ktorého deklarovaná verzia je nesprávna alebo ktorého riadok s verziou sa nachádza kúsok hlbšie v súbore a nie hneď na nultom bajte. Číslo verzie je len pomôckou, aké funkcie možno očakávať, nie prísnou bariérou.

Druhý riadok je ten, ktorý ľudia často omylom vymažú a potom strávia popoludnie hľadaním chyby. Je to tiež komentár, ale jeho obsahom sú štyri bajty s hodnotami nad ASCII 127. Existujú preto, aby akýkoľvek prenos súboru v „textovom režime“ rozpoznal, že ide o binárny súbor, a prestal prepisovať konce riadkov. Súbor PDF obsahuje komprimované dátové toky (streams), ktorých bajty sa môžu náhodne zhodovať so znakmi konca riadku (CR/LF). Ak ich prenosový nástroj prepíše, dĺžka toku zaznamenaná v slovníku už nebude zodpovedať bajtom na disku a súbor sa poškodí. Tento komentár s vysokými hodnotami bajtov je štyridsať rokov stará ochrana pred FTP prenosom v ASCII režime a stále sa nachádza v každom súbore, ktorý zapíše seriózny nástroj, pretože zlyhanie, ktorému predchádza, je tiché a fatálne.

Telo obsahuje objekty, z ktorých každý má svoje číslo

Všetko, čo tvorí dokument, sa nachádza v tele ako plochá sekvencia nepriamych objektov (indirect objects). Každý objekt sa začína dvoma celými číslami a kľúčovým slovom obj, obsahuje svoj obsah a končí slovom endobj. Objekt 1 in ukážke vyššie je uzol stromu stránok: 1 0 obj, potom slovník a potom endobj. Prvé celé číslo je číslo objektu, druhé je číslo generácie. Generácia je v novozapísanom súbore takmer vždy nula. Zvyšuje sa len vtedy, kedy sa číslo objektu znova použije pri úpravách, čo je natoľko zriedkavé, že nenulovú generáciu môžete považovať za príznak toho, že súbor prešiel prírastkovými aktualizáciami. Obsah medzi kľúčovými slovami je v tomto prípade slovník zapísaný medzi << a >>, no rovnako by to mohlo byť číslo, reťazec, pole alebo dátový tok.

To, čo z tohto robí graf a nie zoznam, je referenčný token 2 0 R. Znamená to „objekt 2, generácia 0, bez ohľadu na to, kde presne sa v súbore nachádza“. Uzol stromu stránok vyššie neobsahuje priamo svoju stránku, ale odkazuje na objekt 2, ktorý rovnakým mechanizmom odkazuje na svoje zdroje a dátový tok obsahu. Telo je usporiadané v akomkoľvek poradí, ktoré bolo pre zapisovač pohodlné, a odkazy ho spájajú do stromu s koreňom v katalógu. Pozícia v súbore nemá žiadny význam. Identita pochádza z čísla objektu a umiestnenie z tabuľky krížových odkazov.

Tabuľka krížových odkazov je index bajtových posunov

Tabuľka xref (krížových odkazov) je to, čo mení čísla objektov na pozície v súbore. Vďaka nej môže prehliadač otvoriť tisícstránkový dokument a vykresliť stránku 850 bez toho, aby musel analyzovať predchádzajúcich 849 stránok. Každý záznam presne zaznamenáva, kde sa jeho objekt začína, vyjadrené v bajtoch od začiatku súboru:

xref
0 6                  % 6 entries, starting at object 0
0000000000 65535 f   % entry 0: head of the free list
0000000015 00000 n   % object 1 begins at byte 15
0000000074 00000 n   % object 2 begins at byte 74
0000000192 00000 n   % object 3 begins at byte 192
0000000291 00000 n   % object 4 begins at byte 291
0000000409 00000 n   % object 5 begins at byte 409

Pevná šírka je zámerná. Každý záznam má presne dvadsať bajtov: desaťmiestny posun, medzera, päťmiestne číslo generácie, medzera, jednorozmerný typ znaku a dvojobajtový koniec riadku. Keďže riadky sú jednotné, prehliadač môže pomocou aritmetiky prejsť priamo na záznam pre objekt n namiesto postupného prehľadávania. Tabuľka, ktorá poskytuje náhodný prístup k telu, je teda sama o sebe náhodne prístupná. Riadok 0 6 je hlavička podsekcie: hovorí, že nasledujúce záznamy popisujú šesť objektov počnúc číslom 0.

Objekt 0 je špeciálny a vždy prítomný. Jeho typ je f (free - voľný), jeho generácia je 65535 a stojí na čele zoznamu voľných čísiel objektov. V súbore, ktorý nebol nikdy upravovaný, tvorí tento zoznam len tento jediný formálny záznam. Svoj význam nadobúda pri prírastkových aktualizáciách, kedy vymazanie objektu pridá jeho číslo do tohto zoznamu, aby ho neskoršia úprava mohla znova použiť. Ostatné záznamy majú typ n (in-use - používaný) a ich desaťmiestne číslo predstavuje posun, na ktorý sa musíte presunúť, aby ste prečítali definíciu daného objektu.

Trailer je vstupným bodom a nachádza sa na konci

Trailer je prvá vec, ktorú prehliadač skutočne spracuje, hoci sa zapisuje ako posledný. Parser otvorí súbor, presunie sa na koniec a postupuje dozadu, kým nenájde značku %%EOF. Tesne nad ňou sa nachádza kľúčové slovo startxref nasledované číslom, ktoré predstavuje bajtový posun kľúčového slova xref. Pomocou neho prehliadač skočí priamo na tabuľku krížových odkazov bez toho, aby musel prečítať čo i len jeden objekt:

trailer
<<
/Root 5 0 R          % the document catalog
/Size 6
>>
startxref
459
%%EOF

Slovník traileru obsahuje dve hodnoty, ktoré prehliadač potrebuje predtým, ako môže urobiť čokoľvek iné. Odkaz /Root ukazuje na katalóg dokumentu (tu objekt 5), ktorý predstavuje vrchol grafu objektov a prístupovú cestu k stromu stránok. Hodnota /Size je počet záznamov, ktoré by mala tabuľka krížových odkazov obsahovať, čo je o jeden viac ako najvyššie číslo objektu (kvôli voľnému záznamu na pozícii nula). Zo značky %%EOF vyplýva celá sekvencia čítania: nájsť značku, prečítať startxref na lokalizáciu tabuľky, načítať tabuľku, aby sme zistili, kde každý objekt leží, prečítať /Root na nájdenie katalógu a odtiaľ podľa potreby načítať jednotlivé objekty. Hlavička nachádzajúca sa úplne hore sa berie do úvahy až neskôr. Mapa na spodku je to, čo čítačka potrebuje ako prvé.

Prírastková aktualizácia pripája novú mapu namiesto prepisovania tej pôvodnej

Tento dizajn zameraný na čítanie od konca sa vypláca pri zmenách súboru. Súbor PDF je možné upravovať bez prepisovania akýchkoľvek bajtov, ktoré už na disku sú. Nové a upravené objekty sa pripoja na koniec, za nimi nasleduje nová sekcia xref a nový trailer, zatiaľ čo pôvodný súbor pod nimi zostáva nedotknutý. Jediným novým záznamom je položka /Prev in novom traileri, ktorá uchováva bajtový posun predchádzajúcej tabuľky krížových odkazov:

% ... original file, unchanged, ends here ...

6 0 obj                          % an object added by this edit
<< /Type /Annot /Subtype /Text /Rect [100 700 120 720] >>
endobj

xref                             % a second xref section, for the new object only
6 1
0000000612 00000 n

trailer
<<
/Root 5 0 R
/Size 7
/Prev 459                        % byte offset of the earlier xref table
>>
startxref
680                              % offset of this new xref section
%%EOF

Prehliadač stále začína na finálnej značke %%EOF, pokračuje cez startxref k najnovšej tabuľke, no teraz sleduje reťazec /Prev smerom dozadu k starším tabuľkám a spája ich tak, aby vyhral najnovší záznam pre akékoľvek číslo objektu. Sekcie krížových odkazov tvoria prepojený zoznam prechádzajúci súborom, pričom každá prepisuje tú predchádzajúcu pre objekty, ktorých sa týka. Objekt, ktorý bol nahradený úpravou, stále fyzicky existuje na svojom pôvodnom posune, len už skrátka nie je prístupný, pretože neskorší záznam v xref ukazuje na novšie miesto.

Toto je mechanizmus, vďaka ktorému sú digitálne podpísané PDF overiteľné. Digitálny podpis pokrýva konkrétny bajtový rozsah súboru a keďže prírastková aktualizácia iba pripája nové údaje na koniec, podpísané bajty sa nikdy nepresunú. Podpis sa stále overuje voči pôvodnému rozsahu, zatiaľ čo neskoršie revízie ležia za ním, pričom každá má svoj vlastný xref a trailer. To je tiež dôvod, prečo môže PDF niesť obnoviteľnú históriu zmien: každý nahradený objekt stále zostáva na disku pod staršou sekciou krížových odkazov, čo je výhodou pre sledovanie verzií, no nevýhodou pre každého, kto si myslel, že „vymazať“ znamená skutočné odstránenie bajtov.

Daňou za to je nárast veľkosti. Každá úprava pridáva dáta na koniec a nič sa neprepisuje na mieste, takže súbor upravovaný mnohokrát hromadí neaktívne objekty a dlhý reťazec xref sekcií. Riešením je úplný prepis: načítať dokument a uložiť ho nanovo, čo prečísluje zostávajúce objekty, zahodí tie nedostupné a vygeneruje jednu čistú tabuľku krížových odkazov. Tieto dve stratégie stoja priamo proti sebe. Pripájanie je rýchle a zachováva podpisy aj históriu, prepisovanie je pomalšie a obe tieto veci zahadzuje výmenou za kompaktný súbor.

Čítanie štyroch častí v praxi

Znalosť tohto usporiadania stačí na to, aby ste ručne diagnostikovali väčšinu problémov s otváraním súborov. Ak prehliadač odmietne otvoriť PDF, zvyčajní vinníci sa nachádzajú na oboch koncoch, nie v strede. Pri prerušenom sťahovaní sa stratí trailer, takže chýba startxref alebo %%EOF a prehliadač nemá vstupný bod. Tolerantné prehliadače sa vtedy pokúsia prečítať celý súbor a zrekonštruovať xref tabuľku, čo je presne tá pomalá cesta, ktorej sa tabuľka chcela vyhnúť. Nesprávny prenos v textovom režime zasa poškodí bajty dátových tokov alebo posuny prestanú zodpovedať realite a objekty sa načítajú z nesprávnych pozícií. Keď posuny v tabuľke už neukazujú na skutočné kľúčové slová obj, súbor je štrukturálne poškodený, aj keby bol každý jednotlivý objekt úplne v poriadku.

Pre nový kód z tohto rozloženia vyplýva ponaučenie, že správu bajtov by ste mali prenechať knižnici. Posuny v tabuľke krížových odkazov musia do posledného bajtu súhlasiť so skutočnými pozíciami každého objektu, trailer musí ukazovať na správnu tabuľku a prírastkové aktualizácie musia správne odkazovať cez /Prev. Natívny komponent, ako je HotPDF Component pre Delphi a C++Builder, rieši všetky tieto úlohy automaticky pri zápise súboru, vrátane voľby medzi prírastkovým pripojením a prepísaním do kompaktného tvaru. Ak chcete vidieť rovnakú štruktúru vytváranú od nuly a nie len analyzovanú, sprievodný článok o vytvorení PDF dokumentu od nuly vás prevedie postupným generovaním hlavičky, objektov, tabuľky xref a traileru.