Stránka 1 v PDF nie je objekt 1. Tento rozdiel je najčastejším zdrojom chýb typu „extrahovaná nesprávna stránka“ v parseroch PDF a náprava spočíva v čítaní špecifikácie, nie bajtov súboru.
Objekty, odkazy a katalóg
PDF súbor je kolekcia očíslovaných objektov. Každý z nich nesie jedinečné číslo objektu a číslo generácie, zapísané ako N G obj, kde G je takmer vždy 0 v súboroch, ktoré neboli prírastkovo aktualizované. Objekty na seba odkazujú zápisom N G R, takže 3 0 R znamená „aktuálna verzia objektu 3“. Ukončenie súboru (trailer) ukazuje na koreňový objekt katalógu, ktorého položka /Pages vedie k stromu stránok. Všetko, čo sa dá v PDF navigovať, začína od tohto koreňa, nie od prvého bajtu tela súboru.
Tabuľka krížových odkazov (alebo prúd krížových odkazov v PDF 1.5+) mapuje čísla objektov na posuny (offsety) v súbore. Jej úlohou je náhodný prístup, nie určovanie poradia. Zapisovač, ktorý zostavuje dokument prírastkovo, môže na koniec pridať nové objekty s vyššími číslami, aj keď tieto objekty logicky predchádzajú existujúcim objektom v sekvencii stránok. To nie je chyba, ale zámer.
Strom stránok (ISO 32000-1 §7.7.3)
Sekvencia stránok sa nachádza v strome stránok. Koreňový katalóg obsahuje odkaz /Pages, ktorý ukazuje na uzol typu /Pages. Pole /Kids tohto uzla uvádza jeho potomkov v poradí na čítanie. Každý potomok je buď listový uzol typu /Page, alebo iný prechodný uzol /Pages obsahujúci vlastné pole /Kids. Stránka 1 je prvý list dosiahnutý prechodom polí Kids do hĺbky zľava doprava. Položka /Count v každom prechodnom uzle ukladá celkový počet odvodených listových stránok do vyrovnávacej pamäte, takže prehliadač môže skočiť na stránku 500 bez toho, aby musel prechádzať celý strom.
Takto vyzerá minimálny trojstranový strom v čistej syntaxi PDF:
16 0 obj
<<
/Type /Pages
/Count 3
/Kids [20 0 R 1 0 R 4 0 R]
/MediaBox [0 0 612 792]
>>
endobj
20 0 obj
<< /Type /Page /Parent 16 0 R /Contents 21 0 R /Resources 22 0 R >>
endobj
1 0 obj
<< /Type /Page /Parent 16 0 R /Contents 2 0 R /Resources 3 0 R >>
endobj
4 0 obj
<< /Type /Page /Parent 16 0 R /Contents 5 0 R /Resources 6 0 R >>
endobj
Pole Kids obsahuje [20 0 R, 1 0 R, 4 0 R]. Logická stránka 1 je objekt 20, logická stránka 2 je objekt 1, logická stránka 3 je objekt 4. Akýkoľvek kód, ktorý prechádza čísla objektov od 1 nahor, na ne narazí v poradí 1, 4, 20 a vytvorí poradie stránka-2, stránka-3, stránka-1. Výsledný dokument sa potom vykreslí v premiešanom poradí, ktoré môže v prehliadači sledujúcom strom vyzerať úplne normálne, ale v prehliadači, ktorý ho nesleduje, bude katastrofálne nesprávne.
Dedenie
Prechodné uzly môžu niesť vlastnosti, ktoré ich potomkovia dedia. Najčastejšími zdedenými položkami sú /MediaBox (rozmery stránky), /CropBox, /Resources (písma a obrázky) a /Rotate. Listová stránka, ktorá vynecháva /MediaBox, nie je poškodená; preberá hodnotu z najbližšieho predka, ktorý ju definuje. Stránka, ktorá definuje vlastný /MediaBox, prepisuje hodnotu definovanú rodičom, a to iba pre túto konkrétnu stránku.
To je dôležité pri analýze (parsing). Čítanie objektu /Page izolovane a predpoklad, že jeho vlastnosti sú kompletné, povedie k nesprávnemu nahláseniu rozmerov pri každej stránke, ktorá sa spolieha na dedenie. Správna čítačka prechádza reťazcom /Parent nahor, zbiera vlastnosti, ktoré ešte nenašla, a zastaví sa na koreni.
Vnorené stromy
Nič v špecifikácii neobmedzuje strom na jedinú úroveň. Veľký dokument môže zoskupovať stránky pod prechodnými uzlami, ktoré voľne zodpovedajú kapitolám:
2 0 obj % root Pages node, Count = 8
<< /Type /Pages /Count 8 /Kids [3 0 R 4 0 R] >>
endobj
3 0 obj % first chapter, 5 pages
<< /Type /Pages /Parent 2 0 R /Count 5
/Kids [10 0 R 11 0 R 12 0 R 13 0 R 14 0 R]
/MediaBox [0 0 612 792] >>
endobj
4 0 obj % second chapter, 3 pages
<< /Type /Pages /Parent 2 0 R /Count 3
/Kids [20 0 R 21 0 R 22 0 R]
/MediaBox [0 0 612 792] >>
endobj
Algoritmus prechodu je rovnaký: navštívte Kids v danom poradí, rekurzívne prejdite do každého uzla /Pages a zozbierajte listové uzly /Page. Hodnoty /Count umožňujú prehliadaču preskočiť celý podstrom pri skoku na stránku, ktorá leží za ním, čo je dôvod, prečo musia byť tieto počty presné. Niektoré editory PDF z konca 90. rokov a začiatku 21. storočia ich po priamych úpravách neprepočítavali, preto defenzívny parser overuje /Count voči skutočnému počtu listov namiesto toho, aby mu dôveroval pri alokácii poľa.
Kde sa to prejavuje v praxi
Chyba poradia stránok sa najčastejšie objavuje v dvoch scenároch. Prvým je vlastný parser, ktorý skenuje objekty typu /Page namiesto sledovania stromu. Nájde síce každú stránku, ale v poradí podľa čísiel objektov, nie v poradí na čítanie. Náprava je vždy rovnaká: začnite od traileru, rozlíšte koreňový katalóg, prejdite na /Pages a sledujte polia Kids.
Druhým scenárom je súbor s prírastkovou aktualizáciou. Keď editor PDF pridáva zmeny bez prepísania celého súboru, nové objekty stránok dostanú vysoké čísla objektov, zatiaľ čo pole Kids v pôvodnom strome stále riadi ich logickú pozíciu. Stránka, ktorá bola pôvodne objektom 5, sa nahradí novým objektom 143, ale pole Kids teraz odkazuje na 143 namiesto 5, takže logické poradie zostáva zachované. Prechádzanie podľa čísiel objektov by náhradnú stránku zaradilo na nesprávne miesto v sekvencii.
Linearizované PDF (optimalizované pre web) prinášajú tretiu variáciu: súbor je fyzicky usporiadaný tak, aby sa obsah prvej stránky nachádzal blízko začiatku súboru na rýchle zobrazenie pri pomalom pripojení. Štruktúra stromu stránok zostáva smerodajná pre poradie, ale tabuľka krížových odkazov odkazuje na preskupené offsety. Parser, ktorý sa spolieha na pozíciu v súbore namiesto tabuľky xref, nesprávne prečíta aj prvú stránku linearizovaného súboru.
Knižnica HotPDF Component rieši prechod stromom stránok, dedenie vlastností a zlučovanie prírastkových aktualizácií xref interne. Priama práca s jej objektmi stránok znamená, že usporiadanie podľa poľa Kids je už aplikované; indexy stránok mapujú logické stránky, nie čísla objektov.