Haal de paginabeschrijvingen weg en je houdt een dunne laag van structuur over die niemand afdrukt, maar waar elke lezer, indexeerder en archiefsysteem afhankelijk van is. Een pagina-object weet niets over het hoofdstuk waartoe het behoort, de auteur die het heeft geschreven, of de voetnoot die naar elders linkt. Die kennis leeft één niveau hoger, in drie structuren die aan de documentcatalogus (document catalog) zijn bevestigd: de metadata-streams, de outline-boom (outline tree) en de annotatie-arrays per pagina. Ze delen een eigenschap die het makkelijk maakt om ze verkeerd te doen. Geen van hen draagt zichtbare markeringen op de pagina, dus een bestand kan perfect worden weergegeven en toch zijn bladwijzers missen, zijn eigen auteur-veld tegenspreken, of een link naar een pagina-object wijzen dat niet langer bestaat
Dit is de laag die een PDF-bibliotheek blootlegt als documenteigenschappen, bookmark-API's en link- of annotatie-aanroepen, en de laag die een zoekcrawler leest om te beslissen waar je document over gaat. Het objectmodel daaronder wordt behandeld in de walkthrough van de PDF-documentstructuur. Hier ligt de nadruk strikt op wat er aan de catalogus hangt
Alle drie de structuren hechten zich aan de catalogus. Een complete catalogus die ze aan elkaar koppelt, ziet er zo uit:
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
/Outlines 3 0 R
/Names << /EmbeddedFiles 4 0 R >>
/Metadata 5 0 R
>>
endobj
Vier vermeldingen, vier onafhankelijke subsystemen. /Pages is het zichtbare document; /Outlines is de bladwijzerboom (bookmark tree); /Metadata wijst naar de XMP-stream; /Names bereikt de documentbrede naam-dictionary (name dictionary), die onder andere ingesloten bestandsbijlagen (embedded file attachments) bevat. Elk is optioneel, en een lezer die geen van hen vindt, toont nog steeds de pagina's. Die optionele aard is precies de reden waarom de navigatielaag het eerste is dat wegrot wanneer een bestand wordt bewerkt door tools die alleen pagina's begrijpen
Twee metadata-opslagen die het oneens zijn
PDF draagt document-metadata op twee plaatsen tegelijk, en de problemen beginnen wanneer ze verschillende dingen zeggen. Het oorspronkelijke mechanisme is de documentinformatie-dictionary, waarnaar wordt verwezen door /Info in de trailer: een platte set van sleutel-waardeparen (key-value pairs) voor /Title, /Author, /Subject, /Keywords, /Creator, /Producer en de twee datums. Het is eenvoudig en elke viewer leest het. PDF 2.0 raadt het grootste deel ervan af (deprecates) ten gunste van het tweede mechanisme, de XMP metadata-stream
XMP is een op zichzelf staand XML-document, geschreven in RDF, opgeslagen als een stream die de catalogus bereikt via /Metadata en gemarkeerd met /Type /Metadata /Subtype /XML. In tegenstelling tot de Info-dictionary, begraven in de PDF-objectstructuur, is een XMP-pakket ontworpen om op zichzelf te worden geëxtraheerd en geparseerd door tools die niets van PDF afweten. Hier is een representatief pakket:
5 0 obj
<< /Type /Metadata /Subtype /XML /Length 1235 >>
stream
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
<dc:title><rdf:Alt><rdf:li xml:lang="x-default">Quarterly Report</rdf:li></rdf:Alt></dc:title>
<dc:creator><rdf:Seq><rdf:li>A. Author</rdf:li></rdf:Seq></dc:creator>
<xmp:CreateDate>2026-06-16T10:46:27+08:00</xmp:CreateDate>
<xmp:CreatorTool>Reporting Service 4.2</xmp:CreatorTool>
<pdf:Producer>losLab PDF Library</pdf:Producer>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
endstream
endobj
Drie details in dat blok bepalen of de metadata contact met echte tooling overleeft. De xpacket-verwerkingsinstructies zijn geen decoratie: ze omlijsten het pakket zodat een extractor het binnen een grotere bytestream kan vinden, en een schrijver die de sluitende <?xpacket end="w"?> weglaat, produceert een bestand dat prima opent maar stricte validators doet struikelen. De datatypen van de eigenschappen doen er ook toe. dc:title is een taalalternatief verpakt in rdf:Alt, terwijl dc:creator een geordende lijst is en rdf:Seq aanneemt; het uiten van beide als een kale text-node (bare text node) is de meest gemaakte XMP-fout, getolereerd door de meeste viewers totdat je er één tegenkomt die dat niet doet. De namespace-voorvoegsels (namespace prefixes) zijn conventioneel, maar de URI's waaraan ze binden zijn normatief: een parser baseert zich op de URI, niet op het voorvoegsel
De harde regel met twee opslagen is dat ze het met elkaar eens moeten zijn. Als /Info zegt dat de auteur de ene persoon is en dc:creator een ander noemt, heb je een document afgeleverd dat dezelfde vraag op twee manieren beantwoordt, en welk antwoord wint hangt af van welk veld de consumerende tool leest. Een bibliotheek schrijft meestal beide voor je, maar op het moment dat je er één met de hand bewerkt, of bestanden van verschillende generatoren samenvoegt, drijven de twee uit elkaar. Behandel de Info-dictionary als legacy-compatibiliteit en XMP als de bron van waarheid (source of truth), en genereer beide opnieuw vanuit één set waarden in plaats van ze onafhankelijk van elkaar te patchen. Voor PDF/A wordt dit een conformiteitsvereiste: ISO 19005 stelt XMP verplicht en verbiedt elke Info-eigenschap die zijn XMP-tegenhanger tegenspreekt
De outline-boom achter het bladwijzerpaneel
Wat een viewer toont als een bladwijzerpaneel is, in het bestand, een dubbel gekoppelde boom van dictionaries die de document-outline wordt genoemd. De catalogus wijst naar een root-outline-dictionary via /Outlines; de root wijst naar zijn eerste en laatste items op het hoogste niveau; en elk item is gekoppeld (threaded) aan zijn buren en zijn ouder (parent). Er is nergens een array van bladwijzers. De hele structuur wordt gereconstrueerd door verwijzingen te volgen, en dat is precies waarom een enkele verbroken link ervoor kan zorgen dat een hele tak uit het paneel verdwijnt zonder enige foutmelding
8 0 obj % de outline-root
<< /Type /Outlines /Count 4 /First 9 0 R /Last 9 0 R >>
endobj
9 0 obj % hoogste niveau: een hoofdstuk
<< /Title (Chapter 1: Results)
/Parent 8 0 R /Count 2
/First 12 0 R /Last 15 0 R >>
endobj
12 0 obj % eerste kind
<< /Title (Introduction)
/Parent 9 0 R /Next 15 0 R
/Dest [3 0 R /XYZ 72 720 0] >>
endobj
15 0 obj % tweede kind, laatste broer of zus (sibling)
<< /Title (Methodology)
/Parent 9 0 R /Prev 12 0 R
/Dest [3 0 R /Fit] >>
endobj
Lees de links en de invarianten worden duidelijk. Elk item wijst terug naar zijn /Parent. Siblings (broers en zussen) vormen een keten via /Prev en /Next, waarbij het eerste item /Prev weglaat en het laatste /Next weglaat. Een ouder benoemt zijn eerste en laatste kinderen via /First en /Last, en de kinderen daartussenin zijn alleen bereikbaar door de sibling-keten te bewandelen. Heb er één verkeerd en de fout is stil: een verouderde /Next kapt een hoofdstuk af, een ouder wiens /Last de keten niet beëindigt, laat items verweesd (orphaned) achter, en de viewer rendert wat hij maar kan bereiken
Het /Count-veld draagt een stukje status dat mensen verrast. Op de root en op elk uitgevouwen (expanded) item bevat het het aantal afstammelingen dat momenteel zichtbaar is; op een ingeklapt (collapsed) item is het een negatief getal waarvan de grootte aangeeft hoeveel afstammelingen zouden verschijnen bij het uitvouwen. Dus /Count is geen vast structureel feit over de boom, het is de opgeslagen open of gesloten status van het paneel, en een generator die het hard-codeert als een positief totaal heropent elke tak die de auteur bedoeld had om gesloten te laten
Elk item verdient zijn plaats door ergens naar te wijzen. De /Title is wat het paneel toont; de /Dest is waar een klik landt. Een bestemming (destination) kan inline zijn in het item, zoals hierboven, of een naam die wordt opgelost via de naam-dictionary van het document, wat de betere keuze is wanneer veel bladwijzers en links naar dezelfde plekken wijzen, omdat je een verplaatst doel op één plek kunt repareren. Een bibliotheek verbergt deze boom over het algemeen achter een outline-root-handle en methoden die kind-vermeldingen (child entries) toevoegen; in HotPDF stelt het document een OutlineRoot van het type THPDFDocOutlineObject beschikbaar en koppelt (threads) het de /Prev, /Next, /Parent en /Count links voor je naarmate je items toevoegt. Het is de moeite waard om daarvan te profiteren, want het handmatig onderhouden van die invarianten over bewerkingen heen is waar outlines kapot gaan
Bestemmingen: de grammatica van waar een klik naartoe gaat
Zowel bladwijzers als link-annotaties wijzen naar bestemmingen, en een bestemming is meer dan een paginanummer. Het is een array die een pagina-object benoemt en vervolgens, via een werkwoord in de tweede slot, specificeert hoe de viewer het moet inkaderen. De meest voorkomende en de meest misbruikte is /XYZ, in de vorm [page /XYZ left top zoom]. De drie operanden (operands) zijn onafhankelijk, en elk mag null zijn om te betekenen "laat dit zoals de lezer het had." Dus [page /XYZ null null null] springt naar de pagina zonder de scrollpositie of zoom aan te raken, wat meestal is wat je wilt van een "ga naar pagina"-link. De getallen zijn in standaard gebruikersruimte (default user space), gemeten vanaf linksonder met y toenemend naar boven, hetzelfde coördinatensysteem dat de pagina-inhoud gebruikt. Auteurs die vanuit schermlay-out aankomen, meten reflexmatig vanaf de bovenkant en sturen de lezer naar de verkeerde kant van de pagina
De /Fit-familie ruilt nauwkeurige positionering in voor veerkracht. [page /Fit] schaalt de hele pagina in het venster, [page /FitH top] past de paginabreedte in met een gegeven bovenrand (top edge), en [page /FitR l b r t] zoomt in op een rechthoek om het zicht te vullen. Omdat deze schaal berekenen uit paginageometrie in plaats van vaste coördinaten, doet een /Fit-bestemming nog steeds iets zinvols nadat het formaat van de pagina is gewijzigd, terwijl een /XYZ-bestemming met een ingebakken zoom de lezer naar de marge kan laten staren. Voor een inhoudsopgave (table of contents) blijft /FitH met de bovenste coördinaat van de sectie beter bruikbaar dan /XYZ met een gegokte zoom
Annotaties: alles interactiefs dat geen pagina-inhoud is
Een annotatie is een object dat over de pagina heen ligt zonder deel uit te maken van de content stream. Links, plaknotities (sticky notes), markeringen (highlights), formulierwidgets, bijlagepictogrammen (file-attachment icons), stempels: ze zijn allemaal annotaties, opgesomd in de /Annots-array van de pagina waarop ze zich bevinden. Het verwijderen van een annotatie uit die array verwijdert deze van de pagina, ook al blijft de onderliggende inhoud onaangetast. Dat is het hele punt: annotaties zijn een bewerkingslaag, gescheiden van de markeringen waar ze overheen liggen
Elke annotatie deelt een kleine ruggengraat (spine). /Subtype benoemt de soort, /Rect geeft het begrenzingsvak (bounding box) in paginacoördinaten, en /Contents bevat tekst die tevens dient als de toegankelijke (accessible) beschrijving. De link-annotatie is de case die het bestuderen waard is, omdat deze in twee vormen komt: een kale bestemming, en een actie
12 0 obj % link naar een bestemming
<< /Type /Annot /Subtype /Link
/Rect [100 200 300 250]
/Border [0 0 0]
/Dest [5 0 R /XYZ null null null] >>
endobj
13 0 obj % link die een actie uitvoert
<< /Type /Annot /Subtype /Link
/Rect [50 50 200 100]
/Border [0 0 0]
/A << /Type /Action /S /URI /URI (https://www.example.com) >> >>
endobj
De /Rect is een hotspot; erop klikken stuurt de lezer naar de bestemming, waarbij dezelfde grammatica wordt hergebruikt als die de outline gebruikt. De /Border [0 0 0] verricht echt werk, en onderdrukt de lelijke standaard rechthoek die viewers rond links tekenen. De tweede vorm verwisselt de kale /Dest voor een /A actie, waarvan het /S subtype het gedrag selecteert: /GoTo binnen dit bestand, /GoToR voor een ander bestand, /URI voor een webadres, /Launch om een extern programma uit te voeren. Dat laatste roept wantrouwen op. Een /Launch die een uitvoerbaar bestand (executable) start, is het gedrag dat PDF's een malware-vector maakt, dus conforme viewers blokkeren het of vragen er luidruchtig om (prompt loudly) en de link faalt voor de meeste lezers. Grijp naar /URI en /GoTo en laat /Launch met rust
Opmaak-annotaties (markup annotations) zoals markeringen en plaknotities, en vorm-annotaties zoals /Square, voegen een rimpeling toe: hun uiterlijk op het scherm wordt niet geïmpliceerd door hun type. Een viewer rendert zijn eigen versie, tenzij je het uiterlijk vastzet (pin) met een uiterlijkstream (appearance stream), de /AP-vermelding, die verwijst naar een vorm XObject (form XObject) met de tekenoperatoren. Sla het over en dezelfde markering kan er in twee readers anders uitzien, of voor en na een bewerkingsronde (editor round-trip). Voor alles waarvan het exacte uiterlijk deel uitmaakt van het document, lever je de /AP aan. Bestandsbijlagen hergebruiken overigens dezelfde machinerie: een ingesloten bestandsstream (embedded file stream) en een bestandsspecificatie-dictionary, aan de oppervlakte gebracht als een /FileAttachment-annotatie of via de /EmbeddedFiles naamboom (name tree) onder de /Names van de catalogus
Waar deze laag breekt, en hoe je dit opvangt
De terugkerende fout in dit alles is de zwevende verwijzing (dangling reference). Bladwijzers verschijnen niet meer wanneer de catalogus geen /Outlines-vermelding heeft of een sibling-keten in het midden van de boom breekt; metadata wordt genegeerd wanneer de XMP-stream zijn /Type /Metadata /Subtype /XML markering mist of de xpacket-wrapper verkeerd gevormd is. In alle gevallen is de pagina-inhoud in orde, dus een terloopse opening ziet er correct uit en het defect komt pas aan de oppervlakte in het paneel dat niemand heeft gecontroleerd
Twee goedkope gewoonten vangen het meeste op. Open het voltooide bestand in een echte viewer en klik door het bladwijzerpaneel en een steekproef (sample) van links, wat de verwijzingsgraaf test op de manier waarop een lezer dat zal doen. Lees vervolgens de metadata terug met een afzonderlijke tool en bevestig dat de Info-dictionary en de XMP het met elkaar eens zijn, de ene onenigheid die geen hoeveelheid klikken zal onthullen. Genereer deze laag via een bibliotheek die de boekhouding van de links bezit, en de meeste van deze vallen (traps) gaan nooit open. De HotPDF Delphi Component voor Delphi en C++Builder stelt de outline-, annotatie- en metadata-structuren beschikbaar via API's op documentniveau, zodat je de bladwijzerhiërarchie en de links beschrijft en het de verwijzingen laat koppelen (thread). Wat betreft het objectmodel waaraan deze structuren zich hechten, behandelt het technische overzicht van de PDF-bestandsstructuur de catalogus en de kruisverwijzingstabel (cross-reference table) waarvan ze afhankelijk zijn