Technisch artikel

PDF-bestandsstructuur: Hoe het formaat daadwerkelijk werkt

PDF is geen documentformaat zoals Word of RTF dat zijn. Die formaten slaan een reeks content op die een renderer pas op het moment van weergave interpreteert, zodat de uitvoer afhankelijk is van welke lettertypen en lay-out-engine (layout engine) er toevallig aanwezig zijn. PDF slaat het resultaat van dat proces op: nauwkeurige renderinstructies, lettertypeprogramma's, gecomprimeerde afbeeldingsstromen (image streams) en een objectengraaf (object graph) die ze samenbindt tot een op zichzelf staande beschrijving van elke pagina. Het bestand bevat voldoende informatie om elke pagina identiek te reproduceren op elke conforme renderer. Dit is zowel het belangrijkste ontwerddoel als de bron van de meeste complexiteit die u tegenkomt wanneer u er programmatisch een probeert te genereren, te parsen of aan te passen.

Het objectmodel

Elke PDF is een verzameling genummerde objecten. Een object kan een boolean, geheel getal (integer), reëel getal (real number), naam (name), string, array, woordenboek (dictionary), stream of null zijn. Bijna alles wat interessant is, is een woordenboek: een set sleutel-waardeparen (key-value pairs) waarbij de sleutels namen zijn en de waarden elk ander type object, inclusief referenties naar andere objecten via nummer en generatietelling (generation count). Een stream is een woordenboek gevolgd door een bytereeks, die doorgaans is gecomprimeerd.

Het cataloguswoordenboek (catalog dictionary) vormt de root. Het wijst naar de paginaboom (page tree), die de paginawoordenboeken in een gebalanceerde boomstructuur ordent in plaats van in een vlakke lijst. Hierdoor vereist het navigeren naar pagina 5.000 van een document met 10.000 pagina's niet het doorlopen van elke voorgaande paginabeschrijving. Elk paginawoordenboek verwijst naar zijn content streams (één of meer reeksen van paginabeschrijvingsoperatoren), zijn bronwoordenboek (resource dictionary, die op zijn beurt verwijst naar lettertypebeschrijvingen (font descriptors), kleurruimten en afbeelding-XObjects) en zijn mediabox (de coördinatenruimte waarin de pagina leeft). De coördinaatoorsprong bevindt zich in de linkerbenedenhoek, met positieve Y-waarden naar boven, in eenheden van 1/72 inch.

Aan het einde van het bestand bevindt zich de kruisverwijzingstabel (cross-reference table), die elk objectnummer koppelt aan zijn byte-offset in het bestand. Dit is wat random access mogelijk maakt: een viewer leest eerst de kruisverwijzingstabel en zoekt vervolgens rechtstreeks naar de objecten die hij nodig heeft. PDF 1.5 introduceerde cross-reference streams, die de tabel comprimeren in een streamobject en gerelateerde objecten verpakken in object streams, waardoor de bestandsgrootte aanzienlijk kleiner wordt voor documenten met veel kleine objecten.

Content streams en het grafische model

De visuele inhoud van een pagina bevindt zich in een of meer content streams. Elke stream is een reeks PDF-operatoren afgewisseld met hun operanden. De tekstoperator BT begint een tekstobject, Tf selecteert een lettertype en grootte uit het bronwoordenboek, Td positioneert de tekstcursor, Tj of TJ schildert (tekent) een string, en ET sluit het tekstobject. Vectorgraphics volgen een vergelijkbaar patroon: m stelt het startpunt van een pad in, l voegt een lijnsegment toe, c voegt een bézierkromme toe en f of S vult (fills) of omlijnt (strokes) het pad.

De grafische status (graphics state) regelt alles wat er tussen operatoren gebeurt: de huidige transformatiematrix, lijnbreedte, kleurruimte, vulkleur, omlijningskleur (stroke color) en het bijsnijdpad (clipping path). Operatoren zoals q en Q pushen en poppen de grafische status op en van een stack (stapel). Zo implementeert PDF lokale coördinatentransformaties en tijdelijke statusoverschrijvingen zonder de context eromheen te beïnvloeden. Form XObjects generaliseren dit: een op zichzelf staande content stream met een eigen bronwoordenboek (resource dictionary) dat met een enkele Do-operator op willekeurige posities en schalen op een pagina kan worden geschilderd.

Lettertype-insluiting (font embedding) en tekstextractie

PDF kan per naam naar lettertypen verwijzen en erop vertrouwen dat de viewer iets vervangt. In de praktijk moet elk document dat u wilt delen echter de lettertypegegevens insluiten (embedden). Een in een PDF ingesloten Type 1 of TrueType/OpenType-lettertype bevat een font descriptor-woordenboek dat naar een font file stream wijst. Voor TrueType-lettertypen bevat die stream het binaire lettertypeprogramma; voor Type 1 is het de PFB-data. Subsetting, wat elke serieuze PDF-generator doet, stript de tekens (glyphs) weg waarnaar niet in het document wordt verwezen, waardoor de bestandsgrootte beheersbaar blijft, zelfs voor grote Unicode-lettertypen.

