Teknisk artikel

PDF-metadata, outlines och kommentarer förklarade

Ta bort sidbeskrivningarna och du lämnas med ett tunt lager av struktur som ingen skriver ut men som varje läsare, indexerare och arkivsystem är beroende av. Ett sidobjekt vet ingenting om vilket kapitel det tillhör, vilken författare som skrev det eller vilken fotnot som länkar någon annanstans. Den kunskapen lever en nivå upp, i tre strukturer fästa vid dokumentkatalogen: metadataströmmen, outline-trädet (outline tree) och arrayerna för kommentarer (annotations) per sida. De delar ett drag som gör dem lätta att få fel. Inga bär synliga märken på sidan, så en fil kan renderas perfekt och ändå sakna sina bokmärken, motsäga sitt eget författarfält, eller peka en länk mot ett sidobjekt som inte längre existerar

Detta är lagret ett PDF-bibliotek exponerar som dokumentegenskaper, bokmärkes-API:er och länk- eller kommentarssamtal, och lagret en sökrobot (search crawler) läser för att avgöra vad ditt dokument handlar om. Objektmodellen under den täcks i genomgången av PDF-dokumentstruktur. Här ligger fokus strikt på vad som hänger från katalogen

Alla tre strukturer fäster vid katalogen. En komplett katalog som kopplar ihop dem ser ut så här:

1 0 obj
<< /Type /Catalog
   /Pages 2 0 R
   /Outlines 3 0 R
   /Names << /EmbeddedFiles 4 0 R >>
   /Metadata 5 0 R
>>
endobj

Fyra poster, fyra oberoende delsystem. /Pages är det synliga dokumentet; /Outlines är bokmärkesträdet; /Metadata pekar på XMP-strömmen; /Names når dokumentets övergripande namn-ordlista (name dictionary), som bland annat innehåller inbäddade filbilagor. Var och en är valfri, och en läsare som inte hittar någon av dem visar fortfarande sidorna. Denna valfrihet är exakt anledningen till varför navigeringslagret är det första som ruttnar när en fil redigeras av verktyg som bara förstår sidor

Två metadatalager som inte är överens

PDF bär på dokumentmetadata på två ställen samtidigt, och problemen börjar när de säger olika saker. Den ursprungliga mekanismen är dokumentinformations-ordlistan, refererad av /Info i trailern: en platt uppsättning nyckel-värde-par för /Title, /Author, /Subject, /Keywords, /Creator, /Producer och de två datumen. Den är enkel och varje visare läser den. PDF 2.0 nedvärderar (deprecates) det mesta av den till förmån för den andra mekanismen, XMP-metadataströmmen

XMP är ett fristående XML-dokument, skrivet i RDF, lagrat som en ström katalogen når genom /Metadata och markerat /Type /Metadata /Subtype /XML. Till skillnad från Info-ordlistan som ligger begravd inuti PDF-objektstrukturen, är ett XMP-paket utformat för att kunna extraheras och tolkas på egen hand av verktyg som inte vet något om PDF. Här är ett representativt paket:

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

Tre detaljer i det blocket avgör huruvida metadatan överlever kontakt med riktiga verktyg. xpacket-behandlingsinstruktionerna är inte dekoration: de ramar in paketet så att en extraherare kan hitta det inuti en större byteström, och en skrivare som utelämnar den avslutande <?xpacket end="w"?> producerar en fil som öppnas bra men fäller strikta validatorer. Egenskapernas datatyper spelar också roll. dc:title är ett språkalternativ insvept i rdf:Alt, medan dc:creator är en ordnad lista och tar rdf:Seq; att avge (emit) någotdera som en naken textnod är det enskilt vanligaste XMP-misstaget, vilket tolereras av de flesta visare fram till den som inte gör det. Namnrymdsprefixen (namespace prefixes) är konventionella, men URI:erna de binder till är normativa: en tolkare (parser) nycklar sig till (keys off) URI:n, inte prefixet

Den hårda regeln med två lager är att de måste vara överens. Om /Info säger att författaren är en person och dc:creator namnger en annan, har du levererat ett dokument som svarar på samma fråga på två sätt, och vilket svar som vinner beror på vilket fält det konsumerande verktyget läser. Ett bibliotek skriver vanligtvis båda åt dig, men i det ögonblick du redigerar en för hand, eller slår samman filer från olika generatorer, glider de två isär. Behandla Info-ordlistan som föråldrad kompatibilitet (legacy compatibility) och XMP som sanningens källa, och återskapa båda från en enda uppsättning värden snarare än att korrigera dem oberoende av varandra. För PDF/A blir detta ett överensstämmelsekrav: ISO 19005 kräver XMP och förbjuder all Info-egenskap som motsäger dess XMP-motsvarighet

