Teknisk artikel

PDF:s logiska objektmodell: Typer, referenser och struktur

En PDF-fil är, i grunden, en samling objekt som pekar på varandra. Ta bort komprimeringen, korsreferensbokföringen och byte-förskjutningarna, och det som återstår är en graf: en liten uppsättning typade värden, ihopkopplade med referenser, rotade i ett enda objekt som läsaren vet hur den ska hitta. Allt som en PDF kan uttrycka, från ett textstycke till ett inbäddat typsnitt till en digital signatur, byggs av åtta primitiva objekttyper och regeln som låter ett objekt referera till ett annat. Lär dig dessa, så läses resten av formatet som komposition snarare än ett mysterium

Detta är det logiska lagret av PDF, definierat i ISO 32000-1 klausul 7.3, och det sitter en nivå över den fysiska fillayouten (headern, kroppen, korsreferenstabellen och trailern, vilket är ett eget ämne i den tekniska översikten av PDF-filstruktur). Den logiska modellen är vad dessa bytes betyder när de väl tolkats. En visare läser filen baklänges för att hitta trailern, följer den till roten, och därifrån vecklar dokumentet ut sig som objekt som refererar till objekt. Detta är den del du resonerar kring när du felsöker en missbildad sida, skriver en tolkare (parser), eller förlitar dig på ett bibliotek för att montera ihop ett dokument

Åtta objekttyper, och ingenting annat

PDF definierar exakt åtta grundläggande objekttyper. Varje värde i ett dokument är ett av dem, vilket är vad som håller formatet hanterbart trots dess räckvidd

Booleans är nyckelorden true och false. De slår flaggor på och av, som exempelvis huruvida en kommentar (annotation) skrivs ut

Numbers (siffror) kommer i två smaker som specifikationen behandlar som en typ: heltal (integers) som 42 och reella tal (reals) som 3.14 eller -0.002. PDF har ingen exponentnotation, så du kommer aldrig att se 1e6 i en överensstämmande fil. Koordinater, typsnittsstorlekar och rotationsvinklar är alla siffror

Strings (strängar) rymmer sekvenser av bytes, skrivna antingen inom parentes, (Hello), eller inom vinkelparenteser som hexadecimala, <48656C6C6F>. Båda notationerna kodar identiskt innehåll; hex är nödutgången för bytes som är besvärliga inuti parenteser. Strängar bär text, men de är i första hand bytes, vilket spelar roll i det ögonblick du hanterar något bortom ASCII

Names (namn) är atomära tokens inledda med ett snedstreck: /Type, /Pages, /MediaBox. Ett namn är inte en sträng; det är en identifierare, som används som en ordlistenyckel (dictionary key) eller ett uppräknat värde, och två namn är lika endast om de matchar byte för byte. Snedstrecket är syntax, inte en del av namnet. Detta fäller nybörjare som behandlar /Times-Roman och strängen (Times-Roman) som utbytbara; formatet gör inte det

Arrays är ordnade, heterogena listor inom hakparenteser: [0 0 612 792] är en sidorektangel, och en array kan blanda typer fritt, inklusive referenser till andra objekt. Dictionaries (ordlistor) är arbetshästarna. Skriven mellan << och >>, mappar en ordlista namnnycklar till värden av valfri typ, och nästan varje meningsfull struktur i PDF, såsom sida, katalog, typsnitt, kommentar, är en ordlista med en /Type-nyckel som deklarerar vad den är

Streams (strömmar) är ordlistor med en svans av råa bytes mellan nyckelorden stream och endstream. Ordlistan beskriver byten (deras längd, och eventuella filter som FlateDecode som komprimerar dem), och byten bär den skrymmande nyttolasten: sidinnehållsinstruktioner, inbäddade typsnittsprogram, bilder. En ström är där PDF lägger in allt som är för stort eller för binärt för att sitta inline

Den åttonde typen är nullobjektet, nyckelordet null. Det är ett verkligt värde, skilt från att en nyckel saknas. En ordlistepost satt till null behandlas som om den inte vore närvarande, och en referens som löses till ett icke-existerande objekt ger också null snarare än ett fel. Det förlåtande beteendet är avsiktligt: det låter en skadad fil degraderas i stället för att vägra öppnas. Det finns ingen nionde typ; allt som PDF uttrycker kommer från hur dessa åtta kombineras

