Technical Article

Poradie stránok v PDF: Ako strom stránok riadi ich postupnosť

Objekt číslo 1 nie je stránka 1. Tento jediný fakt spôsobuje chyby v kóde na spracovanie PDF častejšie než akýkoľvek iný aspekt tohto formátu. Pochopenie dôvodu vyžaduje pozrieť sa za to, čo zobrazuje prehliadač, priamo do grafu objektov, ktorý prehliadač v skutočnosti číta.

PDF súbor je kolekcia očíslovaných nepriamych objektov. Stránky patria medzi tieto objekty, ale ich poradie zobrazenia nemá nič spoločné s tým, kde sa v súbore nachádzajú alebo aké čísla nesú. Poradie zobrazenia je určené výhradne stromom /Pages, čo je prepojená štruktúra vychádzajúca z katalógu dokumentu. Ak tento strom ignorujete a prechádzate objekty číselne, pri významnej časti reálnych súborov zostavíte stránky v nesprávnom poradí.

Strom stránok: čo skutočne určuje poradie

Každé PDF začína katalógom dokumentu (ISO 32000-2 §7.7.2). Katalóg obsahuje položku /Pages, ktorá ukazuje na koreňový uzol stromu stránok. Tento koreňový uzol je slovník s /Type /Pages, poľom nepriamych odkazov /Kids a položkou /Count, ktorá udáva celkový počet listových stránok pod ním. Poradie zobrazenia je určené prechodom tohto stromu do hĺbky zľava doprava, a ničím iným.

Minimálny trojstranový súbor to ilustruje v praxi:

%PDF-1.7

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

2 0 obj
<< /Type /Pages /Kids [20 0 R  4 0 R  9 0 R] /Count 3 >>
endobj

% Object 4 is stored third in the file but is page 2 in display order
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 5 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

% Object 9 is stored fourth but is page 3
9 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 10 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

% Object 20 is stored last but is page 1; Kids[0] decides, not object number
20 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
   /Contents 21 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj

Pole /Kids obsahuje [20 0 R 4 0 R 9 0 R], takže objekt 20 je stránka 1, objekt 4 je stránka 2 a objekt 9 je stránka 3. Číslovanie objektov je nepodstatné. Akýkoľvek kód, ktorý prechádza objekty v číselnom poradí a zbiera tie s /Type /Page, vytvorí pri tomto súbore nesprávne poradie.

Prečo generátory vytvárajú nesekvenčné rozloženia? Existuje na to niekoľko dôvodov. Knižnica, ktorá predbežne alokuje čísla objektov pre všetky stránky pred zápisom ich obsahu, ich očísluje v poradí vytvorenia a potom zapíše skutočné bajty v akomkoľvek poradí, ktoré vyhovuje serializátoru. Nástroj na spájanie (merge), ktorý spája dokumenty dohromady, prečísluje objekty z každého zdrojového dokumentu, aby sa zabránilo kolíziám. Prečíslované objekty stránok tak skončia roztrúsené po celej tabuľke kombinovaných objektov, zatiaľ čo nové koreňové pole /Kids obsahuje správne poradie zobrazenia. Prírastkové aktualizácie (incremental updates) pridávajú nové objekty na koniec súboru s novými číslami, takže stránka pridaná ako revízia sa nachádza blízko konca bajtového prúdu, aj keď v poradí zobrazenia patrí na prvú pozíciu.

Ploché stromy a vnorené podstromy

Špecifikácia povoľuje dva tvary stromu stránok. Jednoduché generátory vytvárajú plochú štruktúru: jeden koreňový uzol /Pages, ktorého pole /Kids obsahuje iba listové objekty /Page. To sa prechádza ľahko: jedna úroveň hĺbky, jeden prechod.

Veľké dokumenty bežne používajú vyvážený strom. Pole /Kids koreňového uzla /Pages obsahuje prechodné uzly /Pages, z ktorých každý má svoje vlastné pole /Kids. Hodnota /Count na každom prechodnom uzle udáva celkový počet listových stránok v jeho podstrome, takže prehliadač môže pri prechode na stránku podľa indexu preskočiť celé podstromy bez toho, aby musel spracovať každý objekt. Dokument s 1 000 stránkami štruktúrovaný ako vyvážený strom s 10 stránkami na listový uzol dokáže lokalizovať stránku 750 pomocou binárneho vyhľadávania cez tri alebo štyri vyhľadania v slovníku, namiesto prechádzania 750 položiek /Kids.

Dôsledok pre kód na spracovanie: nemôžete predpokladať, že prvá úroveň /Kids obsahuje objekty /Page. Každý potomok sa musí skontrolovať. Ak je jeho /Type rovný /Pages, prejdite doň rekurzívne. Ak je jeho /Type rovný /Page, ide o list. Zastavenie na prvej úrovni potichu vynechá celé podstromy v každom dokumente, kde sa generátor rozhodol pre vnorenie.

Zdedené atribúty stránky

