PDF-katalogens ordlista (Catalog dictionary) har exakt en obligatorisk navigeringsnyckel: /Pages. Den nyckeln måste peka på ett indirekt objekt av typen /Pages, som i sin tur håller /Kids-arrayen och det totala antalet sidor i /Count. Ta bort den pekaren och ingen överensstämmande läsare kan lokalisera en enda sida i filen. ISO 32000-1 §7.7.2 är otvetydig på denna punkt: Katalogen ska ha en /Pages-post, och det refererade objektet ska vara av typen /Pages. Filer som bryter mot detta krav är inte bara icke-överensstämmande; de är strukturellt trasiga på ett sätt som de flesta tolkare (parsers) hanterar dåligt
Vad specifikationen faktiskt säger
En minimal överensstämmande PDF har minst tre objekt. Objekt 1 är katalogen, objekt 2 är Pages-roten, och objekt 3 och framåt är enskilda sidordlistor (Page dictionaries). Katalogen pekar på Pages-roten; Pages-roten listar sina barn i /Kids; varje sida bär en /Parent-tillbakareferens. Hela kedjan är dubbelriktad avsiktligt (by design), så en tolkare kan börja från endera änden och färdas till valfri sida på O(log n) tid för balanserade träd
% Minimal överensstämmande struktur (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ädet kan vara nästlat. Ett dokument med tusentals sidor grupperar typiskt sidor i mellanliggande nodobjekt som också har typen /Pages, vart och ett med sina egna /Kids och en /Count som reflekterar underträdet inunder det. Rotnodens /Count är alltid lika med det totala antalet sidor. Det antalet är vad visare visar i sidnummerfältet innan de har tolkat en enda sida, eftersom det är mycket billigare att läsa ett heltal från objekt 2 än att vandra genom hela trädet
Hur en fil utan Pages ser ut
Filer som saknar Pages-ordlistan härstammar typiskt från PDF-generatorer som skriver sidobjekt direkt utan att montera ihop dem till ett träd, eller från korruption som tar bort rotnoden men lämnar kvar lövsidobjekten orörda. Katalogen i en sådan fil saknar antingen /Pages-nyckeln helt, eller håller en referens till ett objekt som inte längre existerar i korsreferenstabellen
% Icke-överensstämmande: Katalog utan /Pages-referens
1 0 obj
<< /Type /Catalog >>
endobj
% Sidobjekt existerar men är onåbara från katalogen
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 tolkare som följer specifikationen kommer att läsa katalogen, försöka lösa /Pages, inte hitta något (eller en död referens), och antingen kasta ett fel eller rapportera noll sidor. Vad den inte får göra är att fortsätta som om filen hade noll sidor och i tysthet lyckas; det producerar en blank utmatning som ser korrekt ut för automatiserade verktyg och fel för varje människa som öppnar den
Varför tolkare kraschar
De flesta PDF-tolkare allokerar sin interna sidtabell vid laddningstid baserat på /Count-värdet från Pages-roten. När den roten saknas, läser tolkaren antingen in noll, allokerar ingenting, och derefererar sedan en null-pekare första gången någon kod ber om sida 1, eller så läser den in skräp och allokerar en vilt inkorrekt buffert. Inget av utfallen är snyggt. Den åtkomstöverträdelse (access violation) vid 0x008E5D78 som dyker upp i kraschloggar från bearbetning av en sådan fil är exakt detta: en null-pekar-dereferens inuti sökvägen för sidåtkomst, utlöst av frånvaron av den struktur som tolkaren antog alltid skulle finnas där
Det bakomliggande designantagandet är rimligt. Den stora majoriteten av alla PDF-filer som existerar har en Pages-ordlista. Tolkare som hoppar över existenskontrollen för att spara några instruktioner är inte vårdslösa; de optimerar för det vanligaste fallet. De filer som straffar den optimeringen är så sällsynta att produktionskod kanske aldrig stöter på en förrän den väl gör det, varpå kraschen är både reproducerbar och förbryllande om ingenjören inte har läst §7.7.2
Återhämtning utan ett Pages-träd
Om en tolkare måste hantera dessa filer istället för att avvisa dem, följer återhämtningen en förutsägbar väg: skanna varje indirekt objekt i korsreferenstabellen, samla in de med /Type /Page, och sortera dem efter objektnummer. Objektnummerordning garanteras inte matcha läsordningen i specifikationen, men i praktiken tenderar generatorer som utelämnar Pages-trädet att mata ut sidor sekventiellt, så objektnummerordning är korrekt oftare än inte
Själva kontrollen är billig. Innan du vandrar längs katalogens /Pages-pekare, bekräfta att pekaren existerar, att den kan lösas till ett riktigt objekt, och att det lösta objektets /Type är lika med /Pages. Om något av dessa tre villkor misslyckas, fall tillbaka (fall through) till den linjära skanningen. Skanningen är långsammare än trädvandring för stora dokument, eftersom den läser varje objektheader i stället för att följa en balanserad väg, men det fungerar, och för en fil som redan är missbildad rankas korrekthet högre än hastighet
Ett gränsfall som linjär skanning inte löser automatiskt: sidordning. Utan en /Kids-array för att definiera sekvens är den "korrekta" ordningen odefinierad av specifikationen. Objektnummerordning är den pragmatiska standarden; om filen är tillräckligt viktig för att bearbetas noggrant, är det värt det extra arbetet att kontrollera huruvida sidobjekten bär på en uttrycklig /StructParents eller referenser till kommentarer som antyder en lässekvens
Implikationer för PDF-generatorer
För alla som skriver en PDF-generator i stället för en tolkare är läxan smal: mata alltid ut Pages-roten innan du stänger filen. Katalogen utan en /Pages-post är inte en giltig PDF under någon revision av specifikationen. Generatorer som bygger sidobjekt i farten och monterar ihop trädet vid slutförandet (tillvägagångssättet som de flesta strömmande skrivare använder) är okej så länge slutförandet faktiskt körs. Det vanligaste felläget (failure mode) är ett undantag (exception) eller en tidig retur som avbryter skrivningen innan trailern är komplett, vilket lämnar efter sig en fil som öppnas i vissa visare (som har återhämtningsheuristik) och misslyckas i andra (som inte har det)
PDF/A och PDF/UA ålägger ytterligare begränsningar på sidträdet utöver vad grundspecifikationen kräver, men ingen av dem lättar på /Pages-kravet. En validator som kontrollerar överensstämmelse med ISO 19005 eller ISO 14289 kommer att fånga en saknad Pages-ordlista som ett brott mot grundspecifikationen innan den ens når de profilspecifika reglerna