Teknisk artikkel

PDF Uten en Pages-ordbok: Implikasjoner for Parsing

PDF-dokumentkatalogen (Catalog dictionary) har nøyaktig én obligatorisk navigasjonsnøkkel (navigation key): /Pages. Den nøkkelen må peke til et indirekte objekt av typen /Pages, som igjen inneholder /Kids-arrayen og det totale /Count (antall) sider. Ta den pekeren (pointer) bort, og ingen samsvarende (conforming) leser kan lokalisere en eneste side i filen. ISO 32000-1 §7.7.2 er utvetydig på dette punktet: Katalogen (Catalog) skal ha en /Pages-oppføring, og det refererte objektet skal ha typen /Pages. Filer som bryter dette kravet, er ikke bare ikke-samsvarende; de er strukturelt ødelagte på en måte som de fleste parsere håndterer dårlig

Hva spesifikasjonen faktisk sier

En minimalt samsvarende PDF har minst tre objekter. Objekt 1 er katalogen, objekt 2 er Pages-roten (the Pages root), og objekt 3 og utover er individuelle Page-ordbøker (Page dictionaries). Katalogen peker til Pages-roten; Pages-roten lister opp barna (children) i /Kids; hver side bærer en /Parent-tilbakereferanse. Hele kjeden (chain) er toveis i sitt design, så en parser kan starte fra hver ende og traversere til hvilken som helst side på O(log n) tid for balanserte trær

% 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

Pages-treet kan være nøstet (nested). Et dokument med tusenvis av sider grupperer typisk sider inn i mellom-nodeobjekter (intermediate node objects) som også bærer typen /Pages, hver med sin egen /Kids og et /Count som reflekterer undertreet (subtree) under den. Rotnodens (The root node's) /Count er alltid lik det totale sideantallet. Dette antallet er det seere (viewers) viser i sidenummerfeltet før de har parset en eneste side, fordi det å lese ett heltall fra objekt 2 er mye billigere enn å gå gjennom hele treet

Hvordan en Pages-løs fil ser ut

Filer som mangler Pages-ordboken, stammer typisk fra PDF-generatorer som skriver side-objekter direkte uten å sette dem sammen til et tre, eller fra korrupsjon (corruption) som fjerner rotnoden (the root node) samtidig som blad-Page-objektene (leaf Page objects) forblir inntakte. Katalogen i en slik fil mangler enten /Pages-nøkkelen helt, eller så har den en referanse til et objekt som ikke lenger finnes i kryssreferansetabellen

% 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

En parser som følger spesifikasjonen vil lese katalogen, forsøke å løse opp (resolve) /Pages, ikke finne noe (eller en død referanse), og enten kaste en feil eller rapportere null sider. Det den ikke må gjøre, er å fortsette som om filen hadde null sider og lykkes i stillhet; dette produserer et blankt resultat (blank output) som ser riktig ut for automatiserte verktøy og feil ut for alle mennesker som åpner det

Hvorfor parsere krasjer

De fleste PDF-parsere allokerer sin interne sidetabell på innlastingstidspunktet (load time) basert på /Count-verdien fra Pages-roten. Når denne roten mangler, leser parseren enten null, allokerer ingenting, og derefererer deretter en null-peker den første gangen kode spør etter side 1, eller så leser den søppel (garbage) og allokerer en vilt ukorrekt buffer. Ingen av utfallene er elegante. Tilgangsbruddet (access violation) på 0x008E5D78 som dukker opp i krasjlogger fra behandling av en slik fil er nøyaktig dette: en null-peker-dereferering (null-pointer dereference) inni side-tilgangsveien (page-access path), utløst av fraværet av strukturen som parseren antok alltid ville være der

Den underliggende designantakelsen er rimelig. Det store flertallet av PDF-er i eksistens har en Pages-ordbok. Parsere som hopper over eksistens-sjekken for å spare noen instruksjoner, er ikke dumdristige; de optimaliserer for det vanligste tilfellet (the common case). Filene som straffer denne optimaliseringen er sjeldne nok til at produksjonskode kanskje aldri støter på en før den plutselig gjør det, og da er krasjen både reproduserbar og forvirrende om ingeniøren ikke har lest §7.7.2

Gjenoppretting uten et Pages-tre

Hvis en parser må håndtere disse filene i stedet for å avvise dem, følger gjenoppretting en forutsigbar vei: skann hvert indirekte objekt i kryssreferansetabellen, samle inn de med /Type /Page, og sorter dem etter objektnummer. Objektnummer-rekkefølge er ikke garantert å samsvare med leserekkefølge i spesifikasjonen, men i praksis pleier generatorer som utelater Pages-treet å produsere sider sekvensielt, så objektnummer-rekkefølge er riktig oftere enn det er feil

Selve sjekken (check) er billig. Før man følger Katalogens /Pages-peker, bekreft at pekeren finnes, at den løses opp (resolves) til et ekte objekt, og at det oppløste objektets /Type er lik /Pages. Hvis noen av de tre betingelsene feiler, falle (fall through) til det lineære skannet (linear scan). Skanningen er tregere enn tretraversering for store dokumenter, fordi den leser hver objekt-header snarere enn å følge en balansert sti, men det fungerer, og for en fil som allerede er misdannet (malformed), trumfer korrekthet hastighet

Ett unntakstilfelle (edge case) som lineært skann ikke løser automatisk: siderekkefølge. Uten en /Kids-array for å definere sekvensen, er den "riktige" rekkefølgen udefinert av spesifikasjonen. Objektnummer-rekkefølge er den pragmatiske standarden; hvis filen er viktig nok til å behandles med omhu, er det verdt ekstra-arbeidet å sjekke om Page-objektene (Page objects) har en eksplisitt /StructParents eller merknads-referanser (annotation references) som impliserer en lesesekvens

Implikasjoner for PDF-generatorer

For alle som skriver en PDF-generator fremfor en parser, er lærdommen smal: skriv alltid ut (emit) Pages-roten før filen lukkes. Katalogen uten en /Pages-oppføring er ikke en gyldig PDF under noen revisjon av spesifikasjonen. Generatorer som bygger side-objekter løpende (on the fly) og setter sammen treet ved ferdigstillelse (the approach most streaming writers use), klarer seg fint, så lenge ferdigstillelsen (finalization) faktisk kjøres. Den vanlige feilmodusen (failure mode) er et unntak (exception) eller tidlig retur (early return) som avbryter skrivingen før traileren er ferdig, noe som etterlater en fil som åpnes i noen seere (som har gjenopprettings-heuristikk (recovery heuristics)) og feiler i andre (som ikke har det)

PDF/A og PDF/UA pålegger ytterligere begrensninger på sidetreet utover det basis-spesifikasjonen (base spec) krever, men ingen av dem lemper på /Pages-kravet. En validator som sjekker samsvar (conformance) med ISO 19005 eller ISO 14289, vil fange opp en manglende Pages-ordbok som et brudd på basis-spesifikasjonen før den i det hele tatt når de profil-spesifikke (profile-specific) reglene