Teknisk artikel

PDF Siderækkefølge: Hvordan sidetræet styrer sidesekvensen

Objekt nummer 1 er ikke side 1. Det ene faktum spænder ben for (trips up) mere PDF-behandlingskode end noget andet aspekt af formatet, og at forstå hvorfor, kræver at man ser forbi, hvad en fremviser (viewer) viser dig, og ind i objektgrafen, som fremviseren rent faktisk læser

En PDF-fil er en samling af nummererede indirekte objekter. Hvert objekt bærer et objektnummer og et generationsnummer, og andre objekter peger på det med en reference skrevet som N G R: 3 0 R betyder den aktuelle version af objekt 3. Sider er blandt disse objekter, men deres visningsrækkefølge har intet at gøre med, hvor de sidder i filen, eller hvilke numre de bærer. Visningsrækkefølgen bestemmes udelukkende af /Pages-træet, en sammenkædet struktur rodfæstet i dokumentkataloget. Hvis du ignorerer træet og scanner objekter numerisk, vil du samle sider i den forkerte rækkefølge for en betydelig del af virkelige filer

Sidetræet: hvad der faktisk sætter rækkefølgen

Enhver PDF begynder med et dokumentkatalog (ISO 32000-2 §7.7.2). Kataloget indeholder en /Pages-indgang, der peger på rodknuden (root node) af sidetræet. Den rodknude er en ordbog (dictionary) med /Type /Pages, et /Kids-array af indirekte referencer, og en /Count, der angiver det samlede blad-side-antal (leaf-page count) under sig. Visningsrækkefølgen er den dybde-først venstre-til-højre gennemgang (traversal) af det træ, punktum

