Vår tillhörande förklaring om PDF-sidordning täcker grundregeln: visningsordningen kommer från en djup-först, vänster-till-höger-vandring i /Kids-arrayerna i /Pages-trädet, aldrig från objektnummer. Den här artikeln tittar på trädet från en annan vinkel — dess form. Varför ger mogna PDF-skrivare ifrån sig hierarkier av mellanliggande noder när en enda platt array vore helt laglig? Vad är det som faktiskt ändras när ett verktyg plattar till eller bygger om trädet? Och vad händer när /Count-bokföringen som gör hela strukturen snabb slutar berätta sanningen?
Utspridning är ett prestandabeslut
Inget tvingar en författare att nästla. Ett dokument på 10 000 sidor med en /Pages-rotnod och 10 000 lövreferenser i en enda /Kids-array överensstämmer med specifikationen. PDF-referensen rekommenderar ändå ett balanserat träd för stora dokument, och vanliga generatorer följer de råden med en blygsam utspridning (fan-out), vanligtvis ett par dussin barn per mellanliggande nod
Anledningen är vad en visare måste läsa innan den kan visa något. Överväg ett hopp direkt till sida 8 214 i den där 10 000-sidiga filen. Med ett platt träd måste visaren först tolka rotnoden, och den rotnoden är en enorm array: med ungefär åtta byte per indirekt referens, ett 80 KB-objekt som måste tokeniseras från början till slut innan post 8 213 kan lösas upp. Med ett balanserat träd med utspridning 32, läser samma hopp roten, jämför löpande /Count-summor för att välja rätt barn, och går ner — tre eller fyra små ordböcker totalt, var och en på några hundra byte. Det är den O(log n) slumpmässiga åtkomst som trädet utformades för att tillhandahålla, och det är hela anledningen till att /Count existerar på mellanliggande noder: det låter en läsare hoppa över ett helt underträd utan att öppna ett enda objekt inuti det
Trädformen sätter också kostnaden för redigering. En inkrementell uppdatering som infogar en sida måste skriva om varje nod vars /Kids eller /Count ändrades, vilket betyder vägen från det nya lövets förälder upp till roten. I ett balanserat träd är den vägen en handfull små ordböcker som läggs till på slutet av filen. I ett platt träd är "vägen" den enda gigantiska rot-arrayen, duplicerad i sin helhet vid varje revision. Ett avtal som går igenom trettio granska-och-anteckna-cykler kan sluta med att släpa runt på trettio ersatta kopior av samma 80 KB-array i sin byteström
Inre noder bär ärvda egenskaper
Mellanliggande noder är inte bara dirigering. De fyra ärvbara sidegenskaperna — /Resources, /MediaBox, /CropBox och /Rotate — kan lyftas upp till vilken /Pages-nod som helst, där de gäller för varje löv under den såvida inte en ättling åsidosätter dem. En författare som producerar en rapport med en liggande (landscape) bilaga kan uttrycka den layouten i själva trädet:
5 0 obj % document root
<< /Type /Pages /Count 6 /Kids [6 0 R 7 0 R] >>
endobj
6 0 obj % report body: portrait A4, body font
<< /Type /Pages /Parent 5 0 R /Count 3
/Kids [30 0 R 31 0 R 32 0 R]
/MediaBox [0 0 595 842]
/Resources << /Font << /F1 8 0 R >> >> >>
endobj
7 0 obj % appendix: landscape A4, rotated, its own font
<< /Type /Pages /Parent 5 0 R /Count 3
/Kids [40 0 R 41 0 R 42 0 R]
/MediaBox [0 0 842 595] /Rotate 90
/Resources << /Font << /F2 9 0 R >> >> >>
endobj
40 0 obj % appendix page: inherits size, rotation, fonts
<< /Type /Page /Parent 7 0 R /Contents 43 0 R >>
endobj
Objekt 40 till 42 är nästan tomma. Deras sidstorlek, rotation och typsnittsresurser anländer alla genom arv från nod 7, vilket håller filen kompakt och självunderhållande: lägg till en fjärde sida under bilaganoden och den blir automatiskt liggande (landscape)
Samma mekanism skapar den klassiska faran med att flytta sidor. Antag att ett verktyg flyttar objekt 40 in i rapportkroppen genom att redigera de två /Kids-arrayerna och peka om /Parent till nod 6. Flytten är strukturellt giltig, ändå ärver objekt 40 nu den stående (portrait) /MediaBox, ingen rotation och typsnitt /F1 — medan dess innehållsström fortfarande väljer /F2, som inte längre löser upp sig. Sidan krymper, av-roteras och förlorar sin text i en enda redigering. Robust omordningskod materialiserar därför de upplösta värdena för alla fyra ärvbara egenskaper på sidordboken innan den byter förälder på den. Om du någonsin har dragit en sida i en redigerare och sett den byta storlek eller orientering, är det här mekanismen du bevittnade
Plattläggning: lagligt, vanligt, ibland dyrt
Många verktyg går åt andra hållet. Minimala skrivare avger ett en-nivåträd eftersom det är enkelt, och många sammanslagnings- och delningsverktyg bygger om vilket träd de än läser till en platt /Kids-array, eftersom att generera balanserad struktur är extra arbete och platt utdata alltid är konform. En korrekt ombyggnad måste lösa arv samtidigt: varje egenskap ett löv ärvde måste kopieras till lövet, eller lyftas till den nya roten om det är enhetligt över dokumentet — annars ändrar utdatan geometri precis på samma sätt som i fallet med sidflyttningen
För typiska dokument är plattläggning (flattening) ofarligt. Det gör ont i stor skala, på de två sätt som redan beskrivits: rot-arrayen blir ett stort objekt som varje öppning och varje sidhopp måste tolka i sin helhet, och varje strukturell redigering skriver om den i sin helhet. Vad plattläggning inte förstör är delning genom indirekta referenser — ett platt träd där alla 10 000 sidor pekar på samma /Resources-ordboksobjekt är fortfarande avduplicerat. Vad som går förlorat är bara möjligheten att lämna posten utanför sidan och låta en förfader tillhandahålla den
När /Count ljuger
/Count är ren bokföring: det måste vara lika med antalet lövsidor i nodens underträd, och ingenting i filformatet upprätthåller det. Två korruptionsmönster står för de flesta av de ljugande räkningar som ses i det vilda
Det första är den inaktuella räkningen som lämnas kvar av en inkrementell uppdatering. En redigerare infogar en sida, skriver om den omedelbara föräldern med en ny /Kids och en uppdaterad /Count, lägger till båda i filen — och rör aldrig förfäderna:
% Original revision
12 0 obj
<< /Type /Pages /Count 9 /Kids [13 0 R 14 0 R 15 0 R] >>
endobj
14 0 obj
<< /Type /Pages /Parent 12 0 R /Count 3
/Kids [50 0 R 51 0 R 52 0 R] >>
endobj
% Appended revision: one page inserted into the middle branch.
% Object 14 is superseded; object 12 is never rewritten
14 0 obj
<< /Type /Pages /Parent 12 0 R /Count 4
/Kids [50 0 R 51 0 R 90 0 R 52 0 R] >>
endobj
Trädet rymmer nu tio löv, men roten säger fortfarande nio. En visare som litar på roten rapporterar nio sidor i sin sidräknare. En som använder inre räkningar för att binärsöka ett sidhopp beräknar ett fel index för varje sida efter infogningspunkten. En fullständig traversering hittar tio. Tre olika svar, en fil
Det andra mönstret är räkningen som aldrig kan vara rätt: negativ, noll på en fylld nod, eller absurt enorm. Dessa kommer från fuzzing, från överföringsskador och ibland från aritmetiska buggar i redigerare. De är farliga specifikt för kod som litar på /Count för allokering — att dimensionera en array från en /Count på -3 väcker i bästa fall ett intervallfel, och att göra det från en /Count på två miljarder är en överbelastningsallokering. Värdet är opålitlig indata, som alla andra nummer i filen
Tolkar delas in i två läger över allt detta. Strikta konsumenter — preflight-verktyg, PDF/A-validatorer, arkiv-pipelines — jämför /Count mot traverseringsresultatet och avvisar eller flaggar filen. Interaktiva visare är nästan universellt milda (lenient): de traverserar, härleder det riktiga antalet, och ignorerar tyst det lagrade, vilket är exakt varför en fil med inaktuellt antal kan cirkulera i flera år utan klagomål tills den möter en strängare tolk inuti något automatiserat arbetsflöde. Den defensiva medelvägen för bibliotekskod är att behandla /Count som en ledtråd — användbart för förallokering, och för att hoppa över underträd när det väl har verifierats — medan traversering förblir sanningens källa
För själva traverseringsalgoritmen, arvsuppslagningsreglerna och vandringen från katalog till löv, börja med förklaringen om sidordning. För att se hur dessa fellägen ser ut när ett verkligt kunddokument når produktionskoden, läs fallstudien om felsökning av sidordning, som följer en incident med blandade sidor från symptom till grundorsak
HotPDF-komponenten hanterar allt detta internt: den traverserar nästlade träd av vilket djup som helst, löser upp ärvda egenskaper när sidor kopieras eller flyttas, och verifierar /Count mot faktiska lövantal i stället för att lita på den, så sidindex i dess API betyder alltid logiska sidor