Technical Article

PDF bez rječnika Pages: implikacije parsiranja

Katalog (Catalog) PDF-a ima točno jedan obvezan navigacijski ključ: /Pages. Taj ključ mora pokazivati na neizravni objekt tipa /Pages, koji pak sadrži polje /Kids i ukupan broj stranica (/Count). Ako uklonite taj pokazivač, nijedan usklađeni preglednik ne može locirati niti jednu stranicu u datoteci. ISO 32000-1 §7.7.2 je nedvosmislen po tom pitanju: katalog mora imati unos /Pages, a referencirani objekt mora biti tipa /Pages. Datoteke koje krše ovaj zahtjev nisu samo neusklađene sa standardom; one su strukturno neispravne na način koji većina parsera loše podnosi.

Što specifikacija zapravo kaže

Minimalni usklađeni PDF ima najmanje tri objekta. Objekt 1 je katalog (Catalog), objekt 2 je korijen stranica (Pages root), a od objekta 3 nadalje su pojedinačni rječnici stranica (Page dictionaries). Katalog pokazuje na korijen stranica; korijen stranica navodi svoju djecu u polju /Kids; svaka stranica nosi povratnu referencu /Parent. Cijeli lanac je namjerno dvosmjeran, tako da parser može početi s bilo kojeg kraja i proći do bilo koje stranice u vremenu O(log n) za balansirana stabla.

% Minimalna usklađena struktura (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

Stablo stranica može biti ugniježđeno. Dokument s tisućama stranica obično grupirira stranice u objekte srednjih čvorova koji također nose tip /Pages, od kojih svaki ima vlastiti /Kids i /Count koji odražava podstablo ispod njega. /Count korijenskog čvora uvijek je jednak ukupnom broju stranica. Taj broj je ono što preglednici prikazuju u polju za broj stranice prije nego što analiziraju ijednu stranicu, jer je čitanje jednog cijelog broja iz objekta 2 mnogo jeftinije od prolaska kroz cijelo stablo.

Kako izgleda datoteka bez Pages rječnika

Datoteke kojima nedostaje rječnik Pages obično potječu od PDF generatora koji pišu objekte stranica izravno bez sastavljanja u stablo, ili od oštećenja koje uklanja korijenski čvor dok ostavlja objekte stranica (listove) netaknutima. Katalog u takvoj datoteci ili uopće nema ključ /Pages ili sadrži referencu na objekt koji više ne postoji u tablici unakrsnih referenci.

% 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 koji prati specifikaciju pročitat će katalog, pokušati razriješiti /Pages, pronaći ništa (ili mrtvu referencu) i ili javiti pogrešku ili prijaviti nula stranica. Ono što ne smije učiniti jest nastaviti kao da datoteka ima nula stranica i tiho uspjeti; to proizvodi prazan izlaz koji se automatiziranim alatima čini ispravnim, a pogrešan je svakom čovjeku koji ga otvori.

Zašto se parseri ruše

Većina PDF parsera alocira svoju internu tablicu stranica u trenutku učitavanja na temelju vrijednosti /Count iz korijena Pages. Kada taj korijen nedostaje, parser ili čita nulu, ne alocira ništa i zatim dereferencira null pokazivač prvi put kada kod zatraži stranicu 1, ili čita smeće i alocira potpuno pogrešan međuspremnik. Nijedan ishod nije elegantan. Povreda pristupa (access violation) na adresi 0x008E5D78 koja se pojavljuje u dnevnicima rušenja pri obradi takve datoteke upravo je to: dereferenciranje null pokazivača unutar staze pristupa stranici, izazvano nedostatkom strukture za koju je parser pretpostavio da će uvijek biti tamo.

Temeljna pretpostavka dizajna je razumna. Velika većina postojećih PDF-ova ima rječnik Pages. Parseri koji preskaču provjeru postojanja kako bi uštedjeli nekoliko instrukcija nisu nemarni; oni optimiziraju za uobičajeni slučaj. Datoteke koje kažnjavaju tu optimizaciju dovoljno su rijetke da ih produkcijski kod možda nikada neće susresti dok se to ne dogodi, a u tom je trenutku rušenje i ponovljivo i zbunjujuće ako inženjer nije pročitao odjeljak §7.7.2.

Oporavak bez stabla Pages

Ako parser mora obraditi te datoteke umjesto da ih odbaci, oporavak prati predvidljivu stazu: skenira se svaki neizravni objekt u tablici unakrsnih referenci, prikupljaju se oni koji imaju /Type /Page i sortiraju se po broju objekta. Redoslijed brojeva objekata ne jamči podudaranje s redoslijedom čitanja u specifikaciji, ali u praksi generatori koji izostavljaju stablo Pages obično emitiraju stranice sekvencijalno, tako da je redoslijed brojeva objekata točan u većini slučajeva.

Sama provjera je jeftina. Prije praćenja pokazivača /Pages u katalogu, potvrdite da pokazivač postoji, da se razrješava u stvarni objekt i da je /Type razriješenog objekta jednak /Pages. Ako bilo koji od ova tri uvjeta ne uspije, prijeđite na linearno skeniranje. Skeniranje je sporije od prolaska kroz stablo za velike dokumente jer čita svako zaglavlje objekta umjesto da prati balansirani put, ali radi, a za datoteku koja je već neispravna, točnost je važnija od brzine.

Jedan granični slučaj koji linearno skeniranje ne rješava automatski: redoslijed stranica. Bez polja /Kids koje definira slijed, 'ispravan' redoslijed nije definiran specifikacijom. Redoslijed brojeva objekata je pragmatična zadana opcija; ako je datoteka dovoljno važna za pažljivu obradu, vrijedi provjeriti nose li objekti Page eksplicitan /StructParents ili reference napomena koje impliciraju redoslijed čitanja.

Implikacije za PDF generatore

Za svakoga tko piše PDF generator umjesto parsera, lekcija je uska: uvijek emitirajte korijen Pages prije zatvaranja datoteke. Katalog bez unosa /Pages nije valjan PDF pod niti jednom revizijom specifikacije. Generatori koji grade objekte stranica u letu i sastavljaju stablo pri finalizaciji (pristup koji koristi većina strujnih pisača) rade u redu sve dok se finalizacija stvarno izvodi. Uobičajeni način zakazivanja je iznimka ili rani povratak koji prekida pisanje prije nego što je trailer dovršen, ostavljajući iza sebe datoteku koja se otvara u nekim preglednicima (koji imaju heuristiku oporavka), a ne uspijeva u drugima (koji je nemaju).

PDF/A i PDF/UA nameću dodatna ograničenja na stablo stranica izvan onoga što zahtijeva osnovna specifikacija, ali nijedan ne ublažava zahtjev /Pages. Validator koji provjerava usklađenost s ISO 19005 ili ISO 14289 uočit će nedostatak rječnika Pages kao kršenje osnovne specifikacije prije nego što uopće dođe do pravila specifičnih za profil.