Technical Article

PDF bez slovníka Pages: Dôsledky pre parsovanie

Slovník PDF Catalog (katalóg) obsahuje presne jeden povinný navigačný kľúč: /Pages. Tento kľúč musí odkazovať na nepriamy objekt typu /Pages, ktorý zasa obsahuje pole /Kids a celkový počet strán /Count. Ak tento ukazovateľ odstránite, žiadna kompatibilná čítačka nedokáže v súbore lokalizovať ani jednu stránku. Norma ISO 32000-1 §7.7.2 hovorí jasne: katalóg musí obsahovať položku /Pages a odkazovaný objekt musí byť typu /Pages. Súbory, ktoré túto požiadavku porušujú, nie sú len nekompatibilné, ale sú štrukturálne poškodené spôsobom, s ktorým si väčšina parserov nevie rady.

Čo v skutočnosti hovorí špecifikácia

Minimálne kompatibilné PDF má aspoň tri objekty. Objekt 1 je Catalog (katalóg), objekt 2 je koreň Pages (stránky) a objekt 3 a ďalšie sú jednotlivé slovníky Page (strana). Katalóg ukazuje na koreň Pages, koreň Pages uvádza svojich potomkov v poli /Kids a každá stránka obsahuje spätný odkaz /Parent. Celý reťazec je zámerne obojsmerný, takže parser môže začať z ktoréhokoľvek konca a pri vyvážených stromoch prejsť k akejkoľvek stránke v čase O(log n).

% 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

Strom Pages môže byť vnorený. Dokument s tisíckami strán zvyčajne zoskupuje strany do prechodných uzlových objektov, ktoré sú tiež typu /Pages, pričom každý má vlastné pole /Kids a hodnotu /Count odrážajúcu podstrom pod ním. Hodnota /Count koreňového uzla sa vždy rovná celkovému počtu strán. Tento počet zobrazujú prehliadače v poli čísla stránky ešte predtým, ako analyzujú jedinú stranu, pretože prečítanie jedného celého čísla z objektu 2 je oveľa rýchlejšie ako prechod celým stromom.

Ako vyzerá súbor bez slovníka Pages

Súbory, v ktorých chýba slovník Pages, zvyčajne pochádzajú z PDF generátorov, ktoré zapisujú objekty stránok priamo bez ich zostavenia do stromu, alebo z poškodenia, ktoré odstráni koreňový uzol, no ponechá koncové objekty stránok (Page) neporušené. Katalóg v takomto súbore buď úplne postráda kľúč /Pages, alebo obsahuje odkaz na objekt, ktorý už v tabuľke krížových odkazov neexistuje.

% 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, ktorý sa riadi špecifikáciou, prečíta katalóg, pokúsi sa nájsť kľúč /Pages, nič (alebo neplatný odkaz) nenájde a buď vyvolá chybu, alebo nahlási nula stránok. Čo však nesmie urobiť, je pokračovať, akoby mal súbor nula stránok a ticho uspieť. To by totiž viedlo k prázdnemu výstupu, ktorý sa automatickým nástrojom javí ako správny, no pre každého človeka, ktorý ho otvorí, je nesprávny.

Prečo parsery padajú

Väčšina parserov PDF alokuje svoju internú tabuľku stránok pri načítaní na základe hodnoty /Count z koreňa Pages. Keď tento koreň chýba, parser buď načíta nuku, nič nealokuje a potom pri prvom pokuse kódu o prístup k strane 1 odkáže na nulový ukazovateľ (null pointer dereference), alebo načíta nesprávne dáta a alokuje úplne chybnú vyrovnávaciu pamäť. Ani jeden z týchto výsledkov nie je korektný. Porušenie prístupu na adrese 0x008E5D78, ktoré sa objavuje v chybových logoch pri spracovaní takýchto súborov, je presne toto: dereferencia nulového ukazovateľa v prístupovej ceste k stránke vyvolaná absenciou štruktúry, o ktorej parser predpokladal, že bude prítomná.

Východiskový predpoklad návrhu je rozumný. Drvivá väčšina existujúcich PDF obsahuje slovník Pages. Parsery, ktoré vynechávajú kontrolu existencie s cieľom ušetriť niekoľko inštrukcií, nepostupujú bezohľadne. Iba optimalizujú pre bežný prípad. Súbory, ktoré túto optimalizáciu potrestajú, sú natoľko zriedkavé, produkčný kód na ne nemusí naraziť veľmi dlho. Keď sa tak však stane, pád je reprodukovateľný a pre vývojára, ktorý nečítal sekciu §7.7.2, úplne nepochopiteľný.

Obnova bez stromu Pages

Ak parser musí tieto súbory spracovať namiesto ich odmietnutia, obnova prebieha predvídateľným spôsobom: prehľadá sa každý nepriamy objekt v tabuľke krížových odkazov, zozbierajú sa tie, ktoré majú /Type /Page, a zoradia sa podľa čísla objektu. Poradie čísel objektov špecifikácia negarantuje ako zhodné s poradím čítania, no v praxi generátory, ktoré strom Pages vynechávajú, zapisujú stránky sekvenčne, takže zoradenie podľa čísla objektu býva vo väčšine prípadov správne.

Samotná kontrola je nenáročná. Pred prejdením na ukazovateľ /Pages z katalógu overte, či tento ukazovateľ existuje, či odkazuje na skutočný objekt a či typ /Type odkazovaného objektu je /Pages. Ak ktorákoľvek z týchto troch podmienok zlyhá, prejdite na lineárne skenovanie. Skenovanie je pri veľkých dokumentoch pomalšie ako prechod stromom, pretože číta hlavičku každého objektu namiesto sledovania vyváženej cesty. Funguje však a pri poškodenom súbore má správnosť prednosť pred rýchlosťou.

Jeden okrajový prípad lineárne skenovanie automaticky nerieši: poradie stránok. Bez poľa /Kids, ktoré by definovalo sekvenciu, je „správne“ poradie podľa špecifikácie nedefinované. Zoradenie podľa čísla objektu je pragmatickou predvolenou možnosťou. Ak je súbor dostatočne dôležitý na starostlivé spracovanie, oplatí sa skontrolovať, či objekty Page neobsahujú explicitnú položku /StructParents alebo odkazy na anotácie, ktoré by naznačovali poradie čítania.

Dôsledky pre PDF generátory

Pre vývojárov píšucich generátory PDF namiesto parserov z toho vyplýva jasné ponaučenie: pred zatvorením súboru vždy zapíšte koreň Pages. Katalóg bez položky /Pages nie je platným PDF v žiadnej revízii špecifikácie. Generátory, ktoré vytvárajú objekty stránok za behu a strom zostavujú až pri dokončení (prístup používaný väčšinou prúdových zapisovačov), fungujú správne, pokiaľ sa dokončenie skutočne vykoná. Častým chybovým stavom býva výnimka alebo predčasný návrat, ktorý preruší zápis pred dokončením traileru, čím vznikne súbor, ktorý sa v niektorých prehliadačoch (s heuristikou obnovy) otvorí, no v iných (bez nej) zlyhá.

Štandardy PDF/A a PDF/UA kladú na strom stránok ďalšie obmedzenia nad rámec základnej špecifikácie, no žiaden z nich nepoľavuje v požiadavke na /Pages. Validátor kontrolujúci zhodu s ISO 19005 alebo ISO 14289 odhalí chýbajúci slovník Pages ako porušenie základnej špecifikácie ešte predtým, ako sa dostane k pravidlám špecifickým pre daný profil.