Ga naar inhoud

Wat een PDF eigenlijk is

Evidence: Standard-backed

Een PDF is geen paginabeschrijving die toevallig in een bestand staat. Het is een kleine grafendatabase met een printer eraan vastgekoppeld. Deze pagina beschrijft de vier onderdelen waaruit elke PDF bestaat — header, body, cross-referencetabel, trailer — en hoe NextPDF ze schrijft, zodat een lezer elk object kan vinden zonder te gokken.

De meeste PDF-bugs zijn geen weergavebugs, maar structuurbugs: een byte-offset die één teken voorbij het bedoelde object wijst, een trailer die de verkeerde root benoemt, een cross-referenceregel die niet overeenkomt met waar het object werkelijk staat. Geen van deze fouten verandert hoe een pagina eruitziet, totdat een lezer een andere route door het bestand neemt en voorbij het einde ervan terechtkomt.

Als je een PDF als een ondoorzichtig geheel behandelt, lijken die fouten willekeurig. Als je het objectmodel kent, zie je ze voor wat ze zijn: een getal dat niet overeenkomt met een positie. Het formaat kunnen lezen maakt het verschil tussen „de PDF is beschadigd” en „de offset van object 14 is verouderd omdat de writer die heeft gemeten voordat de stream-lengte werd afgerond.”

Een PDF heeft vier onderdelen, in bestandsvolgorde:

  1. Een header — één regel die de versie benoemt (%PDF-2.0).
  2. Een body — een reeks genummerde indirecte objecten: dictionaries, streams, arrays, getallen, strings, names.
  3. Een cross-referencetabel (of, in PDF 2.0, een cross-reference stream) — een opzoektabel van objectnummer naar byte-offset, zodat elk object bereikbaar is zonder het bestand te scannen.
  4. Een trailer — een kleine dictionary die het rootobject van het document benoemt en aangeeft waar de cross-referencesectie begint.

Een lezer leest een PDF niet van voor naar achter. De lezer leest eerst de laatste regel, vindt startxref, springt naar de cross-referencesectie en gebruikt die als index in de body. Het formaat is gebouwd om achterstevoren te worden gelezen. Dat ene feit verklaart het grootste deel van het ontwerp.

NextPDF bouwt een PDF op in dezelfde volgorde waarin het formaat wordt gelezen: eerst het object schrijven, daarna de offset vastleggen en als laatste de tabel schrijven.

Elk indirect object krijgt een nummer van één registry (src/Core/ObjectRegistry.php). De registry deelt opeenvolgende nummers uit via allocate() en legt via register() de byte-offset vast, nadat de bytes van een object naar de uitvoerbuffer zijn geschreven. Offsets worden nooit vooraf geraden. Ze worden afgelezen van BinaryBuffer::getOffset() op het moment waarop de objectheader wordt uitgeschreven. Daarom kan een cross-referenceregel van NextPDF niet afwijken van het object dat de regel beschrijft: de offset is precies de werkelijke positie van de buffer.

Zodra de body compleet is, schrijft een versiespecifieke serialisatiestrategie (src/Writer/PdfSerializationStrategy.php) de cross-referencesectie en de trailer:

  • Pdf20StreamStrategy genereert een gecomprimeerde cross-reference stream (/Type /XRef) — de PDF 2.0-standaard.
  • Pdf17TableStrategy en Pdf14TableStrategy genereren een traditionele cross-reference tabel met regels van 20 bytes plus een aparte trailer-dictionary — vereist door de PDF/A-profielen die een oudere bestandsstructuur voorschrijven.

De strategie wordt gekozen door het uitvoerprofiel, niet afgeleid. Welke strategie ook wordt gebruikt, de laatste bytes hebben dezelfde vorm: de cross-referencesectie, dan startxref, dan de byte-offset, dan %%EOF. Dat staartstuk vindt een lezer als eerste.

  1. Step 1 of 4: ISO 32000-2 §7.5.5 — %%EOF and startxref at the file end
  2. Step 2 of 4: ISO 32000-2 §7.5.4 / §7.5.8 — the cross-reference section maps object number to offset
  3. Step 3 of 4: ISO 32000-2 §7.5.5 — the trailer names /Root, the document catalog
  4. Step 4 of 4: ISO 32000-2 §7.3.10 — each indirect object is reached at its recorded offset
Hoe een lezer een object oplost in een NextPDF-bestand, en de bepaling uit ISO 32000-2 die elke stap definieert: hij begint aan het einde van het bestand en werkt naar binnen toe.

De vierdelige structuur is geen NextPDF-conventie, maar de bepaling over de bestandsstructuur uit Spec: ISO 32000-2, §7.5 . De standaard definieert een PDF als een header, een body met objecten, een cross-referencetabel en een trailer, en stelt dat een lezer moet parsen vanaf het einde van het bestand. De laatste regel is %%EOF, en de twee regels ervoor zijn het sleutelwoord startxref en de byte-offset naar de cross-referencesectie.

Evidence: Standard-backed

