Technisch artikel

Het logische PDF-objectmodel: typen, verwijzingen en structuur

Een PDF-bestand is in de kern een verzameling objecten die naar elkaar verwijzen. Haal de compressie, de cross-reference-administratie en de byte-offsets weg, en wat overblijft is een graaf: een kleine reeks getypeerde waarden, onderling verbonden door verwijzingen, geworteld in één object dat een lezer weet te vinden. Alles wat een PDF kan uitdrukken, van een alinea tekst tot een ingesloten lettertype of een digitale handtekening, is opgebouwd uit acht primitieve objecttypen en de regel waarmee één object naar een ander kan verwijzen. Wie die begrijpt, leest de rest van het formaat als compositie in plaats van mysterie.

Dit is de logische laag van PDF, gedefinieerd in ISO 32000-1 clausule 7.3, en zij bevindt zich één niveau boven de fysieke bestandsindeling (de header, de body, de cross-reference-tabel en de trailer, een apart onderwerp in het technisch overzicht van de PDF-bestandsstructuur). Het logische model is de betekenis van die bytes na het parsen. Een viewer leest het bestand van achter naar voren om de trailer te vinden, volgt die naar de root, en van daaruit ontvouwt het document zich als objecten die naar objecten verwijzen. Dit is het deel waarover je nadenkt als je een misvormde pagina debugt, een parser schrijft of een bibliotheek vertrouwt om een document samen te stellen.

Acht objecttypen, en niets meer

PDF definieert precies acht basisobjecttypen. Elke waarde in een document is er één van, wat het formaat beheersbaar houdt ondanks zijn reikwijdte.

Booleans zijn de sleutelwoorden true en false. Ze zetten vlaggen aan en uit, zoals of een annotatie wordt afgedrukt.

Getallen bestaan in twee varianten die de specificatie als één type beschouwt: gehele getallen zoals 42 en reële getallen zoals 3.14 of -0.002. PDF kent geen exponentnotatie, zodat u nooit 1e6 in een conform bestand zult zien. Coördinaten, lettergroottes en rotatiehoeken zijn allemaal getallen.

Tekenreeksen bevatten reeksen bytes, geschreven tussen haakjes, (Hello), of tussen punthaken als hexadecimaal, <48656C6C6F>. Beide notaties coderen identieke inhoud; hex is de uitweg voor bytes die lastig zijn tussen haakjes. Tekenreeksen dragen tekst, maar zijn in de eerste plaats bytes, wat van belang wordt zodra u iets anders dan ASCII verwerkt.

Namen zijn atomaire tokens die beginnen met een schuine streep: /Type, /Pages, /MediaBox. Een naam is geen tekenreeks; het is een identifier, gebruikt als woordenboeksleutel of opgesomde waarde, en twee namen zijn alleen gelijk als ze byte voor byte overeenkomen. De schuine streep is syntaxis, geen deel van de naam. Dit struikelt nieuwelingen die /Times-Roman en de tekenreeks (Times-Roman) als uitwisselbaar beschouwen; het formaat doet dat niet.

Arrays zijn geordende, heterogene lijsten tussen vierkante haakjes: [0 0 612 792] is een paginarechthoek, en een array kan typen vrij mengen, inclusief verwijzingen naar andere objecten. Woordenboeken zijn het werkpaard. Geschreven tussen << en >>, koppelt een woordenboek naamsleutels aan waarden van elk type, en vrijwel elke betekenisvolle structuur in PDF, pagina, catalogus, lettertype, annotatie, is een woordenboek met een /Type-sleutel die aangeeft wat het is.

Streams zijn woordenboeken met een staart van ruwe bytes tussen de sleutelwoorden stream en endstream. Het woordenboek beschrijft de bytes (hun lengte en eventuele filters zoals FlateDecode die ze comprimeren), en de bytes dragen de omvangrijke lading: paginainhoudsinstructies, ingesloten lettertypeprogramma's, afbeeldingen. Een stream is waar PDF alles plaatst dat te groot of te binair is om inline te staan.

Het achtste type is het null-object, het sleutelwoord null. Het is een echte waarde, onderscheiden van een afwezige sleutel. Een woordenboekinvoer ingesteld op null wordt behandeld alsof die niet aanwezig is, en een verwijzing die wordt omgezet naar een niet-bestaand object levert ook null op in plaats van een fout. Dat vergevingsgezinde gedrag is opzettelijk: het laat een beschadigd bestand degraderen in plaats van weigeren te openen. Er is geen negende type; alles wat PDF uitdrukt, komt voort uit de manier waarop deze acht worden gecombineerd.