En minimal tresiders fil gø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-arrayet lyder [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. Enhver kode, der itererer objekter i numerisk rækkefølge og samler dem med /Type /Page, vil producere den forkerte sekvens på denne fil

Hvorfor producerer generatorer ikke-sekventielle layouts? Flere grunde. Et bibliotek, der forhåndsallokerer objektnumre for alle sider før skrivning af deres indhold, vil nummerere dem i oprettelsesrækkefølge og derefter skrive faktiske bytes i hvilken som helst rækkefølge, der passer til serialisatoren. Et fletteværktøj, der syr dokumenter sammen, omnummererer objekter fra hvert kildedokument for at undgå kollisioner; de omnummererede side-objekter ender spredt ud over den kombinerede objekttabel, mens det nye rod /Kids-array holder den korrekte visningsrækkefølge. Trinvise opdateringer tilføjer (append) nye objekter i slutningen af filen med friske numre, så en side tilføjet som en revision lever nær slutningen af byte-strømmen, selvom den hører til på position 1 i visningsrækkefølgen

Flade træer og indlejrede undertræer

Specifikationen tillader to former for sidetræet. Simple generatorer producerer en flad struktur: én rod /Pages-knude, hvis /Kids-array ikke indeholder andet end /Page-bladobjekter. Det er nemt at gennemgå: ét niveau dybt, ét gennemløb

Store dokumenter bruger rutinemæssigt et balanceret træ i stedet. Rod /Pages-knudens /Kids-array indeholder mellemliggende /Pages-knuder, som hver især indeholder sit eget /Kids-array. /Count på hver mellemliggende knude rapporterer det samlede antal bladsider i dens undertræ, så en fremviser kan springe hele undertræer over, når den hopper til en side via indeks, uden at parse hvert objekt. Et 1.000-siders dokument struktureret som et balanceret træ med 10 sider pr. bladknude (leaf node) kan lokalisere side 750 ved binær søgning gennem tre eller fire ordbogsopslag (dictionary lookups) i stedet for at scanne 750 /Kids-indgange

Konsekvensen for behandlingskode (processing code): du kan ikke antage, at det første niveau af /Kids indeholder /Page-objekter. Hvert barn skal tjekkes. Hvis dens /Type er /Pages, skal du recurse ind i den. Hvis dens /Type er /Page, er den et blad (leaf). At stoppe ved det første niveau taber i stilhed hele undertræer på ethvert dokument, hvor generatoren valgte at indlejre (nest). Hvorfor forfattere vælger dybe træer i første omgang, hvad udfladningsværktøjer (flattening tools) opgiver, og hvordan /Count-korruption udspiller sig i praksis, dækkes i vores ledsagende artikel om sidetræ-form, fan-out og /Count-integritet

Nedarvede sideegenskaber

Sidetræet bærer også en ressourcedelingsmekanisme. Visse sideegenskaber: /MediaBox, /CropBox, /Resources og /Rotate kan nedarves (ISO 32000-2 §7.7.3.4). Hvis en /Page-ordbog udelader en af dem, går en læser (reader) op ad /Parent-kæden, indtil den finder egenskaben eller når roden. At placere en delt skrifttype-ordbog i rod /Pages-knuden i stedet for at kopiere den ind i hver blad-side (leaf page) kan reducere filstørrelsen mærkbart for dokumenter, der bruger de samme skrifttyper overalt

Nedarvningsreglen (The inheritance rule) skaber en subtilitet for kode, der læser sideegenskaber. At læse /MediaBox direkte fra et /Page-objekt og behandle en manglende nøgle som en fejl er forkert; nøglen kan simpelthen være nedarvet. Kode, der korrekt løser sidegeometri, skal følge forældrekæden (the parent chain). Den har også brug for en cyklusvagt (cycle guard): en korrumperet fil kan have en /Parent-reference, der peger tilbage på en allerede besøgt knude, hvilket ville køre i en uendelig løkke uden et besøgt-objekt tjek

Xref-tabellen og krydsreference-strømme

Indirekte objektopslag går gennem krydsreferencetabellen (eller dens efterfølger, krydsreferencestrømmen introduceret i PDF 1.5). Xref-en mapper hvert objektnummer til en byte-offset inde i filen. En overholdende læser (conforming reader) bruger xref'en til at hoppe direkte til ethvert objekt; den scanner ikke filen sekventielt. Det random-access design er det, der gør hurtigt sidehop muligt: fremviseren læser kataloget, løser /Pages-referencen via xref'en, læser rod /Pages-knuden, løser en /Kids-indgang og så videre, idet den kun rører (touching) de objekter, den har brug for

Trinvise opdateringer tilføjer en ny xref-sektion i slutningen af filen med en trailer, der lænker tilbage til den forrige. Et objekt opdateret i en revision får en ny indgang i den tilføjede (appended) xref-sektion; de originale bytes bliver på plads, men erstattes (superseded). Det er sådan, digitalt signerede PDF'er forbliver verificerbare, selv efter at annotations- eller formularudfyldningsrevisioner er tilføjet: det signerede byte-område røres aldrig, og det nye indhold lever i den tilføjede sektion. Sidetræet kan også opdateres, så sidetilføjelser eller -sletninger i en revision producerer en ny /Pages-rod med et revideret /Kids-array, mens det gamle rodobjekt stadig optager sin oprindelige position i filen. Lineariseret (web-optimeret) filer tilføjer et byte-layout twist: objekterne for side 1 flyttes fysisk til forsiden af filen, så en fremviser kan vise den første side, mens resten stadig downloader, alligevel forbliver sidetræet den eneste autoritet på rækkefølge — kun offsets registreret i xref'en ændres

Hvad går galt uden træ-gennemgang (tree traversal)

Fejltilstanden (The failure mode) for objekt-scannings tilgange er stille. Output-dokumentet ser plausibelt ud: det har det rigtige antal sider, og hver side indeholder genkendeligt indhold. Rækkefølgen er bare forkert, og forkert på en måde, der afhænger af generatoren, antallet af revisioner og om nogen sider blev flettet fra eksterne kilder. Et testkorpus (test corpus) af filer produceret af et enkelt værktøj kan bestå fuldstændigt; filer fra et andet værktøj eller en flette-workflow vil fejle. Den inkonsekvens er grunden til, at heuristiske rettelser aldrig holder. For en gennemgang af nøjagtig denne fejl i et rigtigt kundedokument — symptom, fejldiagnose og gennemgangs-rettelsen (the traversal fix) — se vores sidetræ-fejlfindings-casestudie

Filer med trinvis opdatering er særligt tilbøjelige til dette, fordi sider tilføjet eller omarrangeret i senere revisioner bærer høje objektnumre, mens visningsrækkefølgen styres af det opdaterede /Kids-array. En scanning, der behandler objekter i numerisk rækkefølge, vil placere disse sent-nummererede sider til sidst uanset, hvor træet siger, at de hører til

Rettelsen er ikke kompliceret. Start ved kataloget, løs /Pages-referencen, gå (walk) gennem /Kids-arrayet rekursivt, og udsend (emit) blade i den rækkefølge, du støder på dem. Det er visningsrækkefølgen per definition, uanset objektnumre, byte-offsets eller filstruktur. De fleste modne PDF-biblioteker udstiller (expose) et sideantal og en indekseret sidetilgang (page accessor), der allerede gør dette korrekt; risikoen ligger i kode, der omgår bibliotekets sidemodel og rører (touches) objektlaget direkte

Én strukturel anomali, der er værd at håndtere eksplicit: /Count-værdien på en mellemliggende /Pages-knude kan være forkert i fejlformaterede (malformed) filer. At stole på /Count til bounds-tjek og derefter stoppe før en fuld gennemgang vil lydløst udelade sider, når antallet er underdrevet. At bruge /Count udelukkende som et præstations-hint til kapacitets-forhåndsallokering (capacity pre-allocation) eller binær søgning, og udlede det faktiske antal fra gennemgang, er det sikrere mønster for vigtige dokumenter

 Næste artikel