Bij tekstextractie slaat lettertype-insluiting (font embedding) terug. De visuele weergave van een teken wordt bepaald door een glyph in het ingesloten lettertypeprogramma. De Unicode-waarde van dat teken wordt bepaald door een ToUnicode CMap-stream die aan het lettertypewoordenboek is gekoppeld. Wanneer de ToUnicode CMap ontbreekt of onjuist is, kan een PDF-viewer tekst leesbaar renderen, maar niet als betekenisvolle Unicode extraheren. Dat is waarom kopiëren en plakken uit sommige PDF's afval (garbage) oplevert. Tagged PDF (ISO 32000 §14.8) voegt een tweede laag toe: een logische structuurboom die de paginacontent koppelt aan document-semantische rollen zoals alinea's, koppen en tabelcellen. Schermlezers en reflow-engines (reflow engines) gebruiken de structuurboom in plaats van de onbewerkte (raw) content stream-volgorde, wat verklaart waarom een visueel goed ingedeelde PDF nog steeds ontoegankelijk kan zijn als de tags ontbreken of verkeerd zijn.

Incrementele updates en digitale handtekeningen

Wanneer u wijzigingen in een bestaande PDF opslaat zonder deze helemaal opnieuw te schrijven (rewriten), worden de nieuwe objecten toegevoegd na de body van het originele bestand, samen met een nieuwe kruisverwijzingssectie (cross-reference section) en een nieuw trailerwoordenboek. De bijgewerkte trailer wijst naar de nieuwe kruisverwijzingsgegevens en vervangen objecten blijven in het bestand, maar er wordt simpelweg niet naar verwezen door de nieuwe kruisverwijzingsketen. Dit heet een incrementele update, en het heeft twee belangrijke gevolgen.

Ten eerste groeit het bestand met elke opslagcyclus. Een document dat herhaaldelijk wordt bewerkt en opgeslagen, verzamelt lagen met verouderde objecten. Tools zoals QPDF kunnen een bestand lineariseren of comprimeren-en-herschrijven om die ruimte terug te vorderen, maar de standaard (default) is opeenhoping (accumulation). Ten tweede zijn digitale handtekeningen afhankelijk van incrementele updates voor hun integriteitsmodel. Een ISO 32000-handtekening dekt een bytebereik van het bestand, typisch alles behalve de tijdelijke aanduiding (placeholder) voor de handtekeningwaarde zelf. Eventuele wijzigingen na ondertekening die verschijnen als aanvullende incrementele updates, zijn zichtbaar voor een validerende reader als aanpassingen gedaan na de ondertekening. Dat is exact het auditspoor (audit trail) dat u wenst. Dit betekent echter ook dat bepaalde wijzigingen, zoals het toevoegen van een goedkeuringshandtekening of het invullen van formuliervelden, expliciet door de standaard zijn toegestaan zonder dat de oorspronkelijke handtekening ongeldig wordt gemaakt. De voorwaarde is wel dat de wijzigingen conform de machtigingsinstellingen van het document zijn (ISO 32000-2 §12.7.6). Een aanpassing die buiten deze permissies valt, wordt gemarkeerd als ongeautoriseerd. Dit onderscheid goed toepassen is belangrijk wanneer u documenten genereert die verderop in de pijplijn (downstream) mede worden ondertekend.

Conformiteitsniveaus (Conformance levels) en de ISO 32000-stamboom

PDF begon in 1993 als een propriëtair formaat van Adobe, nam het weergavemodel van PostScript over en vergaarde gedurende vijftien versies steeds meer functies: encryptie in 1.1, interactieve formulieren in 1.2, digitale handtekeningen en logische structuur in 1.3, transparantie in 1.4, object streams in 1.5 en AES-encryptie in 1.6. Adobe diende in 2007 PDF 1.7 in bij ISO, wat resulteerde in ISO 32000-1:2008. ISO 32000-2:2020 heeft betrekking op PDF 2.0, dat verschillende ondergespecificeerde gebieden heeft aangescherpt, de AES-256-sleutelafleiding heeft herzien (revisie 6 in plaats van revisie 5) en expliciete ondersteuning heeft toegevoegd voor geassocieerde bestanden (associated files) en rich media.

De substandaarden komen voort uit dezelfde basis. PDF/A (ISO 19005) ruilt functionaliteiten in voor archiveringsstabiliteit: geen encryptie, geen externe contentafhankelijkheden, alle lettertypen ingesloten (embedded), apparaatonafhankelijke kleurruimten en verplichte XMP-metadata. PDF/A-1 is gebaseerd op PDF 1.4, PDF/A-2 op PDF 1.7 en PDF/A-3 staat ingesloten bestanden in elk gewenst formaat toe. PDF/X (ISO 15930) is de subset voor printproductie: output intents, bleed- en trimboxen (afloop en snijmarges) en geen transparantie op oudere conformiteitsniveaus. PDF/UA (ISO 14289) stelt getagde structuur (tagged structure), Unicode-mappings en taalmetadata verplicht ten behoeve van toegankelijkheid. Dit zijn geen concurrerende formaten; het zijn sets aanvullende beperkingen (constraints) bovenop kern-PDF. Een enkel bestand kan aan meer dan één formaat tegelijk voldoen, mits de restricties niet met elkaar in conflict komen.

Voor iedereen die code schrijft die PDF's genereert of verwerkt, is de praktische basis ISO 32000-2. Let hierbij zorgvuldig op de secties over het kruisverwijzingsmodel (cross-reference model, §7.5), de grafische status (§8.4), de operatoren voor de tekststatus (text state, §9.3), lettertypebeschrijvingen (font descriptors) en ToUnicode (§9.6 en §9.10), interactieve formulieren (§12.7) en digitale handtekeningen (§12.8). De standaard is lang, maar het meeste programmatische PDF-werk raakt herhaaldelijk slechts een smal deel ervan. Het begrijpen van het objectmodel en het kruisverwijzingsmechanisme is het startpunt; al het andere is specialisatie vanaf daar.