Directe waarden, indirecte objecten en verwijzingen

Elk van die acht typen kan op twee manieren voorkomen. Een direct object wordt ter plekke geschreven, zoals de 612 in een MediaBox-array. Een indirect object krijgt een identiteit zodat andere objecten ernaar kunnen verwijzen: twee gehele getallen, een objectnummer en een generatienummer, die de definitie omhullen in obj en endobj:

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

Dit is object 12, generatie 0, een lettertypewoordenboek. Elders in het bestand verwijst een ander object ernaar met een indirecte verwijzing: dezelfde twee getallen gevolgd door het sleutelwoord R, 12 0 R. De verwijzing is een pointer. Wanneer het resourcewoordenboek van een pagina zegt /Font << /F1 12 0 R >>, benoemt het object 12 als het lettertype achter de resourcenaam /F1, zonder de definitie van het lettertype in de pagina te kopiëren.

Het generatienummer bestaat voor verwijderingen en hergebruik. Wanneer een object wordt vrijgegeven en zijn slot opnieuw wordt gebruikt, wordt de generatie verhoogd zodat een verouderde 12 0 R niet kan worden omgezet naar de nieuwe bewoner van slot 12. Vers geschreven bestanden zijn vrijwel allemaal generatie 0, maar een sterk bewerkt bestand kan hogere nummers bevatten, en een parser die de generatie negeert, leest uiteindelijk het verkeerde object.

Indirectie is wat PDF efficiënt en bewerkbaar maakt. Eén lettertype, afbeelding of kleurruimte kan eenmaal worden gedefinieerd en vanuit honderd pagina's worden verwezen. Een kleine wijziging kan worden toegevoegd als een nieuwe revisie die één object vervangt in plaats van het bestand opnieuw te schrijven. De cross-reference-tabel is de index die een objectnummer omzet in een byte-offset, zodat de lezer rechtstreeks naar 12 0 obj springt zonder te scannen, maar dat is een fysieke optimalisatie. Logisch gezien hoeft u alleen te weten dat 12 0 R betekent "het object geïdentificeerd als 12 0."

De catalogus: waar elk document begint

Het omzetten van verwijzingen moet ergens beginnen, en dat beginpunt is de /Root-invoer van de trailer, die verwijst naar de documentcatalogus: de wortel van de objectgraaf, een woordenboek met /Type /Catalog. De lezer bereikt die als eerste omdat de trailer als eerste wordt gevonden, en van daaruit is elk ander deel van het document bereikbaar door verwijzingen te volgen.

De catalogus bevat slechts twee strikt vereiste invoeren: zijn /Type en /Pages, een indirecte verwijzing naar de wortel van de paginaboom. De rest is optioneel en beschrijft documentbrede gedragingen in plaats van inhoud: /Outlines verwijst naar de bladwijzerboom, /Names bevat naambomen gekeyed op tekenreeks, /Metadata verwijst naar een XMP-metadatastream, en /PageMode en /PageLayout suggereren hoe een viewer het document moet openen. Geen van die is nodig om een pagina te renderen; ze configureren de ervaring rondom de pagina's. De bladwijzer-, metadata- en annotatiestructuren die aan de catalogus hangen, komen aan bod in het artikel over PDF-metadata, bladwijzers en annotaties.

Het onderstaande diagram toont waar de objectbody zich bevindt in het omringende bestand. De catalogus en paginaboom bevinden zich daarbinnen als gewone indirecte objecten; de header, cross-reference-tabel en trailer eromheen zijn de fysieke steigers waarmee een lezer ze kan lokaliseren.

Diagram van de vier fysieke secties van een PDF-bestand: een versieheader, een body met de documentobjecten inclusief de catalogus en paginaboom, een cross-reference-tabel met objectoffsets, en een trailer die naar de root verwijst

De paginaboom: een uitgebalanceerde hiërarchie van pagina's

Vanuit /Pages vertakt het document zich in de paginaboom, waarbij PDF's keuze voor een graaf boven een platte lijst zijn vruchten afwerpt. Pagina's worden niet opgeslagen als een eenvoudige reeks; ze hangen aan een boom waarvan de interne knooppunten paginaboomknooppunten zijn (/Type /Pages) en waarvan de bladeren paginaobjecten zijn (/Type /Page). Een intern knooppunt somt zijn kinderen op in een /Kids-array en registreert in /Count hoeveel bladpagina's eronder hangen. Elk knooppunt behalve de root bevat een /Parent-verwijzing naar boven, zodat de boom in beide richtingen kan worden doorlopen.