Direkta värden, indirekta objekt och referenser

Vilken som helst av dessa åtta typer kan dyka upp på två sätt. Ett direkt objekt skrivs på plats, som 612 inuti en MediaBox-array. Ett indirekt objekt ges en identitet så att andra objekt kan peka på det: två heltal, ett objektnummer och ett generationsnummer, som omsluter definitionen i obj och endobj:

12 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj

Detta är objekt 12, generation 0, en typsnittsordlista (font dictionary). Någon annanstans i filen refererar ett annat objekt till det med en indirekt referens: samma två nummer följt av nyckelordet R, 12 0 R. Referensen är en pekare. När en sidas resursordlista säger /Font << /F1 12 0 R >>, namnger den objekt 12 som typsnittet bakom resursnamnet /F1, utan att kopiera typsnittets definition till sidan

Generationsnumret finns till för raderingar och återanvändning. När ett objekt frigörs och dess plats återanvänds, inkrementeras generationen så att en inaktuell (stale) 12 0 R inte kan lösas till den nya hyresgästen på plats 12. Nyskrivna filer är nästan alla i generation 0, men en kraftigt redigerad fil kan bära på högre nummer, och en tolkare som ignorerar generationen kommer till slut att läsa fel objekt

Indirektion är vad som gör PDF effektiv och redigerbar. Ett typsnitt, en bild eller en färgrymd (color space) kan definieras en gång och refereras till från hundra sidor. En liten förändring kan läggas till (append) som en ny revision som ersätter ett enskilt objekt i stället för att skriva om hela filen. Korsreferenstabellen är det index som förvandlar ett objektnummer till en byte-förskjutning, så att läsaren kan hoppa rakt till 12 0 obj utan att skanna, men det är en fysisk optimering. Rent logiskt är allt du behöver veta att 12 0 R betyder "objektet identifierat som 12 0."

Katalogen: där varje dokument börjar

Att lösa referenser måste börja någonstans, och detta någonstans är trailerns /Root-post, som pekar på dokumentkatalogen (document catalog): objektgrafens rot, en ordlista med /Type /Catalog. Läsaren når den först eftersom trailern hittas först, och därifrån kan varje annan del av dokumentet nås genom att följa referenser

Katalogen bär bara på två strikt obligatoriska poster: dess /Type, och /Pages, en indirekt referens till sidträdets rot. Resten är valfria och beskriver dokumentövergripande beteende snarare än innehåll: /Outlines pekar på bokmärkesträdet, /Names rymmer namnträd nycklade med strängar, /Metadata refererar till en XMP-metadataström, och /PageMode och /PageLayout föreslår hur en visare ska öppna dokumentet. Inga av dessa behövs för att rendera en sida; de konfigurerar upplevelsen kring sidorna. Bokmärkes-, metadata- och kommentarstrukturer som hänger i katalogen tas upp i artikeln om PDF-metadata, bokmärken och kommentarer

Diagrammet nedan visar var objektkroppen sitter i den omgivande filen. Katalogen och sidträdet bor inuti den kroppen som vanliga indirekta objekt; headern, korsreferenstabellen och trailern runt dem är de fysiska byggnadsställningarna som låter en läsare lokalisera dem

Diagram över en PDF-fils fyra fysiska sektioner: en versionsheader, en kropp som rymmer dokumentobjekten inklusive katalog och sidträd, en korsreferenstabell över objektförskjutningar, och en trailer som pekar på roten

Sidträdet: en balanserad hierarki av sidor

Från /Pages förgrenar sig dokumentet in i sidträdet, där PDF:s val av en graf över en platt lista ger utdelning. Sidor lagras inte som en enkel sekvens; de hänger i ett träd vars inre noder är sidträdsnoder (/Type /Pages) och vars löv är sidobjekt (/Type /Page). En inre nod listar sina barn i en /Kids-array och registrerar, i /Count, hur många lövsidor som bor inunder den. Varje nod utom roten bär en /Parent-referens tillbaka upp, så trädet kan vandras i båda riktningarna

