Technický článek

Struktura souboru PDF: Header, Body, Xref a Trailer

Čtečka PDF nezačíná číst od začátku souboru, ale od konce. Posledních několik bajtů obsahuje adresu všeho ostatního, a analyzátor, který tomuto pořadí nerozumí, bude chybně číst formát už od prvního řádku. Nejužitečnější způsob, jak se naučit PDF na disku, je naučit se ho tak, jak to dělá čtečka: nejprve konec, pak skok zpět na mapu, a pak nalezení objektů, na které mapa odkazuje

Samotné bajty jsou celkem snadno čitelné v textovém editoru, když nic není komprimováno. Minimální jednostránkový dokument, který vykreslí „Hello, World!“, se vejde do pěti set bajtů a každý strukturální prvek formátu je v něm viditelný. Zde je celý soubor se čtyřmi označenými částmi:

%PDF-1.0                          % Header (hlavička)
%âãÏÓ

1 0 obj                           % Body (tělo): sekvence objektů
<<
/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                              % Tabulka Xref (Cross-reference)
0 6
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n

trailer                           % Trailer (zakončení)
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF

Čtyři části, vždy v tomto pořadí směrem dolů v souboru: hlavička, tělo s objekty, cross-reference (xref) tabulka křížových odkazů a trailer. Háček je v tom, že je čtete v podstatě v opačném pořadí. Specifikace ISO 32000-2 §7.5.1 popisuje stejnou čtyřdílnou anatomii, a důvod pro přístup od konce je ryze praktický: čtečka, která skočí rovnou na objekt, který potřebuje, je mnohem rychlejší než ta, která skenuje každý bajt od začátku, a tento náhodný přístup je přesně to, k čemu slouží trailer a tabulka křížových odkazů

Hlavička má dva řádky a na tom druhém záleží

První řádek je %PDF-1.0. Znak procenta z něj dělá z hlediska syntaxe komentář, ale čtečky s ním zacházejí jako se signaturou souboru a vytahují z něj číslo verze. S verzemi se v praxi zachází volně. Čtečka vytvořená pro PDF 2.0 bez problémů otevře soubor, který deklaruje verzi 1.0, a většina čteček se pokusí otevřít soubor, jehož deklarovaná verze je špatná nebo jehož řádek verze je skrytý kousek dál v souboru, nikoli na bajtu nula. Číslo je spíše nápovědou, jaké funkce očekávat, nikoli překážkou

Druhý řádek je ten, který lidé omylem smažou a pak stráví odpoledne laděním chyb. Je to také komentář, ale jeho obsahem jsou čtyři bajty nad hodnotou ASCII 127. Existují proto, aby jakýkoli nástroj přesunující soubor v „textovém režimu“ ho rozpoznal jako binární a přestal přepisovat konce řádků. PDF obsahuje komprimované streamy, jejichž bajty se mohou náhodně shodovat se znaky konce řádku (carriage return) nebo nového řádku (line feed); pokud je nástroj pro přenos přepíše, délka streamu zapsaná ve slovníku už neodpovídá bajtům na disku a soubor je poškozen. Tento komentář s vysokými bajty je čtyřicet let stará obrana proti FTP v ASCII režimu a stále je v každém souboru, který seriózní nástroj zapíše, protože selhání, kterému zabraňuje, je tiché a fatální

Tělo obsahuje objekty, každý z nich je očíslovaný

Vše, co tvoří dokument, žije v těle jako plochá sekvence nepřímých objektů (indirect objects). Každý začíná dvěma celými čísly a klíčovým slovem obj, obsahuje svůj obsah a končí endobj. Objekt 1 v ukázce výše je uzel stromu stránek (page-tree node): 1 0 obj, poté slovník, pak endobj. První celé číslo je číslo objektu, druhé je číslo generace (generation number). Generace je u nově napsaného souboru téměř vždy nula; stoupá pouze tehdy, když se číslo objektu znovu použije během úprav, což je natolik vzácné, že nenulovou generaci můžete považovat za znak toho, že soubor prošel inkrementálními aktualizacemi. Obsah mezi klíčovými slovy je zde slovník zapsaný mezi << a >>, ale mohl by to být stejně tak dobře číslo, řetězec, pole nebo stream