Een indirect object wordt gedefinieerd als een objectnummer en een generatienummer, gescheiden door witruimte, gevolgd door de waarde van het object tussen de sleutelwoorden obj en endobj. De combinatie van objectnummer en generatienummer identificeert het object uniek; een indirecte referentie ernaar wordt geschreven als het objectnummer, het generatienummer en het sleutelwoord R. De ObjectRegistry van NextPDF volgt dit precies: een opeenvolgend nummer, generatie 0 voor nieuw geschreven objecten, en een vastgelegde offset.

Vanaf PDF 1.5 mogen objecten ook in een object stream staan; daarin worden ze opgeslagen zonder de sleutelwoorden obj/endobj en moeten ze generatie nul hebben. De cross-reference stream (/Type /XRef, Spec: ISO 32000-2, §7.5.8 ) is het PDF 2.0-mechanisme dat zowel gewone objecten als deze gecomprimeerde objecten indexeert. De CrossReferenceStream van NextPDF bouwt deze stream met een /W-array met veldbreedtes en FlateDecode-compressie.

Dit is de vorm van een minimale PDF-body en de bijbehorende trailer. De getallen in de cross-referencesectie zijn byte-offsets. Ze moeten exact kloppen; daarom legt NextPDF ze vast vanuit de buffer in plaats van ze te berekenen.

%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
0000000122 00000 n
trailer
<< /Size 4 /Root 1 0 R >>
startxref
196
%%EOF

Een lezer leest dit van onderaf: %%EOF, dan startxref 196, dan springt de lezer naar byte 196, waar xref begint. Daar leest de lezer dat object 1 op byte 9 staat, volgt /Root 1 0 R naar de catalog en doorloopt vandaaruit de paginaboom. Object 0 is altijd de kop van de vrije lijst met generatie 65535 — een eigenaardigheid uit het vroegste ontwerp van het formaat die getrouw wordt gereproduceerd omdat lezers dat verwachten.

De valkuil is denken dat een PDF van boven naar beneden wordt gelezen, zoals broncode. Dat is niet zo. De body kan de objecten in elke willekeurige volgorde bevatten. Objectnummers hoeven in het bestand niet opeenvolgend te zijn; een lezer vertrouwt er nooit op dat dit wel zo is. De enige gezaghebbende index is de cross-referencesectie, en de enige manier om die te vinden is de trailer aan het einde. Een PDF met een volkomen geldige body en één enkel verkeerd getal in startxref is onleesbaar. Een PDF met objecten in een door elkaar gehusselde volgorde, maar met een correcte cross-referencetabel, is prima. Positie is betekenisloos; de vastgelegde positie is alles.

Deze pagina beschrijft de bestandsstructuur, niet de pagina-inhoud. Hoe markeringen op een pagina terechtkomen — content streams, grafische operatoren, tekstweergave — is een apart onderwerp. Deze pagina behandelt ook niet wat er gebeurt wanneer een bestand wordt gewijzigd nadat het is geschreven. Dat is de taak van incrementele updates, waarbij de writer een tweede cross-referencesectie toevoegt en de trailer terugkoppelt naar de vorige.

NextPDF is een writer. Het gedrag dat hier wordt beschreven, is de manier waarop NextPDF een document serialiseert dat het zelf heeft opgebouwd. NextPDF is geen algemene PDF-parser of reparatietool. NextPDF belooft niet dat het een willekeurig bestand van derden met een beschadigde cross-referencetabel kan lezen, reconstrueren of redden. De garantie is beperkt en bewust gekozen. De bestanden die NextPDF schrijft, hebben correcte offsets, omdat ze worden gemeten en niet voorspeld.

Waarom gebruiken nieuwe bestanden altijd 0 als generatienummer? Generatienummers bestaan voor hergebruik van objecten over updates heen. In een net geschreven bestand heeft elk object generatie 0. Generaties die niet nul zijn, verschijnen pas wanneer een bestand incrementeel is bijgewerkt en een objectnummer wordt hergebruikt.

Kunnen twee objecten hetzelfde nummer hebben? Niet binnen één cross-referencesectie. Over meerdere incrementele updates heen kan een bestand fysiek meerdere kopieën van hetzelfde objectnummer bevatten. De meest recente cross-referenceregel wint. Dat is het onderwerp van de volgende pagina.

Maakt de objectvolgorde in het bestand uit voor de uitvoer? Nee. NextPDF schrijft objecten in een deterministische volgorde voor reproduceerbare builds, maar een lezer lost alles op via de cross-referencesectie; de fysieke volgorde is dus semantisch niet van betekenis.

  • Indirect object — een genummerd object in de body, geschreven als N G obj … endobj, waarbij N het objectnummer is en G het generatienummer.
  • Indirect reference — een verwijzing naar een indirect object, geschreven als N G R.
  • Cross-reference table (xref) — de index van objectnummer naar byte-offset. In PDF 2.0 is dit doorgaans een cross-reference stream (/Type /XRef) in plaats van de klassieke teksttabel van 20 bytes per regel.
  • Trailer — de dictionary aan het einde van een cross-referencesectie die /Root (de document catalog) en /Size noemt en via de startxref-offset wordt gevonden.
  • Object stream — een stream-object dat zelf andere indirecte objecten bevat (samen gecomprimeerd); leden hebben geen obj/endobj en generatie nul.
  • Document catalog — het object dat door /Root wordt aangewezen; het toegangspunt tot de paginaboom en al het andere in het document.