2 0 obj                                  % root of the page tree
<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 3 >>
endobj

3 0 obj                                  % a leaf page
<< /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                                  % an interior node grouping two more pages
<< /Type /Pages /Parent 2 0 R /Kids [6 0 R 7 0 R] /Count 2 >>
endobj

Hier is object 2 de root, met drie pagina's eronder: de bladpagina 3, plus twee meer bereikbaar via intern knooppunt 4. De /Count van 3 van de root moet gelijk zijn aan het totale aantal bladeren eronder, en een telling die niet overeenkomt met de werkelijke structuur is een veelvoorkomende manier waarop een handmatig bewerkt bestand fout gaat. Het nut van de boom is lokaliteit van toegang. Een lezer die pagina 900 opent van een document van duizend pagina's, loopt niet 900 objecten af; hij daalt een handvol knooppunten af, omdat een welgevormde boom ondiep en uitgebalanceerd blijft. Zo'n boom met de hand opbouwen is lastig genoeg om van begin tot eind te laten zien, wat de doorloop in een PDF-document van scratch opbouwen doet.

De boom verdient zijn waarde ook door overerving. Een handvol paginaattributen, /Resources, /MediaBox, /CropBox en /Rotate, kan worden ingesteld op een intern knooppunt en weggelaten bij de afzonderlijke pagina's, die dan de waarde van de dichtstbijzijnde voorouder overerven. Stel /MediaBox eenmaal in op de root en elk blad krijgt hetzelfde paginaformaat zonder het te herhalen; een pagina die afwijkt, geeft zijn eigen waarde aan. Dit is de enige plek in het objectmodel waar de betekenis van een waarde afhangt van de positie van een object in de boom, niet alleen van zijn eigen inhoud.

Wat een bladpagina werkelijk bevat

Een paginaobject is het koppelpunt tussen het structurele model en de zichtbare inhoud. Zijn /Contents-invoer verwijst naar een of meer inhoudsstreams, de tekeninstructies die tekst en afbeeldingen op de pagina schilderen. Zijn /Resources-woordenboek benoemt de lettertypen, afbeeldingen en kleurruimten waarop die operators vertrouwen, elke invoer een indirecte verwijzing naar een object dat over pagina's wordt gedeeld. De /MediaBox geeft de paginarechthoek in punten (1/72 inch), en invoeren zoals /Rotate en /CropBox passen de presentatie aan.

Die taakverdeling is het hele model in het klein. Het paginawoordenboek is structuur: getypeerde invoeren en verwijzingen die zeggen wat de pagina is en waarmee zij tekent. De inhoudsstream zijn instructies: een afzonderlijk, comprimeerbaar blok dat zegt hoe te tekenen. Het lettertype achter /F1 is een gedeelde resource, eenmaal gedefinieerd en overal waarnaar wordt verwezen. Woordenboek, stream en verwijzing werken samen om één pagina te renderen, en dezelfde patronen schalen naar het hele document. De inhoudsstream-operators in dat blok worden apart behandeld voor tekst en lettertypen en voor afbeeldingen en visuele elementen.

Waarom dit model de moeite waard is om te kennen

De meeste ontwikkelaars komen in aanraking met het objectmodel pas als er iets misgaat: een pagina wordt leeg weergegeven omdat zijn /Contents-verwijzing bungelt, tekst verschijnt als blokjes omdat een lettertyperesource nooit is ingesloten, een tool rapporteert een /Count die niet overeenkomt met de pagina's die het kan vinden. Elk van die gevallen is een uitspraak over de graaf, en de graaf direct lezen is beter dan gokken. De acht typen en de verwijzingsregel zijn een klein genoeg vocabulaire om in uw hoofd te houden, en zodra u een PDF ziet als objecten die naar objecten verwijzen, houden misvormde bestanden op ondoorgrondelijk te zijn.

Dat gezegd hebbende, is het model met de hand schrijven zelden de juiste keuze buiten het leren om. Cross-reference-offsets, generatienummers, paginaboomtellingen en streamlengtes consistent houden bij bewerkingen is de boekhouding waarvoor een bibliotheek bestaat. In productie beheert een volwassen PDF-ontwikkelbibliotheek de objectgraaf terwijl u kunt denken in termen van pagina's en inhoud. Kennis van het model loont nog steeds: u begrijpt wat de bibliotheek onderliggend opbouwt, en waarom.