Ga naar inhoud
getnextpdf.com

De anatomie van een PDF-bestand

Open een willekeurige PDF in een gewone teksteditor en het eerste wat je ziet, is geruststellend: een %PDF-1.x- of %PDF-2.0-header. Het laatste wat je ziet, is %%EOF. Alles tussen die twee regels is een nette kleine machine om objecten te vinden op nummer. Deze pagina is een ontleding. We openen het bestand, benoemen elk orgaan, en laten zien hoe ze met elkaar verbonden zijn.

Het is de structurele tegenhanger van twee buren. Wat een PDF eigenlijk is behandelt het bestand als een object- graaf; incrementele updates gaan over hoe het in de loop der tijd groeit. Deze pagina blijft dicht bij de bytes — de fysieke regio’s die een parser doorloopt, in de volgorde waarin ze op schijf staan.

Je hebt dit bijna nooit nodig om een PDF te gebruiken. Je hebt het nodig op de dag dat er een misgaat. Een bestand opent in de ene viewer en niet in de andere; een validator meldt een “beschadigde cross-referencetabel”; een ondertekend document faalt plotseling bij verificatie. Geen van die zijn mysteries zodra je de anatomie kunt lezen. Ze zijn een getal dat niet langer overeenkomt met een positie, een regio op de verkeerde plek, of een staart die naar niets wijst.

De indeling kennen verandert “de PDF is corrupt” in een diagnose waar je naar kunt handelen. Het is het verschil tussen je schouders ophalen bij een black box en wijzen naar de exacte byte die liegt.

Een conforme PDF heeft vier fysieke onderdelen, in deze bestandsvolgorde (Spec: ISO 32000-2, §7.5.1):

  1. Een header — één regel, %PDF-2.0, die de versie benoemt.
  2. Een body — het grootste deel van het bestand: een reeks genummerde indirecte objecten.
  3. Een cross-referencesectie — een index van objectnummer naar de byte- offset waar dat object staat. Klassieke PDF’s gebruiken een tekst-tabel; PDF 2.0 gebruikt een gecomprimeerde xref-stream.
  4. Een trailer — een kleine dictionary die het toegangspunt benoemt, gevolgd door startxref, een offset en %%EOF.

De twist: een lezer begint niet bovenaan. Hij begint onderaan, leest startxref om de index te vinden, en gebruikt die index om elk object direct te bereiken. Het bestand wordt van voor naar achter geschreven maar van achter naar voren gelezen.

Laten we de vier regio’s op volgorde doorlopen, met de bytes voor ons.

De header is één regel. NextPDF schrijft %PDF-2.0, en conventioneel een tweede commentaarregel van high-bit-bytes zodat naïeve transfertools het bestand als binair behandelen, niet als tekst. Die tweede regel is waarom een PDF die als gewone tekst wordt geopend een kleine wirwar laat zien direct na de versie.

De body is waar het document leeft. Elk indirect object is een nummer, een generatie, het sleutelwoord obj, een waarde, en endobj (Spec: ISO 32000-2, §7.3.10). De waarde is een van enkele vormen — maar twee dragen vrijwel het hele gewicht:

  • Een dictionary, << /Key value … >>, is een map van names naar values. De paginaboom, de catalog, de font descriptors: allemaal dictionaries.
  • Een stream is een dictionary gevolgd door stream, een blok willekeurige bytes, en endstream. Pagina-inhoud, ingesloten lettertypen en afbeeldingen zijn streams, vrijwel altijd gecomprimeerd. (Hun filters zijn een verhaal apart, verteld in streams en filters.)

Objecten wijzen naar elkaar via een indirect reference2 0 R betekent “object 2, generatie 0.” Dat is de bedrading die een platte lijst objecten verandert in een graaf.

