Objekt nummer 1 är inte sida 1. Det enda faktumet sätter krokben för mer PDF-behandlingskod än någon annan aspekt av formatet, och för att förstå varför krävs det att man tittar förbi vad en visare visar dig och in i den objektgraf som visaren faktiskt läser
En PDF-fil är en samling numrerade indirekta objekt. Varje objekt bär ett objektnummer och ett generationsnummer, och andra objekt pekar på det med en referens skriven som N G R: 3 0 R betyder den nuvarande versionen av objekt 3. Sidor finns bland dessa objekt, men deras visningssekvens har ingenting att göra med var de sitter i filen eller vilka nummer de bär. Visningsordningen bestäms helt av /Pages-trädet, en länkad struktur rotad i dokumentkatalogen. Om du ignorerar trädet och skannar objekt numeriskt kommer du att sätta ihop sidor i fel ordning för en betydande del av verkliga filer
Sidträdet: vad som faktiskt sätter ordningen
Varje PDF börjar med en dokumentkatalog (ISO 32000-2 §7.7.2). Katalogen har en /Pages-post som pekar på rotnoden för sidträdet. Den rotnoden är en ordbok med /Type /Pages, en /Kids-array av indirekta referenser och en /Count som ger det totala antalet lövsidor under den. Visningsordningen är trädets djup-först (depth-first) vänster-till-höger-traversering, punkt slut
En minimal tresidig fil gör detta konkret:
%PDF-1.7
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [20 0 R 4 0 R 9 0 R] /Count 3 >>
endobj
% Object 4 is stored third in the file but is page 2 in display order
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 5 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
% Object 9 is stored fourth but is page 3
9 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 10 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
% Object 20 is stored last but is page 1; Kids[0] decides, not object number
20 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 21 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
/Kids-arrayen lyder [20 0 R 4 0 R 9 0 R], så objekt 20 är sida 1, objekt 4 är sida 2, och objekt 9 är sida 3. Objektnumrering är irrelevant. Varje kod som itererar objekt i numerisk ordning och samlar de med /Type /Page kommer att producera fel sekvens för denna fil
Varför producerar generatorer icke-sekventiella layouter? Av flera skäl. Ett bibliotek som förallokerar objektnummer för alla sidor innan de skriver deras innehåll kommer att numrera dem i skapandeordning, och sedan skriva de faktiska byten i vilken ordning som helst som passar serialiseraren. Ett sammanslagningsverktyg som syr ihop dokument omnumrerar objekt från varje källdokument för att undvika kollisioner; de omnumrerade sidobjekten hamnar utspridda över den kombinerade objekttabellen medan den nya /Kids-rotarrayen håller den korrekta visningssekvensen. Inkrementella uppdateringar lägger till (append) nya objekt i slutet av filen med nya nummer, så en sida som läggs till som en revision lever nära slutet av byteströmmen även om den hör hemma på position 1 i visningsordningen
Platta träd och nästlade underträd
Specifikationen tillåter två former för sidträdet. Enkla generatorer producerar en platt struktur: en /Pages-rotnod vars /Kids-array inte innehåller något annat än /Page-lövobjekt. Det är lätt att traversera: en nivå djupt, ett pass
Stora dokument använder rutinmässigt ett balanserat träd i stället. Den rotade /Pages-nodens /Kids-array innehåller mellanliggande /Pages-noder, som var och en i sin tur rymmer en egen /Kids-array. /Count på varje mellanliggande nod rapporterar det totala antalet lövsidor i dess underträd, så en visare kan hoppa över hela underträd när den hoppar till en sida via index utan att tolka varje objekt. Ett 1 000-sidigt dokument strukturerat som ett balanserat träd med 10 sidor per lövnod kan lokalisera sida 750 genom binär sökning via tre eller fyra ordboksuppslagningar i stället för att skanna 750 /Kids-poster
Konsekvensen för behandlingskoden: du kan inte anta att den första nivån av /Kids innehåller /Page-objekt. Varje barn (child) måste kontrolleras. Om dess /Type är /Pages, rekursivt gå in i den. Om dess /Type är /Page, är det ett löv. Att stanna vid den första nivån tappar tyst hela underträd i varje dokument där generatorn valde att nästla. Varför författare väljer djupa träd i första hand, vad plattande (flattening) verktyg ger upp, och hur /Count-korruption spelar ut i praktiken täcks i vår tillhörande artikel om sidträdsform, utspridning (fan-out) och /Count-integritet
Ärvda sidegenskaper
Sidträdet bär också på en mekanism för resursdelning. Vissa sidegenskaper: /MediaBox, /CropBox, /Resources och /Rotate är ärvbara (ISO 32000-2 §7.7.3.4). Om en /Page-ordbok utelämnar en av dem går en läsare uppåt i /Parent-kedjan tills den hittar egenskapen eller når roten. Att placera en delad typsnittsordbok i rotens /Pages-nod snarare än att kopiera den till varje lövsida kan minska filstorleken märkbart för dokument som använder samma typsnitt genomgående
Arvsregeln skapar en finess för kod som läser sidegenskaper. Att läsa /MediaBox direkt från ett /Page-objekt och behandla en saknad nyckel som ett fel är fel; nyckeln kan helt enkelt vara ärvd. Kod som korrekt löser upp sidgeometri måste följa föräldrakedjan. Den behöver också ett cykelskydd (cycle guard): en korrumperad fil kan ha en /Parent-referens som pekar tillbaka till en nod som redan besökts, vilket skulle loopa för evigt utan en kontroll av besökta objekt
Xref-tabellen och korsreferensströmmar
Indirekt objektuppslagning går genom korsreferenstabellen (eller dess efterträdare, korsreferensströmmen introducerad i PDF 1.5). Xref:en mappar varje objektnummer till en byte-offset inuti filen. En konform läsare använder xref:en för att hoppa direkt till valfritt objekt; den skannar inte filen sekventiellt. Den slumpmässiga (random-access) designen är vad som gör snabba sidhopp möjliga: visaren läser katalogen, löser upp /Pages-referensen via xref:en, läser rotens /Pages-nod, löser upp en /Kids-post, och så vidare, och rör bara vid de objekt den behöver
Inkrementella uppdateringar lägger till en ny xref-sektion i slutet av filen med en trailer som kedjar tillbaka till den föregående. Ett objekt som uppdaterats i en revision får en ny post i den tillagda xref-sektionen; de ursprungliga byten stannar på plats men ersätts. Detta är hur digitalt signerade PDF-filer förblir verifierbara även efter att anteckningar eller blankettifyllnads-revisioner (form-fill) har lagts till: det signerade byte-intervallet berörs aldrig, och det nya innehållet lever i den tillagda sektionen. Sidträdet kan också uppdateras, så sidtillägg eller raderingar i en revision producerar en ny /Pages-rot med en reviderad /Kids-array, medan det gamla rotobjektet fortfarande intar sin ursprungliga position i filen. Linjäriserade (webboptimerade) filer lägger till en byte-layout-tvist: objekten för sida 1 flyttas fysiskt till framsidan av filen så att en visare kan visa den första sidan medan resten fortfarande laddas ner, men sidträdet förblir den enda auktoriteten för ordning — endast offseterna som registrerats i xref:en ändras
Vad som går fel utan trädtraversering
Felläget för objektskanningsmetoder är tyst. Utdata-dokumentet ser rimligt ut: det har rätt antal sidor och varje sida innehåller igenkännbart innehåll. Ordningen är bara fel, och fel på ett sätt som beror på generatorn, antalet revisioner och huruvida några sidor sammanfogades (merged) från externa källor. En testkorpus av filer producerade av ett enda verktyg kan passera helt; filer från ett annat verktyg eller ett sammanslagningsarbetsflöde kommer att misslyckas. Den inkonsekvensen är anledningen till att heuristiska lösningar aldrig håller. För en genomgång av exakt detta fel i ett verkligt kunddokument — symptom, felaktig diagnos och traverseringsfixen — se vår fallstudie av felsökning av sidordning
Filer med inkrementell uppdatering är särskilt benägna för detta eftersom sidor som läggs till eller omarrangeras i senare revisioner bär höga objektnummer medan visningsordningen styrs av den uppdaterade /Kids-arrayen. En skanning som behandlar objekt i numerisk ordning kommer att placera de sent numrerade sidorna i slutet oavsett var trädet säger att de hör hemma
Fixen är inte komplicerad. Börja vid katalogen, lös upp /Pages-referensen, gå igenom /Kids-arrayen rekursivt och mata ut löv i den ordning du stöter på dem. Det är visningsordningen per definition, oavsett objektnummer, byte-offsets eller filstruktur. De flesta mogna PDF-bibliotek exponerar ett sidantal och en indexerad sidåtkomst som redan gör detta korrekt; risken ligger i kod som kringgår bibliotekets sidmodell och rör vid objektlagret direkt
En strukturell anomali värd att hantera uttryckligen: /Count-värdet på en mellanliggande /Pages-nod kan vara fel i illa formaterade (malformed) filer. Att lita på /Count för gränskontroll och sedan stanna före en fullständig traversering kommer tyst att utelämna sidor när antalet är underskattat. Att använda /Count enbart som ett prestandatips för kapacitets-förallokering eller binär sökning, och härleda det faktiska antalet från traverseringen är det säkrare mönstret för viktiga dokument