2 0 obj                                  % sidträdets rot
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>
endobj

3 0 obj                                  % en lövsida
<< /Type /Page /Parent 2 0 R
   /MediaBox [0 0 612 792]
   /Resources << /Font << /F1 12 0 R >> >>
   /Contents 5 0 R >>
endobj

4 0 obj                                  % en inre nod som grupperar ytterligare två sidor
<< /Type /Pages /Parent 2 0 R /Kids [6 0 R 7 0 R] /Count 2 >>
endobj

Här är objekt 2 roten, med tre sidor under sig: lövsidan 3, plus två till nåbara genom inre nod 4. Rotens /Count på 3 måste vara lika med de totala löven inunder den, och ett antal som inte stämmer överens med den faktiska strukturen är ett vanligt sätt som en handredigerad fil går snett på. Poängen med trädet är åtkomstlokalitet (locality of access). En läsare som öppnar sida 900 av ett tusensidigt dokument vandrar inte igenom 900 objekt; den stiger ner ett fåtal noder, eftersom ett välformat träd förblir grunt och balanserat. Att bygga ett sådant träd för hand är tillräckligt pilligt för att vara värt att se från början till slut, vilket genomgången om att bygga ett PDF-dokument från grunden gör

Trädet förtjänar sitt andra uppehälle genom arv (inheritance). En handfull sidattribut, /Resources, /MediaBox, /CropBox och /Rotate, kan sättas på en inre nod och lämnas borta från de enskilda sidorna, som då ärver den närmaste förfaderns värde. Sätt /MediaBox en gång på roten och varje löv får samma sidstorlek utan att repetera det; en sida som behöver skilja sig åt deklarerar sin egen. Detta är det enda stället i objektmodellen där ett värdes mening beror på ett objekts position i trädet, inte bara på dess eget innehåll

Vad en lövsida faktiskt innehåller

Ett sidobjekt är sammankopplingspunkten mellan den strukturella modellen och det synliga innehållet. Dess /Contents-post refererar till en eller flera innehållsströmmar (content streams), de ritningsoperatorer som målar text och grafik på sidan. Dess /Resources-ordlista namnger typsnitten, bilderna och färgrymderna som dessa operatorer förlitar sig på, där varje post är en indirekt referens till ett objekt delat över sidor. /MediaBox anger sidans rektangel i punkter (1/72 tum), och poster som /Rotate och /CropBox justerar hur den presenteras

Den arbetsdelningen är hela modellen i miniatyr. Sidordlistan är struktur: typade poster och referenser som säger vad sidan är och vad den ritar med. Innehållsströmmen är instruktioner: en separat, komprimerbar klump (blob) som säger hur det ska ritas. Typsnittet bakom /F1 är en delad resurs, definierad en gång och pekad på varhelst det används. Ordlista, ström och referens samarbetar för att rendera en sida, och samma mönster skalar till hela dokumentet. Innehållsströmsoperatorerna inuti den klumpen täcks separat för text och typsnitt samt för grafik och visuella element

Varför denna modell är värd att känna till

De flesta utvecklare möter objektmodellen endast när något går sönder: en sida renderas blank eftersom dess /Contents-referens hänger löst (dangles), text kommer ut som lådor eftersom en typsnittsresurs aldrig var inbäddad, ett verktyg rapporterar en /Count som inte stämmer överens med de sidor den kan hitta. Vart och ett av dessa är ett uttalande om grafen, och att läsa grafen direkt överträffar gissningar. De åtta typerna och referensregeln är en tillräckligt liten vokabulär för att hålla i huvudet, och när du väl ser en PDF som objekt som pekar på objekt, slutar missbildade filer att vara ogenomskinliga (opaque)

Som sagt, att skriva modellen för hand är sällan rätt val bortom lärande. Att hålla korsreferensförskjutningar, generationsnummer, sidträdsantal och strömlängder konsekventa genom redigeringar är den typen av bokföring som ett bibliotek existerar för att hantera. I produktion hanterar ett moget PDF-utvecklingsbibliotek objektgrafen samtidigt som det lämnar dig att tänka i sidor och innehåll. Att känna till modellen lönar sig ändå: du förstår vad biblioteket bygger under huven, och varför