De cross-referencesectie is het onderdeel dat de meeste mensen zich nooit correct voorstellen. In de klassieke vorm is het gewone tekst: het sleutelwoord xref, dan subsecties van vaste-breedte 20-byte-regels (Spec: ISO 32000-2, §7.5.4). Elke regel is een tiencijferige byte-offset, een vijfcijferige generatie, en een enkele flag — n voor in gebruik, f voor vrij — aangevuld tot precies twintig bytes zodat een lezer naar elke vermelding kan springen met alleen rekenkunde. PDF 2.0 vervangt dit door een cross-reference stream: dezelfde index, maar binair en gecomprimeerd binnen een stream-object gemarkeerd /Type /XRef (Spec: ISO 32000-2, §7.5.8). Kleiner, en in staat om objecten te beschrijven die binnen object streams zijn verpakt.

De trailer is de inhoudsopgave van het bestand (Spec: ISO 32000-2, §7.5.5). Hij benoemt /Root — de document catalog, het enige object waar al het andere aan hangt — en /Size, het objectaantal. Dan komt de handdruk die achterwaarts lezen mogelijk maakt: startxref, een byte-offset op een eigen regel, en %%EOF. Een lezer springt naar het einde, leest die offset, springt direct naar de cross-referencesectie, en is vertrokken.

  1. HeaderOne line, %PDF-2.0, naming the version. A binary-marker comment usually follows.
  2. BodyNumbered indirect objects — dictionaries and streams — referenced by N G R.
  3. Cross-reference sectionA text table of 20-byte entries, or a compressed /Type /XRef stream in PDF 2.0.
  4. TrailerNames /Root and /Size, then startxref + offset + %%EOF.
  5. Read orderA reader starts at %%EOF, follows startxref to the index, then reaches each object directly.
De vier fysieke regio's van een PDF in bestandsvolgorde, en het pad dat een lezer er werkelijk doorheen neemt — beginnend bij de trailer en naar binnen werkend via de cross-referencesectie.

Er is een vijfde regio die een langlevend bestand laat groeien: een incrementele update. Een wijziging wordt niet ter plaatse geschreven. De gewijzigde objecten, een verse cross-reference- sectie en een nieuwe trailer worden toegevoegd na de eerste %%EOF, en die nieuwe trailer draagt /Prev — de offset van de vorige cross-reference- sectie (Spec: ISO 32000-2, §7.5.6). De secties vormen een achterwaartse keten; voor elk objectnummer wint de nieuwste vermelding. Omdat de oorspronkelijke bytes nooit verschuiven, houdt de cryptografische controle over het bytebereik dat een handtekening daadwerkelijk dekt nog steeds stand na een update. Een latere incrementele update kan nog steeds wijzigen wat een validator over het document als geheel rapporteert — of de wijzigingen na ondertekening zijn toegestaan, en wat de handtekening geacht wordt te certificeren — maar hij kan de ondertekende bytes zelf niet wijzigen. Die eigenschap is het hele onderwerp van incrementele updates.

Hier is de hele anatomie in één minimaal bestand. De getallen onder xref zijn byte-offsets, en ze moeten exact zijn — wijs één teken voorbij waar een object begint en een strikte lezer geeft het op.

%PDF-2.0
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer
<< /Size 4 /Root 1 0 R >>
startxref
186
%%EOF

Lees het zoals een parser het doet. Laatste regel: %%EOF. Daarboven: startxref 186, dus spring naar byte 186, waar xref begint. De tabel zegt dat object 1 op byte 9 staat. De /Root 1 0 R van de trailer wijst daarheen — de catalog — en vanuit de catalog loop je /Pages naar de paginaboom en vind je de enkele pagina. Object 0 is altijd de kop van de vrije lijst met generatie 65535, een fossiel uit het eerste ontwerp van het formaat dat elke lezer nog steeds verwacht te zien.

