Objekt nummer 1 er ikke side 1. Dette ene faktumet forårsaker flere feil i PDF-behandlingskode enn noe annet aspekt av formatet, og å forstå hvorfor krever at man ser forbi det et visningsprogram viser deg og inn i objektgrafen visningsprogrammet faktisk leser
En PDF-fil er en samling av nummererte indirekte objekter. Hvert objekt bærer et objektnummer og et generasjonsnummer, og andre objekter peker på det med en referanse skrevet som N G R: 3 0 R betyr den gjeldende versjonen av objekt 3. Sider er blant disse objektene, men visningsrekkefølgen deres har ingenting å gjøre med hvor de befinner seg i filen eller hvilke tall de bærer. Visningsrekkefølgen bestemmes utelukkende av /Pages-treet, en lenket struktur med roten i dokumentkatalogen. Hvis du ignorerer treet og skanner objekter numerisk, vil du sette sammen sidene i feil rekkefølge for en betydelig andel av filer i den virkelige verden
Sidetreet: det som faktisk bestemmer rekkefølgen
Hver PDF begynner med en dokumentkatalog (ISO 32000-2 §7.7.2). Katalogen inneholder en /Pages-oppføring som peker på rotnoden til sidetreet. Den rotnoden er en ordbok med /Type /Pages, en /Kids-matrise med indirekte referanser, og en /Count som gir det totale antallet bladsider under den. Visningsrekkefølgen er en dybde-først, venstre-til-høyre traversering av det treet, punktum
En minimal tresiders fil gjør dette 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-matrisen leser [20 0 R 4 0 R 9 0 R], så objekt 20 er side 1, objekt 4 er side 2, og objekt 9 er side 3. Objektnummerering er irrelevant. All kode som itererer objekter i numerisk rekkefølge og samler de med /Type /Page vil produsere feil sekvens på denne filen
Hvorfor produserer generatorer ikke-sekvensielle oppsett? Flere grunner. Et bibliotek som forhåndstildeler objektnumre for alle sider før innholdet skrives, vil nummerere dem i opprettelsesrekkefølge, og deretter skrive faktiske byter i den rekkefølgen som passer for serialisereren. Et sammenslåingsverktøy (merge tool) som syr sammen dokumenter, nummererer objekter på nytt fra hvert kildedokument for å unngå kollisjoner; de omnummererte sideobjektene ender opp spredt utover den kombinerte objekttabellen mens den nye rots-/Kids-matrisen holder den riktige visningsrekkefølgen. Inkrementelle oppdateringer legger til nye objekter på slutten av filen med ferske numre, så en side lagt til som en revisjon lever nær slutten av bytestrømmen, selv om den hører hjemme i posisjon 1 av visningsrekkefølgen
Flate trær og nestede undertrær
Spesifikasjonen tillater to former for sidetreet. Enkle generatorer produserer en flat struktur: én rot-/Pages-node der /Kids-matrisen ikke inneholder noe annet enn /Page-bladobjekter. Det er enkelt å traversere: ett nivå dypt, ett pass
Store dokumenter bruker rutinemessig et balansert tre i stedet. Rot-/Pages-nodens /Kids-matrise inneholder mellomliggende /Pages-noder, som hver igjen inneholder en egen /Kids-matrise. /Count på hver mellomnode rapporterer det totale antallet bladsider i sitt undertre, så et visningsprogram kan hoppe over hele undertrær når det hopper til en side via indeks, uten å tolke hvert enkelt objekt. Et dokument på 1 000 sider strukturert som et balansert tre med 10 sider per bladnode, kan finne side 750 ved binærsøk gjennom tre eller fire ordbokoppslag, i stedet for å skanne 750 /Kids-oppføringer
Konsekvensen for prosesseringskode: du kan ikke anta at det første nivået av /Kids inneholder /Page-objekter. Hvert barn må sjekkes. Hvis dets /Type er /Pages, rekursivt gå inn i det. Hvis dets /Type er /Page, er det et blad. Å stoppe på det første nivået forkaster stille hele undertrær på ethvert dokument der generatoren valgte å neste. Hvorfor skrivere i det hele tatt velger dype trær, hva utflatingsverktøy gir opp, og hvordan /Count-korrupsjon utspiller seg i praksis, dekkes i vår ledsagerartikkel om sidetreform, utbredelse og /Count-integritet
Arvede sideattributter
Sidetreet bærer også en ressursdelingsmekanisme. Visse sideattributter: /MediaBox, /CropBox, /Resources og /Rotate er arvelige (ISO 32000-2 §7.7.3.4). Hvis en /Page-ordbok utelater en av dem, går en leser oppover /Parent-kjeden til den finner attributtet eller når roten. Å plassere en delt skrifttypeordbok i rot-/Pages-noden i stedet for å kopiere den inn i hver bladside kan redusere filstørrelsen merkbart for dokumenter som bruker de samme skrifttypesnittene hele veien
Arveregelen skaper en finurlighet for kode som leser sideegenskaper. Å lese /MediaBox direkte fra et /Page-objekt og behandle en manglende nøkkel som en feil er galt; nøkkelen kan rett og slett være arvet. Kode som løser opp sidegeometri på riktig måte, må følge foreldrekjeden. Den trenger også en syklusvakt (cycle guard): en korrumpert fil kan ha en /Parent-referanse som peker tilbake på en node som allerede er besøkt, noe som ville gå i uendelig løkke uten en sjekk av besøkte objekter
Xref-tabellen og kryssreferansestrømmer
Oppslag av indirekte objekter går gjennom kryssreferansetabellen (eller etterfølgeren, kryssreferansestrømmen introdusert i PDF 1.5). Xref tilordner hvert objektnummer til en byteforskyvning inne i filen. En samsvarende leser bruker xref for å hoppe direkte til ethvert objekt; den skanner ikke filen sekvensielt. Denne tilfeldige tilgangsdesignen er det som gjør rask sidehopping mulig: visningsprogrammet leser katalogen, løser opp /Pages-referansen via xref, leser rot-/Pages-noden, løser opp en /Kids-oppføring, og så videre, og berører bare de objektene det trenger
Inkrementelle oppdateringer legger til en ny xref-seksjon på slutten av filen med en trailer som lenker tilbake til den forrige. Et objekt oppdatert i en revisjon får en ny oppføring i den tilføyde xref-seksjonen; de opprinnelige bytene blir værende, men erstattes. Dette er hvordan digitalt signerte PDF-er forblir verifiserbare selv etter at annoteringer eller skjemautfyllingsrevisjoner er lagt til: det signerte byteområdet berøres aldri, og det nye innholdet lever i den tilføyde seksjonen. Sidetreet kan også oppdateres, så sidetilføyelser eller -slettinger i en revisjon produserer en ny /Pages-rot med en revidert /Kids-matrise, mens det gamle rotobjektet fortsatt opptar sin opprinnelige posisjon i filen. Lineariserte (nett-optimaliserte) filer legger til en bytelayout-vri: objektene for side 1 flyttes fysisk til fronten av filen, slik at et visningsprogram kan vise den første siden mens resten fortsatt lastes ned, men sidetreet forblir den eneste autoriteten på rekkefølge — bare forskyvningene registrert i xref endres
Hva går galt uten tretraversering
Feilmodusen for objekt-skanning-tilnærminger er stille. Utdatadokumentet ser plausibelt ut: det har riktig antall sider og hver side inneholder gjenkjennelig innhold. Rekkefølgen er bare feil, og feil på en måte som avhenger av generatoren, antall revisjoner og om noen sider ble slått sammen fra eksterne kilder. Et testkorpus av filer produsert av et enkelt verktøy kan passere fullstendig; filer fra et annet verktøy eller en sammenslåingsarbeidsflyt vil mislykkes. Den inkonsekvensen er grunnen til at heuristiske fikser aldri holder. For en gjennomgang av nøyaktig denne feilen i et ekte kundedokument — symptom, feildiagnose og traverseringsfiksen — se vår casestudie for feilsøking av siderekkefølge
Filer med inkrementelle oppdateringer er spesielt utsatt for dette fordi sider lagt til eller omorganisert i senere revisjoner bærer høye objektnumre, mens visningsrekkefølgen styres av den oppdaterte /Kids-matrisen. En skanning som prosesserer objekter i numerisk rekkefølge vil plassere disse høyt nummererte sidene på slutten uansett hvor treet sier at de hører hjemme
Fiksen er ikke komplisert. Start ved katalogen, løs opp /Pages-referansen, gå gjennom /Kids-matrisen rekursivt, og send ut bladene i den rekkefølgen du støter på dem. Det er visningsrekkefølgen per definisjon, uavhengig av objektnumre, byteforskyvninger eller filstruktur. De fleste modne PDF-biblioteker eksponerer en sideteller og en indeksert sidetilgang som allerede gjør dette riktig; risikoen ligger i kode som omgår bibliotekets sidemodell og berører objektlaget direkte
Ett strukturelt avvik verdt å håndtere eksplisitt: /Count-verdien på en mellomliggende /Pages-node kan være feil i misdannede filer. Å stole på /Count for grensjekking for deretter å stoppe før en full traversering vil i stillhet utelate sider når tellingen er underdrevet. Å bruke /Count bare som et ytelsestips for kapasitetsforhåndstildeling eller binærsøk, og utlede den faktiske tellingen fra traversering, er det tryggere mønsteret for viktige dokumenter