PDF katalogo (Catalog) žodynas turi tiksliai vieną privalomą navigacijos raktą: /Pages. Šis raktas privalo rodyti į netiesioginį /Pages tipo objektą, kuris savo ruožtu turi /Kids masyvą ir bendrą puslapių skaičių /Count. Pašalinkite šią nuorodą, ir joks standartus atitinkantis skaitytuvas negalės rasti nė vieno puslapio faile. Standartas ISO 32000-1 §7.7.2 šiuo klausimu yra vienareikšmis: katalogas privalo turėti /Pages įrašą, o nurodytas objektas turi būti /Pages tipo. Failai, kurie pažeidžia šį reikalavimą, yra ne tiesiog neatitinkantys standartų; jie yra struktūriškai pažeisti taip, kad dauguma analizatorių juos apdoroja sunkiai.
Ką iš tikrųjų sako specifikacija
Minimalus standartus atitinkantis PDF failas turi bent tris objektus. 1 objektas yra katalogas (Catalog), 2 objektas – puslapių šaknis (Pages), o nuo 3 objekto prasideda atskiri puslapių (Page) žodynai. Katalogas nurodo į puslapių šaknį; puslapių šaknis išvardija savo vaikus /Kids sąraše; kiekvienas puslapis turi atgalinę nuorodą /Parent. Visa ši grandinė pagal projektą yra dvikryptė, todėl analizatorius gali pradėti nuo bet kurio galo ir pereiti prie bet kurio puslapio per O(log n) laiką subalansuotuose medžiuose.
% 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
Puslapių medis gali būti lizdinis. Dokumentas su tūkstančiais puslapių paprastai sugrupuoja puslapius į tarpinius mazgų objektus, kurie taip pat yra /Pages tipo, kurių kiekvienas turi savo /Kids sąrašą bei skaičių /Count, rodantį po juo esantį pomedį. Šakninio mazgo /Count visada yra lygus bendram puslapių skaičiui. Šį skaičių skaitytuvai rodo puslapių numerio lauke dar prieš išanalizuodami bent vieną puslapį, nes perskaityti vieną sveikąjį skaičių iš 2 objekto yra daug pigiau nei apeiti visą medį.
Kaip atrodo failas be Pages žodyno
Failai be puslapių (Pages) žodyno paprastai atsiranda iš PDF generatorių, kurie įrašo puslapio objektus tiesiogiai, nesurinkdami jų į medį, arba dėl sugadinimo, kuris pašalina šakninį mazgą, tačiau palieka puslapių (Page) objektus nepažeistus. Katalogas tokiame faile arba visiškai neturi /Pages rakto, arba turi nuorodą į objektą, kurio kryžminių nuorodų lentelėje (xref) nebėra.
% 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
Analizatorius, kuris laikosi specifikacijos, perskaitys katalogą, bandys išspręsti nuorodą /Pages, nieko neras (arba ras neveikiančią nuorodą) ir pateiks klaidą arba praneš apie nulį puslapių. Ko jis neturi daryti – tai tęsti darbo taip, tarsi failas turėtų nulį puslapių, ir tyliai baigti darbą sėkmingai; taip sukuriamas tuščias dokumentas, kuris automatiniams įrankiams atrodo teisingas, tačiau yra neteisingas bet kuriam jį atidariusiam žmogui.
Kodėl analizatoriai lūžta
Dauguma PDF analizatorių įkėlimo metu priskiria savo vidinę puslapių lentelę pagal /Count reikšmę iš puslapių šaknies. Kai šios šaknies nėra, analizatorius arba nuskaito nulį, nieko nepriskiria, o tada bando naudoti nulinę nuorodą (angl. null pointer dereference) pirmą kartą, kai kodas kreipiasi į 1 puslapį, arba nuskaito šiukšles ir išskiria visiškai klaidingą buferį. Nei viena baigtis nėra tinkama. Prieigos pažeidimo klaida ties 0x008E5D78, kuri rodoma lūžių žurnaluose apdorojant tokį failą, yra būtent tai: nulinės nuorodos naudojimas puslapio prieigos kelyje, kurį sukelia struktūros, kurią analizatorius tikėjosi visada rasti, nebuvimas.
Ši struktūros prielaida yra pagrįsta. Didžioji dauguma egzistuojančių PDF failų turi Pages žodyną. Analizatoriai, kurie praleidžia šį egzistavimo patikrinimą, kad sutaupytų keletą instrukcijų, nėra neatsargūs; jie tiesiog atlieka optimizavimą dažniausiam atvejui. Failai, kurie baudžia už šį optimizavimą, yra pakankamai reti, kad gamybinis kodas niekada su jais nesusidurtų, kol galiausiai susiduria, ir tada lūžis yra tiek atkuriamas, tiek keliantis painiavą, jei inžinierius nėra perskaitęs standarto §7.7.2 skyriaus.
Atstatymas be puslapių medžio
Jei analizatorius privalo apdoroti tokius failus, o ne juos atmesti, atstatymas vyksta nuspėjamu keliu: nuskaitomas kiekvienas netiesioginis objektas kryžminių nuorodų lentelėje, surenkami tie, kurie turi /Type /Page, ir surūšiuojami pagal objekto numerį. Nors objektų numerių seka pagal specifikaciją negarantuoja skaitymo sekos, praktikoje generatoriai, kurie praleidžia puslapių medį, linkę išvesti puslapius nuosekliai, todėl objektų numerių seka dažniausiai būna teisinga.
Pats patikrinimas yra pigus. Prieš keliaujant pagal katalogo /Pages nuorodą, patikrinkite, ar nuoroda egzistuoja, ar ji nurodo į realų objektą ir ar gauto objekto /Type yra lygus /Pages. Jei bet kuri iš šių trijų sąlygų nepatenkinama, pereikite prie linijinio skenavimo. Skenavimas dideliuose dokumentuose yra lėtesnis nei medžio perėjimas, nes nuskaito kiekvieno objekto antraštę, o ne seka subalansuotu keliu, tačiau tai veikia, o jau sugadintam failui teisingumas yra svarbesnis už greitį.
Vienas ribinis atvejis, kurio linijinis skenavimas neišsprendžia automatiškai – tai puslapių tvarka. Be /Kids masyvo, apibrėžiančio seką, „teisinga“ tvarka specifikacijoje yra neapibrėžta. Pragmatiškas numatytasis pasirinkimas yra objektų numerių seka. Jei failas yra pakankamai svarbus, verta patikrinti, ar puslapio objektai turi aiškius /StructParents įrašus arba anotacijų nuorodas, kurios leistų nustatyti skaitymo seką.
Pasekmės PDF generatoriams
Kiekvienam, rašančiam PDF generatorių, o ne analizatorių, pamoka yra paprasta: prieš uždarydami failą visada išveskite puslapių šaknį (Pages). Katalogas be /Pages įrašo nėra galiojantis PDF failas pagal jokią specifikacijos versiją. Generatoriai, kurie kuria puslapių objektus realiuoju laiku, o medį surenka tik finalizavimo metu (taip elgiasi dauguma srautinio rašymo programų), veikia gerai, jei tik šis finalizavimas įvyksta. Dažnas gedimo scenarijus yra išimtis arba ankstyvas grąžinimas (early return), kuris nutraukia rašymą prieš užbaigiant trailer dalį, palikdamas failą, kurį kai kurios skaityklės atidaro (nes turi atstatymo heuristiką), o kitos – ne.
PDF/A ir PDF/UA taiko papildomus puslapių medžio apribojimus, palyginti su bazine specifikacija, tačiau nei vienas iš jų nesušvelnina /Pages reikalavimo. Tikrintuvas, vertinantis atitiktį ISO 19005 arba ISO 14289 standartams, aptiks trūkstamą Pages žodyną kaip bazinės specifikacijos pažeidimą dar prieš pradėdamas tikrinti specifines profilio taisykles.