Outline-trädet bakom bokmärkespanelen

Vad en visare visar som en bokmärkespanel är, i filen, ett dubbellänkat träd av ordlistor som kallas dokument-outline (document outline). Katalogen pekar på en rot-outline-ordlista genom /Outlines; roten pekar på dess första och sista toppnivåobjekt; och varje objekt är trådat till sina grannar och sin förälder. Det finns ingen array av bokmärken någonstans. Hela strukturen återskapas genom att följa referenser, vilket är precis varför en enda bruten länk kan få en hel gren att försvinna från panelen utan något fel

8 0 obj                                    % outline-roten
<< /Type /Outlines /Count 4 /First 9 0 R /Last 9 0 R >>
endobj
9 0 obj                                    % toppnivå: ett kapitel
<< /Title (Chapter 1: Results)
   /Parent 8 0 R /Count 2
   /First 12 0 R /Last 15 0 R >>
endobj
12 0 obj                                   % första barnet
<< /Title (Introduction)
   /Parent 9 0 R /Next 15 0 R
   /Dest [3 0 R /XYZ 72 720 0] >>
endobj
15 0 obj                                   % andra barnet, sista syskonet
<< /Title (Methodology)
   /Parent 9 0 R /Prev 12 0 R
   /Dest [3 0 R /Fit] >>
endobj

Läs länkarna och invarianterna blir uppenbara. Varje post pekar tillbaka till sin /Parent. Syskon bildar en kedja genom /Prev och /Next, där den första posten utelämnar /Prev och den sista utelämnar /Next. En förälder namnger sina första och sista barn genom /First och /Last, och barnen däremellan är endast nåbara genom att vandra syskonkedjan. Får du ett fel är misslyckandet tyst: en inaktuell /Next trunkerar ett kapitel, en förälder vars /Last inte avslutar kedjan lämnar poster föräldralösa, och visaren renderar vad den än kan nå

/Count-fältet bär på en bit tillstånd som förvånar folk. På roten och på valfri expanderad post rymmer det antalet ättlingar som för närvarande är synliga; på en hopfälld post är det ett negativt nummer vars storlek är hur många ättlingar som skulle dyka upp vid expandering. Så /Count är inte ett fast strukturellt faktum om trädet, det är panelens sparade öppna eller stängda tillstånd, och en generator som hårdkodar den som en positiv total öppnar åter varje gren som författaren avsåg att lämna stängd

Varje post förtjänar sin plats genom att peka någonstans. /Title är vad panelen visar; /Dest är var ett klick landar. En destination (destination) kan vara inbäddad i posten, som ovan, eller ett namn som löses (resolves) genom dokumentets namn-ordlista, vilket är ett bättre val när många bokmärken och länkar riktar sig mot samma ställen, eftersom du då kan fixa ett flyttat mål på ett ställe. Ett bibliotek döljer generellt detta träd bakom ett outline-rot-handtag och metoder som lägger till barnposter; i HotPDF exponerar dokumentet en OutlineRoot av typen THPDFDocOutlineObject och trådar /Prev-, /Next-, /Parent- och /Count-länkarna åt dig när du lägger till poster. Det är värt att dra nytta av, eftersom det är vid handupprätthållandet av de invarianterna över redigeringar som outlines går sönder

Destinationer: grammatiken för vart ett klick bär dig

Både bokmärken och länkkommentarer (link annotations) pekar på destinationer, och en destination är mer än ett sidnummer. Det är en array som namnger ett sidobjekt och sedan anger, genom ett verb i det andra facket, hur visaren ska rama in den. Den vanligaste och mest missbrukade är /XYZ, med formatet [page /XYZ left top zoom]. Dess tre operander är oberoende, och var och en kan vara null för att betyda "lämna detta som läsaren hade det." Så [page /XYZ null null null] hoppar till sidan utan att röra rullningsposition eller zoom, vilket oftast är vad du vill ha från en "gå till sida"-länk. Siffrorna är i standardanvändarrymden (default user space), mätta från nedre vänstra hörnet med y ökande uppåt, samma koordinatsystem som sidinnehållet använder. Författare som anländer från skärmlayout (screen layout) mäter reflexmässigt från toppen och skickar läsaren till fel ände av sidan

/Fit-familjen byter exakt positionering mot motståndskraft. [page /Fit] skalar in hela sidan i fönstret, [page /FitH top] anpassar sig till sidbredden med en given övre kant, och [page /FitR l b r t] zoomar en rektangel för att fylla vyn. Eftersom dessa beräknar skala från sidgeometri snarare än fasta koordinater, gör en /Fit-destination fortfarande det vettiga efter att sidan har ändrat storlek, medan en /XYZ-destination med en inbakad zoom kan lämna läsaren stirrandes på marginalen. För en innehållsförteckning åldras /FitH med sektionens toppkoordinat bättre än /XYZ med en gissad zoom