Co dělá z této struktury graf spíše než seznam, je referenční token 2 0 R. Znamená to „objekt 2, generace 0, ať se v souboru nachází kdekoli“. Výše uvedený uzel stromu stránek neobsahuje svou stránku; odkazuje na objekt 2, který odkazuje na své zdroje (resources) a stream obsahu stejným mechanismem. Tělo je uspořádáno v jakémkoli pořadí, které zapisovači (writer) vyhovovalo, a odkazy jej sešívají do stromu s kořenem v katalogu. Pozice v souboru nenese žádný význam. Identita pochází z čísla objektu a umístění pochází z tabulky křížových odkazů (xref table)

Tabulka xref je index bajtových offsetů

Tabulka xref mění čísla objektů na pozice v souborech. Je důvodem, proč čtečka dokáže otevřít tisícistránkový dokument a vykreslit stránku 850 bez analyzování předchozích 849 stránek. Každý záznam zaznamenává přesně to, kde jeho objekt začíná, počítáno v bajtech od začátku souboru:

xref
0 6                  % 6 záznamů, začínající u objektu 0
0000000000 65535 f   % záznam 0: head volného seznamu
0000000015 00000 n   % objekt 1 začíná na bajtu 15
0000000074 00000 n   % objekt 2 začíná na bajtu 74
0000000192 00000 n   % objekt 3 začíná na bajtu 192
0000000291 00000 n   % objekt 4 začíná na bajtu 291
0000000409 00000 n   % objekt 5 začíná na bajtu 409

Fixní šířka je záměrná. Každý záznam má přesně dvacet bajtů: desetimístný offset, mezera, pětimístná generace, mezera, jednoznakové určení typu a dvoubajtový konec řádku. Vzhledem k tomu, že řádky jsou jednotné, může čtečka indexovat přímo k záznamu pro objekt n aritmetikou, nikoli skenováním, takže tabulka, která poskytuje náhodný přístup k tělu, je sama náhodně přístupná (randomly accessible). Řádek 0 6 je hlavičkou podsekce: říká, že následující záznamy popisují šest objektů, počínaje číslem 0

Objekt 0 je zvláštní a vždy přítomný. Jeho typ je f pro „free“ (volný), jeho generace je 65535 a je v čele propojeného seznamu čísel volných objektů. U souboru, který nebyl nikdy upravován, je free list tvořen pouze tímto jedním záznamem z čisté formality. Své opodstatnění získává během inkrementálních aktualizací, kdy odstraněním objektu je jeho číslo přidáno do tohoto seznamu, aby si je mohla pozdější úprava nárokovat. Ostatní záznamy jsou typu n pro označení objektu v užívání (in-use) a jejich desetimístné číslo je offset, na který byste měli přeskočit, abyste načetli definici daného objektu

Trailer je vstupním bodem a nachází se na konci

Trailer (zakončení) je to první, co čtečka skutečně konzumuje, i když se zapisuje jako poslední. Analyzátor otevře soubor, skočí na konec a postupuje zpět a hledá %%EOF. Těsně nad ním se nachází startxref následovaný jediným číslem a toto číslo je bajtový offset klíčového slova xref. S ním čtečka přeskočí přímo na tabulku křížových odkazů bez toho, aby naskenovala jediný objekt:

trailer
<<
/Root 5 0 R          % katalog dokumentu
/Size 6              % o jednu větší hodnota, než je nejvyšší číslo objektu
>>
startxref
459                  % bajtový offset xref tabulky
%%EOF

Slovník traileru nese dvě hodnoty, které čtečka potřebuje, než může udělat cokoli dalšího. /Root odkazuje na katalog dokumentu, zde objekt 5, což je vrchol grafu objektů a cesta k uspořádání stromu stránek. /Size je počet záznamů, které by tabulka xref měla obsahovat, což je kvůli volnému záznamu ve slotu nula o jeden víc než je nejvyšší číslo objektu. Od %%EOF se odvíjí celá posloupnost čtení: nalezení značky, načtení startxref k nalezení tabulky, načtení tabulky k zjištění umístění každého objektu, přečtení /Root k nalezení katalogu a řešení objektů odtud na vyžádání. Hlavička, nacházející se nahoře, je sotva brána v potaz až do samého konce. Mapa dole je to, co čtečka potřebuje ze všeho nejdřív

Inkrementální aktualizace přidává druhou mapu namísto přepisu

Tento design s přístupem od konce se vyplácí při změně souboru. PDF lze upravovat bez přepisování jakýchkoli bajtů, které jsou již na disku. Nové a upravené objekty se připojují na konec, následované novou xref sekcí a novým trailerem, a původní soubor pod tím zůstává nedotčený. Jedinou novou informací je zápis /Prev v novém traileru, obsahující bajtový offset předchozí xref tabulky:

% ... původní soubor, nezměněný, zde končí ...

6 0 obj                          % objekt přidaný touto úpravou
<< /Type /Annot /Subtype /Text /Rect [100 700 120 720] >>
endobj

xref                             % druhá xref sekce pouze pro nový objekt
6 1
0000000612 00000 n

trailer
<<
/Root 5 0 R
/Size 7
/Prev 459                        % bajtový offset předchozí xref tabulky
>>
startxref
680                              % offset této nové xref sekce
%%EOF

Čtečka stále začíná na konečném %%EOF a následuje startxref k nejnovější tabulce, avšak nově s pomocí řetězce /Prev postupuje pozpátku ke starším tabulkám a slučuje je tak, že nejnovější záznam pro jakékoli číslo objektu vyhrává. Xref sekce tvoří v souboru propojený seznam směrem dolů, kde každá z nich přepisuje tu předchozí u objektů, kterých se dotýká. Objekt, který úprava nahradila, na disku u svého starého offsetu fyzicky stále existuje; již však není dostupný, protože pozdější záznam xref ukazuje někam na novější místo

Tohle je mechanismus, který umožňuje ověřitelnost podepsaných PDF. Digitální podpis pokrývá bajtový rozsah souboru a protože se inkrementální aktualizace vždy jen přidávají (append), podepsané bajty se nikdy neposouvají. Podpis tak nadále ověřuje původní rozsah, zatímco pozdější revize se nacházejí mimo něj, přičemž každá má svůj vlastní xref a trailer. Je to zároveň důvod, proč může PDF obsahovat obnovitelnou historii (recoverable history): každý nahrazený objekt je na disku pod starší xref sekcí stále dostupný. Tato funkce představuje skvělou výhodu pro sledování verzí a naprostou noční můru pro každého, kdo si myslel, že smazáním objektu zmizí i jeho bajty

Daní za tuto vlastnost je nárůst velikosti souboru. Každá úprava soubor doplňuje; nic se nenahrazuje na původním místě (in-place), a proto se v mnoha upravených souborech hromadí mrtvé objekty a dlouhé řetězce xref sekcí. Řešením je kompletní přepis (rewrite): načíst dokument a znovu ho uložit, což přečísluje přeživší objekty, vynechá ty nedosažitelné a vydá jedinou a čistou xref tabulku. Tyto dvě strategie se vzájemně doplňují (trade against each other directly). Přidávání (append) je rychlé a zachovává podpisy a historii; přepisování je pomalejší, ruší obojí a na oplátku vytváří kompaktní soubor

Čtení čtyř částí v praxi

Znalost rozvržení stačí k ručnímu odstranění většiny chyb typu „tento soubor nejde otevřít“. Pokud čtečka PDF odmítne, viníkem bývají obvykle oba konce, nikoliv prostředek. Zkrácené stažení ztratí trailer, takže chybí startxref nebo %%EOF a čtečka nemá vstupní bod. Tolerantní čtečky se v takovém případě vrátí k naskenování celého souboru kvůli přebudování xrefu, což je přesně ta pomalá cesta, které se má tabulka vyhnout. Nepodařený přenos v textovém režimu (text-mode) poškodí bajty streamu nebo způsobí, že offsety přestanou odpovídat realitě, a objekty se nahrávají ze špatné pozice. Když už offsety v tabulce neodkazují na skutečná klíčová slova obj, je soubor strukturálně rozbitý, i když každý objekt je individuálně v pořádku

Poučením při vytváření nového kódu týkajícího se rozvržení struktury je nechat účetnictví bajtů na knihovně. Offsety ve xref tabulce musí souhlasit se skutečnými pozicemi každého objektu přesně na bajt, trailer musí ukazovat na správnou tabulku a inkrementální aktualizace se musí správně řetězit (chain) prostřednictvím /Prev. Nativní komponenta, jako je HotPDF Component pro Delphi a C++Builder, to vše řeší při zápisu souboru za vás, včetně volby mezi připojením inkrementální revize a přepsáním (rewrite) do kompaktního souboru. Pokud chcete vidět jak se stejná struktura namísto rozebírání staví od nuly, článek o vytváření jednoduchého dokumentu PDF od nuly (building a PDF document from scratch) krok za krokem vysvětluje proces odeslání hlavičky, objektů, xref tabulky a traileru ve správném pořadí