Slovar kataloga PDF (PDF Catalog) ima natanko en zahtevan navigacijski ključ: /Pages. Ta ključ mora kazati na posredni objekt tipa /Pages, ki vsebuje polje /Kids in skupno število strani /Count. Če ta kazalec odstranite, noben skladen bralnik ne more najti niti ene same strani v datoteki. Standard ISO 32000-1 §7.7.2 je v tej točki jasen: katalog mora vsebovati vnos /Pages, nanj sklicujoči se objekt pa mora biti tipa /Pages. Datoteke, ki kršijo to zahtevo, niso le neskladne, ampak so strukturno poškodovane na način, ki ga večina parserjev slabo obravnava.
Kaj dejansko določa specifikacija
Minimalen skladen PDF vsebuje vsaj tri objekte. Objekt 1 je katalog (Catalog), objekt 2 je koren strani (Pages root), objekt 3 in naslednji pa so posamezni slovarji strani (Page dictionaries). Katalog kaže na koren strani; koren strani navaja svoje otroke v polju /Kids; vsaka posamezna stran pa vsebuje povratni sklic /Parent. Celotna veriga je dvosmerno zasnovana, zato lahko parser začne na obeh koncih in prehodi pot do katere koli strani v času O(log n) za uravnotežena drevesa.
% Minimal conforming structure (ISO 32000-1 §7.7.2)
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 5 0 R /Resources << >> >>
endobj
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj
Drevo strani (Pages tree) je lahko gnezdeno. Dokument s tisoči strani običajno združuje strani v vmesne vozne objekte, ki so prav tako tipa /Pages, vsak z lastnim poljem /Kids in števcem /Count, ki odraža poddrevo pod njim. Vrednost /Count korenskega vozlišča je vedno enaka skupnemu številu strani. To je tisto število, ki ga pregledovalniki prikažejo v polju za številko strani še pred razčlenjevanjem katere koli strani, saj je branje enega celega števila iz objekta 2 bistveno cenejše od prehajanja celotnega drevesa.
Kako izgleda datoteka brez slovarja Pages
Datoteke brez slovarja Pages običajno izvirajo iz generatorjev PDF, ki zapisujejo objekte strani neposredno, ne da bi jih sestavili v drevo, ali pa so posledica poškodbe datoteke, ki odstrani korensko vozlišče, medtem ko listni objekti strani ostanejo nedotaknjeni. Katalog v takšni datoteki bodisi sploh nima ključa /Pages bodisi vsebuje sklic na objekt, ki ga ni več v tabeli navzkrižnih sklicev.
% Non-conforming: Catalog with no /Pages reference
1 0 obj
<< /Type /Catalog >>
endobj
% Page objects exist but are unreachable from the Catalog
5 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 6 0 R /Resources << >> >>
endobj
15 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 16 0 R /Resources << >> >>
endobj
25 0 obj
<< /Type /Page /MediaBox [0 0 612 792] /Contents 26 0 R /Resources << >> >>
endobj
Parser, ki sledi specifikaciji, bo prebral katalog, poskusil razrešiti ključ /Pages, ugotovil, da ne obstaja (ali pa gre za neveljaven sklic), in sprožil napako ali pa sporočil, da ima dokument nič strani. V nobenem primeru pa ne sme nadaljevati z delom, kot da ima datoteka nič strani in se tiho zaključiti z uspehom; to namreč ustvari prazen izhod, ki se zdi avtomatiziranim orodjem ustrezen, za vsakega človeka, ki datoteko odpre, pa je napačen.
Zakaj se parserji sesujejo
Večina parserjev PDF ob nalaganju dodeli pomnilnik za svojo notranjo tabelo strani na podlagi vrednosti /Count iz korena strani. Ko tega korena ni, parser prebere nič, ne dodeli ničesar in ob prvi zahtevi kode po prvi strani dereferencira prazno kazalo (null pointer), ali pa prebere nepravilne podatke in dodeli povsem napačen medpomnilnik. Noben od teh izidov ni ustrezen. Kršitev dostopa na naslovu 0x008E5D78, ki se pojavi v dnevnikih sesutja ob obdelavi takšne datoteke, je natanko to: dereferenciranje praznega kazalca znotraj poti za dostop do strani, ki ga sproži odsotnost strukture, za katero je parser predvideval, da bo vedno prisotna.
Osnovna predpostavka zasnove je razumna. Velika večina obstoječih PDF-jev vsebuje slovar Pages. Parserji, ki izpustijo preverjanje obstoja, da bi prihranili nekaj ukazov, niso nepremišljeni, ampak le optimizirajo kodo za najpogostejši scenarij. Datoteke, ki kaznujejo to optimizacijo, so dovolj redke, da produkcijska koda nanje morda nikoli ne bo naletela, dokler se to ne zgodi. V tistem trenutku je sesutje ponovljivo in hkrati nerazumljivo, če inženir predhodno ni prebral poglavja §7.7.2.
Obnova brez drevesa Pages
Če mora parser te datoteke obdelati in ne le zavrniti, obnova sledi predvidljivi poti: skenirati je treba vsak posredni objekt v tabeli navzkrižnih sklicev, zbrati tiste s tipom /Type /Page in jih razvrstiti po številki objekta. Vrstni red številk objektov sicer ne zagotavlja vrstnega reda branja po specifikaciji, vendar v praksi generatorji, ki izpustijo drevo Pages, običajno zapišejo strani zaporedno, zato je vrstni red po številkah objektov večinoma pravilen.
Samo preverjanje je poceni. Preden sledite kazalcu /Pages iz kataloga, potrdite, da kazalec obstaja, da kaže na dejanski objekt in da je tip ciljnega objekta /Type enak /Pages. Če kateri koli od teh treh pogojev ni izpolnjen, preklopite na linearno skeniranje. Skeniranje je pri večjih dokumentih počasnejše od prehajanja drevesa, ker mora prebrati glavo vsakega objekta namesto sledenja uravnoteženi poti, vendar deluje. Pri datoteki, ki je že v osnovi poškodovana, pa je pravilnost pomembnejša od hitrosti.
Obstaja en robni primer, ki ga linearno skeniranje ne reši samodejno: vrstni red strani. Brez polja /Kids, ki bi določalo zaporedje, je "pravilen" vrstni red po specifikaciji nedoločen. Zaporedje po številkah objektov je pragmatična privzeta izbira; če pa je datoteka dovolj pomembna za natančno obdelavo, je smiselno preveriti, ali objekti strani vsebujejo eksplicitno polje /StructParents oz. sklice na komentarje, ki nakazujejo vrstni red branja.
Posledice za generatorje PDF
Za vsakogar, ki razvija generator PDF in ne parserja, je nauk preprost: pred zaprtjem datoteke vedno zapišite koren Pages. Katalog brez vnosa /Pages ni veljaven PDF po nobeni različici specifikacije. Generatorji, ki sproti ustvarjajo objekte strani in sestavijo drevo šele ob zaključku shranjevanja (kar uporablja večina pretočnih pisalnikov), delujejo pravilno le, če se postopek zaključevanja dejansko izvede. Pogosta napaka je izjema ali predčasen zaključek, ki prekine zapisovanje pred dokončanjem napovednika, kar zapusti datoteko, ki se v nekaterih pregledovalnikih (ki imajo hevristiko za obnovo) odpre, v drugih (ki je nimajo) pa ne.
Standarda PDF/A in PDF/UA uvajata dodatne omejitve za drevo strani poleg zahtev osnovne specifikacije, vendar noben od njiju ne sprošča zahteve po vnosu /Pages. Orodje za preverjanje skladnosti z ISO 19005 ali ISO 14289 bo manjkajoč slovar Pages zaznalo kot kršitev osnovne specifikacije, še preden doseže pravila, specifična za posamezen profil.