Strom stránok podporuje aj mechanizmus zdieľania prostriedkov. Určité atribúty stránky: /MediaBox, /CropBox, /Resources a /Rotate sú dedičné (ISO 32000-2 §7.7.3.4). Ak slovník /Page niektorý z nich vynechá, prehliadač prechádza reťazcom /Parent nahor, kým atribút nenájde alebo nedosiahne koreň. Umiestnenie zdieľaného slovníka písiem do koreňového uzla /Pages namiesto jeho kopírovania do každej listovej stránky môže výrazne zmenšiť veľkosť súboru pri dokumentoch, ktoré používajú rovnaké písma v celom rozsahu.

Pravidlo o dedení prináša komplikáciu pre kód, ktorý číta vlastnosti stránky. Čítať /MediaBox priamo z objektu /Page a považovať chýbajúci kľúč za chybu je nesprávne; kľúč môže byť jednoducho zdedený. Kód, ktorý správne určuje geometriu stránky, musí sledovať reťazec rodičov. Potrebuje tiež ochranu pred zacyklením: poškodený súbor môže doomed mať odkaz /Parent, ktorý ukazuje späť na už navštívený uzol, čo by bez kontroly navštívených objektov viedlo k nekonečnej slučke.

Tabuľka xref a prúdy krížových odkazov

Vyhľadávanie nepriamych objektov prebieha cez tabuľku krížových odkazov (alebo jej nástupcu, prúd krížových odkazov predstavený v PDF 1.5). Tabuľka xref mapuje každé číslo objektu na bajtový posun (offset) v súbore. Kompatibilný prehliadač používa xref na priamy skok na ľubovoľný objekt; neprechádza súbor sekvenčne. Tento dizajn s náhodným prístupom umožňuje rýchle preskakovanie stránok: prehliadač prečíta katalóg, rozlíši odkaz /Pages pomocou xref, načíta koreňový uzol /Pages, rozlíši položku /Kids a tak ďalej, pričom pracuje iba s objektmi, ktoré potrebuje.

Prírastkové aktualizácie pridávajú novú sekciu xref na koniec súboru s ukončením (trailer), ktoré odkazuje späť na predchádzajúcu sekciu. Objekt aktualizovaný v revízii dostane nový záznam v pripojenej sekcii xref; pôvodné bajty zostávajú na svojom mieste, ale sú nahradené. Vďaka tomu zostávajú digitálne podpísané PDF súbory overiteľné aj po pridaní anotácií alebo úpravách formulárov: podpísaný rozsah bajtov sa nikdy nemení a nový obsah sa ukladá do pripojenej sekcie. Aktualizovať sa dá aj strom stránok, takže pridanie alebo odstránenie stránok v revízii vytvorí nový koreň /Pages s upraveným poľom /Kids, zatiaľ čo starý koreňový objekt stále zaberá svoju pôvodnú pozíciu v súbore.

Čo sa pokazí bez prechádzania stromu

Zlyhanie pri prístupoch založených na skenovaní objektov prebieha ticho. Výstupný dokument vyzerá vierohodne: má správny počet stránok a každá stránka obsahuje rozpoznateľný obsah. Poradie je však nesprávne, a to spôsobom, ktorý závisí od generátora, počtu revízií a toho, či boli niektoré stránky zlúčené z externých zdrojov. Zlyhať môže napríklad proces spájania dokumentov. Testovací korpus súborov vytvorených jedným nástrojom môže prejsť bez chýb, ale súbory z iného nástroja alebo procesu spájania zlyhajú. Táto nekonzistentnosť je dôvodom, prečo heuristické opravy nikdy nefungujú spoľahlivo.

Súbory s prírastkovými aktualizáciami sú na to obzvlášť náchylné, pretože stránky pridané alebo reorganizované v neskorších revíziách majú vysoké čísla objektov, zatiaľ čo poradie zobrazenia je riadené aktualizovaným poľom /Kids. Skenovanie, ktoré spracováva objekty v číselnom poradí, umiestni tieto stránky s vysokými číslami na koniec bez ohľadu na to, kam podľa stromu patria.

Náprava nie je zložitá. Začnite v katalógu, rozlíšte odkaz /Pages, rekurzívne prejdite pole /Kids a spracujte listy v poradí, v akom na ne narazíte. To je z definície poradie zobrazenia, bez ohľadu na čísla objektov, bajtové posuny alebo štruktúru súboru. Väčšina vyspelých PDF knižníc poskytuje počet stránok a indexovaný prístup k stránkam, ktoré to už robia správne. Riziko spočíva v kóde, ktorý obchádza model stránok knižnice a pristupuje priamo k vrstve objektov.

Jedna štrukturálna anomália, ktorú stojí za to riešiť explicitne: hodnota /Count v prechodnom uzle /Pages môže byť v poškodených súboroch nesprávna. Dôvera v hodnotu /Count pri kontrole hraníc a následné predčasné ukončenie prechodu potichu vynechá stránky, ak je počet podhodnotený. Bezpečnejším prístupom je používať /Count iba ako výkonnostnú pomôcku pre predbežnú alokáciu kapacity alebo binárne vyhľadávanie a skutočný počet odvodiť zo samotného prechodu.

 Next Article