Slovník katalogu (Catalog) v PDF má přesně jeden povinný navigační klíč: /Pages. Tento klíč musí ukazovat na nepřímý objekt typu /Pages, který naopak obsahuje pole /Kids a celkový počet stránek /Count. Odstraňte tento ukazatel a žádný odpovídající prohlížeč nebude schopen v souboru najít jedinou stránku. ISO 32000-1 §7.7.2 hovoří v tomto bodě jasně: katalog musí obsahovat položku /Pages a odkazovaný objekt musí být typu /Pages. Soubory, které tento požadavek porušují, nejsou pouze nevyhovující; jsou strukturálně poškozené způsobem, s nímž se většina parserů špatně vyrovnává
Co vlastně říká specifikace
Minimální vyhovující PDF má alespoň tři objekty. Objekt 1 je kořenový adresář (Catalog), objekt 2 je kořen stránek (Pages root) a objekty 3 a dále jsou jednotlivé slovníky stránek (Page). Katalog ukazuje na kořen stránek; kořen stránek vypisuje své potomky v /Kids; každá stránka nese zpětný odkaz /Parent. Celý řetězec je záměrně obousměrný, takže parser může začít z kteréhokoli konce a projít k jakékoliv stránce v čase O(log n) u vyvážených stromů
% 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 stránek může být zanořený. Dokument s tisíci stránkami typicky seskupuje stránky do pomocných uzlových objektů (intermediate nodes), které také nesou typ /Pages, přičemž každý má své vlastní /Kids a /Count odrážející podstrom pod ním. Hodnota /Count u kořenového uzlu se vždy rovná celkovému počtu stránek. Právě tento počet prohlížeče zobrazují v poli čísla stránky ještě předtím, než zpracují jedinou stránku, protože přečtení jednoho celého čísla z objektu 2 je mnohem méně náročné než průchod celým stromem
Jak vypadá soubor bez Pages
Soubory, kterým chybí slovník Pages, typicky pocházejí od generátorů PDF, které zapisují objekty stránek přímo, aniž by je seskládaly do stromu, nebo vznikají poškozením, které odstraní kořenový uzel, ale ponechá listové objekty stránek (leaf Page objects) neporušené. Katalog v takovém souboru buď zcela postrádá klíč /Pages, nebo obsahuje odkaz na objekt, který v tabulce křížových odkazů již 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, který se řídí specifikací, přečte katalog, pokusí se vyřešit /Pages, nenajde nic (nebo mrtvý odkaz) a buď vyvolá chybu, nebo ohlásí nula stránek. Rozhodně nesmí pokračovat, jako by měl soubor nula stránek a tiše uspět; to by vygenerovalo prázdný výstup, který automatizovaným nástrojům připadá správný, ale každému člověku, který ho otevře, nesprávný
Proč parsery padají
Většina PDF parserů alokuje svou interní tabulku stránek při načítání na základě hodnoty /Count z kořene Pages. Když tento kořen chybí, parser buď načte nulu, nealokuje nic a následně dereferencuje nulový ukazatel hned při prvním požadavku na stránku 1, nebo načte nesmyslná data a alokuje zcela nesprávný buffer. Žádný z těchto výsledků není elegantní. Porušení přístupu (access violation) na adrese 0x008E5D78, které se objevuje v protokolech o chybách (crash logs) při zpracování takového souboru, je přesně toto: dereference nulového ukazatele uvnitř cesty k přístupu na stránku, spuštěná absencí struktury, o které parser předpokládal, že tam vždy bude
Základní předpoklad návrhu je rozumný. Drtivá většina existujících PDF má slovník Pages. Parsery, které kvůli ušetření několika instrukcí vynechávají kontrolu existence, nejsou bezohledné; optimalizují pro běžný případ. Soubory, které tuto optimalizaci trestají, jsou natolik vzácné, že se s nimi produkční kód nemusí nikdy setkat, dokud se tak ovšem nestane. V tu chvíli je chyba plně reprodukovatelná, avšak matoucí pro každého inženýra, který si nepřečetl §7.7.2
Obnova bez stromu Pages
Pokud parser musí s těmito soubory pracovat místo toho, aby je zamítl, obnova probíhá předvídatelnou cestou: naskenuje se každý nepřímý objekt v tabulce křížových odkazů, shromáždí se ty s hodnotou /Type /Page a seřadí se podle čísla objektu. Specifikace sice nezaručuje, že pořadí podle čísel objektů odpovídá pořadí čtení, ale v praxi mají generátory vynechávající strom Pages tendenci generovat stránky sekvenčně, takže se pořadí čísel objektů ukazuje většinou jako správné
Samotná kontrola je nenáročná. Před procházením ukazatele katalogu /Pages ověřte, zda tento ukazatel existuje, zda ukazuje na skutečný objekt a zda se /Type tohoto objektu rovná /Pages. Pokud jakákoliv z těchto tří podmínek selže, přejděte na lineární sken. Skenování je pro velké dokumenty pomalejší než procházení stromu, protože čte hlavičku každého objektu místo toho, aby sledovalo vyváženou cestu, ale funguje to a pro soubor, který už je stejně poškozený, má správnost přednost před rychlostí
Existuje jeden hraniční případ, který lineární sken automaticky nevyřeší: pořadí stránek. Bez pole /Kids, které by definovalo posloupnost, není „správné“ pořadí ve specifikaci definováno. Pořadí podle čísel objektů je pragmatickou výchozí volbou; pokud je ale soubor natolik důležitý, že si zaslouží pečlivé zpracování, vyplatí se zkontrolovat, zda objekty Page nenesou explicitní /StructParents nebo odkazy na anotace, jež naznačují pořadí čtení
Důsledky pro generátory PDF
Pro každého, kdo píše generátor PDF, a nikoliv parser, plyne jednoduché ponaučení: před uzavřením souboru vždy vygenerujte kořen stránek (Pages root). Katalog bez položky /Pages není platným PDF podle žádné revize specifikace. Generátory, které vytvářejí objekty stránek za běhu a sestavují strom při finalizaci (což je přístup používaný většinou streamingových zapisovačů), jsou v pořádku, dokud finalizace skutečně proběhne. Běžným scénářem selhání je výjimka nebo předčasný návrat (early return), který přeruší zápis ještě před dokončením traileru a zanechá po sobě soubor, který se otevře v některých prohlížečích (vybavených heuristikou pro obnovu), zatímco v jiných selže (u těch, které jí nedisponují)
PDF/A a PDF/UA kladou na strom stránek další omezení nad rámec toho, co vyžaduje základní specifikace, ale ani jedna z nich nezmírňuje požadavek na /Pages. Validátor kontrolující shodu s ISO 19005 nebo ISO 14289 zachytí chybějící slovník Pages jako porušení základní specifikace ještě předtím, než se vůbec dostane ke specifickým pravidlům profilu