De valkuil is een PDF lezen als een verhaal — van boven naar beneden, op volgorde. Het is geen verhaal; het is een index met een achterwaartse wijzer. Objectnummers hoeven niet opeenvolgend te zijn in het bestand, objecten kunnen in elke fysieke volgorde verschijnen, en een lezer vertrouwt nooit op hun positie. De enige gezaghebbende kaart is de cross-referencesectie, en de enige manier om die kaart te vinden is de startxref- offset helemaal aan het einde.

Het gevolg verrast mensen. Een PDF met een vlekkeloze body en één verkeerd cijfer in startxref is onleesbaar — de lezer kan de index niet vinden. Een PDF met zijn objecten in door elkaar gehusselde volgorde maar een correcte cross-referencesectie is prima. Fysieke positie draagt geen betekenis. De vastgelegde positie draagt het allemaal.

Deze pagina beschrijft de fysieke structuur, niet de pagina-inhoud. Hoe markeringen op een pagina belanden — content-stream-operatoren, tekstweergave, grafische status — is een apart onderwerp. Ze beschrijft ook een welgevormd bestand. PDF’s in de echte wereld zijn vaak een beetje kapot en overleven alleen omdat vergevingsgezinde viewers de cross-referencetabel opnieuw opbouwen door te scannen op obj-sleutelwoorden. Die berging is een viewergedrag, geen iets dat het formaat garandeert.

Reading and repairing arbitrary third-party PDFs — edition availability
EditionAvailability
CoreNextPDF is een writer. Het legt elke offset uit de uitvoerbuffer vast op het moment dat elk object wordt uitgestuurd, zodat de bestanden die het produceert een cross-referencesectie hebben die door constructie overeenkomt met de body.
ProHet parsen, reconstrueren of repareren van een beschadigde cross-referencetabel in een bestand dat NextPDF niet heeft geschreven, valt buiten de scope in elke editie.
EnterpriseGebruik voor inspectie van de structuur van een bestaand bestand een speciale parser of validator; NextPDF garandeert correctheid voor wat het schrijft, niet voor wat het leest.

Waarom heeft een PDF een binary-marker-regel na de header? Sommige oudere transfertools zouden een bestand verminken waarvan ze dachten dat het gewone tekst was. Het high-bit-commentaar laat het bestand er ondubbelzinnig binair uitzien, zodat het de reis ongewijzigd overleeft.

Is de xref-stream gewoon een kleinere tabel? Grotendeels, met één extra kracht. Naast gecomprimeerd zijn, kan een xref-stream objecten beschrijven die binnen object streams zijn opgeslagen — vermeldingen die de klassieke 20-byte- teksttabel op geen enkele manier kan uitdrukken.

Kan ik zowel een tabel als een stream in één bestand hebben? Eén revisie gebruikt het een of het ander. Maar een hybride bestand kan een klassieke tabel voor oude lezers koppelen aan een cross-reference stream voor nieuwe, zodat elk soort lezer een index vindt die hij begrijpt.

  • Header — de eerste regel, %PDF-2.0, die de versie benoemt; doorgaans gevolgd door een binary-marker-commentaar.
  • Indirect object — een genummerd object in de body, geschreven als N G obj … endobj, waarbij N het objectnummer is en G de generatie.
  • Dictionary — een << /Key value … >>-map van names naar values; de meest voorkomende objectvorm.
  • Stream — een dictionary plus een blok bytes tussen stream en endstream, gebruikt voor inhoud, lettertypen en afbeeldingen.
  • Cross-reference table (xref) — de index van objectnummer naar byte- offset; klassiek een teksttabel van 20 bytes per vermelding, een /Type /XRef-stream in PDF 2.0.
  • Trailer — de dictionary die /Root en /Size benoemt, gevonden via de startxref-offset aan het einde van het bestand.
  • Incrementele update — gewijzigde objecten, een nieuwe cross-referencesectie, en een nieuwe trailer toegevoegd na %%EOF, met /Prev die terugkoppelt naar de vorige sectie.