Teknisk artikel

PDF uden en Pages-ordbog: Konsekvenser for parsing

PDF Catalog-ordbogen har præcis én påkrævet navigationsnøgle: /Pages. Denne nøgle skal pege på et indirekte objekt af typen /Pages, som til gengæld indeholder /Kids-arrayet og det samlede /Count af sider. Hvis man fjerner denne pointer, kan ingen konform læser finde en eneste side i filen. ISO 32000-1 §7.7.2 er entydig på dette punkt: Catalog skal have en /Pages-indgang, og det refererede objekt skal have typen /Pages. Filer, der overtræder dette krav, er ikke blot ukorrekte; de er strukturelt ødelagte på en måde, som de fleste parsere håndterer dårligt

Hvad specifikationen faktisk siger

En minimal konform PDF har mindst tre objekter. Objekt 1 er Catalog, objekt 2 er Pages-roden, og objekt 3 og frem er individuelle Page-ordbøger. Catalog peger på Pages-roden; Pages-roden oplister sine børn i /Kids; hver Page har en /Parent-tilbagereference. Hele kæden er tovejs i sit design, så en parser kan starte fra begge ender og gennemløbe til enhver side på O(log n)-tid for balancerede træer

% 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-træet kan være indlejret. Et dokument med tusindvis af sider grupperer typisk sider i mellemliggende knudeobjekter, der også bærer typen /Pages, hver med sin egen /Kids og et /Count, der afspejler undertræet under den. Rodknudens /Count er altid lig med det samlede sideantal. Dette antal er det, som fremvisere viser i sidetal-feltet, før de har parset en eneste side, fordi det er meget billigere at læse et enkelt heltal fra objekt 2 end at gennemgå hele træet

Hvordan en fil uden Pages ser ud

Filer, der mangler Pages-ordbogen, stammer typisk fra PDF-generatorer, der skriver sideobjekter direkte uden at samle dem i et træ, eller fra korruption, der fjerner rodknuden, mens de yderste Page-objekter efterlades intakte. Catalog i en sådan fil mangler enten /Pages-nøglen helt eller indeholder en reference til et objekt, der ikke længere findes i krydsreferencetabellen

% 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, der følger specifikationen, vil læse Catalog, forsøge at løse /Pages, finde intet (eller en død reference) og enten rejse en fejl eller rapportere nul sider. Hvad den ikke må gøre, er at fortsætte, som om filen havde nul sider, og i stilhed lykkes; dette producerer et tomt output, der ser korrekt ud for automatiserede værktøjer, men forkert for ethvert menneske, der åbner det

Hvorfor parsere crasher

De fleste PDF-parsere tildeler deres interne sidetabel på indlæsningstidspunktet baseret på /Count-værdien fra Pages-roden. Når denne rod mangler, læser parseren enten nul, allokerer intet og dereferencerer derefter en null pointer første gang kode beder om side 1, eller den læser affald og allokerer en vildt forkert buffer. Ingen af udfaldene er særligt elegante. Den adgangsfejl (access violation) ved 0x008E5D78, der vises i crash-logs fra behandling af en sådan fil, er præcis dette: en null pointer-dereference inde i sidens adgangssti, udløst af manglen på den struktur, som parseren antog altid ville være der

Den underliggende designantagelse er rimelig. Langt de fleste PDF'er i eksistens har en Pages-ordbog. Parsere, der springer eksistenskontrollen over for at spare nogle få instruktioner, er ikke uforsigtige; de optimerer til det mest almindelige tilfælde. De filer, der straffer denne optimering, er sjældne nok til, at produktionskode måske aldrig støder på en, før den pludselig gør, hvorefter crashet er både reproducerbart og forbløffende, hvis ingeniøren ikke har læst §7.7.2

Genoprettelse uden et Pages-træ

Hvis en parser skal håndtere disse filer i stedet for at afvise dem, følger genoprettelsen en forudsigelig sti: scan hvert indirekte objekt i krydsreferencetabellen, saml dem med /Type /Page, og sorter dem efter objektnummer. Rækkefølge af objektnumre garanteres ikke at stemme overens med læserækkefølgen i specifikationen, men i praksis har generatorer, der udelader Pages-træet, en tendens til at udsende sider sekventielt, så rækkefølgen efter objektnummer er oftere korrekt end forkert

Selve kontrollen er billig. Før du gennemgår Catalogs /Pages-pointer, skal du bekræfte, at pointeren eksisterer, at den løses til et reelt objekt, og at det løste objekts /Type er lig med /Pages. Hvis en af disse tre betingelser fejler, falder du tilbage til den lineære scanning. Scanningen er langsommere end en trægennemgang for store dokumenter, fordi den læser hver objektheader i stedet for at følge en balanceret sti, men det virker, og for en fil, der i forvejen er deformeret, overgår korrekthed hastighed

Én særlig situation (edge case), som den lineære scanning ikke løser automatisk, er siders rækkefølge. Uden et /Kids-array til at definere sekvensen, er den "korrekte" rækkefølge udefineret af specifikationen. Rækkefølge efter objektnummer er den pragmatiske standard; hvis filen er vigtig nok til at blive behandlet omhyggeligt, er det ekstra arbejde værd at kontrollere, om Page-objekterne bærer eksplicitte /StructParents- eller annotationsreferencer, der antyder en læserækkefølge

Konsekvenser for PDF-generatorer

For enhver, der skriver en PDF-generator snarere end en parser, er lektien snæver: Udsend altid Pages-roden, før filen lukkes. Et Catalog uden en /Pages-indgang er ikke en gyldig PDF under nogen revision af specifikationen. Generatorer, der bygger sideobjekter løbende og samler træet ved afslutningen (den tilgang de fleste streamingskribenter anvender), fungerer fint, så længe afslutningen faktisk køres. Den almindelige fejltilstand er en undtagelse (exception) eller tidlig returnering, der afbryder skrivningen, før traileren er fuldført, hvilket efterlader en fil, der åbner i nogle fremvisere (som har genoprettelsesheuristikker) og fejler i andre (som ikke har)

PDF/A og PDF/UA pålægger side-træet yderligere begrænsninger ud over, hvad basisspecifikationen kræver, men ingen af dem slækker på /Pages-kravet. En validator, der kontrollerer konformitet over for ISO 19005 eller ISO 14289, vil fange en manglende Pages-ordbog som en overtrædelse af basisspecifikationen, før den overhovedet når de profilspecifikke regler