Kommentarer: allt interaktivt som inte är sidinnehåll

En kommentar (annotation) är ett objekt som ligger ovanpå sidan utan att vara en del av dess innehållsström. Länkar, klisterlappar (sticky notes), markeringar (highlights), formulärwidgets, filbifogningsikoner, stämplar: alla är kommentarer, listade i /Annots-arrayen på den sida de sitter på. Att ta bort en kommentar från den arrayen tar bort den från sidan även om det underliggande innehållet är orört. Det är hela poängen: kommentarer är ett redigeringslager, separat från de märken de sitter över

Varje kommentar delar en liten ryggrad. /Subtype namnger sorten, /Rect ger dess begränsningsbox (bounding box) i sidkoordinater, och /Contents håller text som dubblerar som den tillgängliga beskrivningen (accessible description). Länkkommentaren är det fall som är värt att studera, eftersom den kommer i två former: en blottad (bare) destination och en handling (action)

12 0 obj                                    % länk till en destination
<< /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                                    % länk som kör en handling
<< /Type /Annot /Subtype /Link
   /Rect [50 50 200 100]
   /Border [0 0 0]
   /A << /Type /Action /S /URI /URI (https://www.example.com) >> >>
endobj

/Rect är en hotspot; ett klick inuti den skickar läsaren till destinationen och återanvänder samma grammatik som outlinen använder. /Border [0 0 0] gör verkligt arbete, och undertrycker (suppressing) den fula standardrektangeln som visare ritar runt länkar. Den andra formen byter ut den nakna /Dest mot en /A-handling (action), vars /S-undertyp (subtype) väljer beteendet: /GoTo inom denna fil, /GoToR för en annan fil, /URI för en webbadress, /Launch för att köra ett externt program. Det sistnämnda förtjänar misstänksamhet. En /Launch som startar en exekverbar fil är beteendet som gör PDF-filer till en vektor för skadlig kod, så överensstämmande visare blockerar den eller varnar högljutt, och länken misslyckas för de flesta läsare. Välj /URI och /GoTo och lämna /Launch ifred

Uppmärkningskommentarer (markup annotations) som markeringar och klisterlappar, och formkommentarer som /Square, lägger till ett problem: deras utseende på skärmen antyds inte av deras typ. En visare renderar sin egen version om du inte fäster utseendet med en utseendeström (appearance stream), /AP-posten, som refererar till ett form XObject som håller ritningsoperatorerna. Hoppa över det och samma markering kan se olika ut i två läsare, eller före och efter en redigeringsrunda (editor round-trip). För allt vars exakta utseende är en del av dokumentet, tillhandahåll /AP. Filbilagor återanvänder, i förbigående, samma maskineri: en inbäddad filström och en filspecifikationsordlista, som dyker upp (surfaced) antingen som en /FileAttachment-kommentar eller genom namnträdet /EmbeddedFiles under katalogens /Names

Var detta lager går sönder, och hur man fångar det

Det återkommande misslyckandet över allt detta är den hängande referensen (dangling reference). Bokmärken slutar visas när katalogen inte har någon /Outlines-post eller när en syskonkedja bryts mitt i trädet; metadata ignoreras när XMP-strömmen saknar sin /Type /Metadata /Subtype /XML-markering eller när xpacket-omslaget är missbildat. I varje fall är sidinnehållet bra, så en tillfällig öppning ser korrekt ut och defekten dyker upp (surfaces) endast i panelen som ingen kollade

Två billiga vanor fångar det mesta. Öppna den färdiga filen i en riktig visare och klicka dig igenom bokmärkespanelen och ett urval av länkar, vilket utövar referensgrafen på det sätt som en läsare kommer att göra. Läs sedan tillbaka metadatan med ett separat verktyg och bekräfta att Info-ordlistan och XMP är överens, den oenighet som ingen mängd klickande avslöjar. Generera detta lager genom ett bibliotek som äger länkbokföringen så att de flesta av dessa fällor aldrig öppnas. HotPDF Component för Delphi och C++Builder exponerar outline-, kommentars- och metadatastrukturerna genom API:er på dokumentnivå, så du beskriver bokmärkeshierarkin och länkarna och låter den tråda referenserna. För objektmodellen dessa strukturer är fästa vid, täcker den tekniska översikten av PDF-filstruktur katalogen och korsreferenstabellen de är beroende av