Kuori pois sivukuvaukset ja jäljelle jää ohut rakennekerros, jota kukaan ei tulosta, mutta josta jokainen lukija, indeksoija ja arkistointijärjestelmä on riippuvainen. Sivuobjekti ei tiedä mitään siitä luvusta, johon se kuuluu, kirjoittajasta, joka sen kirjoitti, tai alaviitteestä, joka linkittää muualle. Se tieto asuu yhtä tasoa ylempänä, kolmessa rakenteessa, jotka on liitetty asiakirjan catalogiin: metatietovirrat, jäsennyspuu (outline tree) ja sivukohtaiset annotaatiotaulukot. Niillä on yhteinen piirre, joka tekee niistä helppoja mokata. Mikään niistä ei kanna näkyviä merkkejä sivulla, joten tiedosto voi renderöityä täydellisesti ja silti menettää kirjanmerkkinsä, olla ristiriidassa oman kirjoittajakenttänsä kanssa tai osoittaa linkillä sivuobjektiin, jota ei enää ole olemassa
Tämä on se kerros, jonka PDF-kirjasto paljastaa asiakirjan ominaisuuksina, kirjanmerkki-API:na sekä linkki- tai annotaatiokutsuina, ja se kerros, jonka hakurobotti (search crawler) lukee päättääkseen, mistä asiakirjassasi on kyse. Tämän alla oleva objektimalli on käsitelty läpikäynnissä PDF-asiakirjan rakenteesta. Tässä keskitytään tiukasti vain siihen, mikä riippuu catalogista
Kaikki kolme rakennetta kiinnittyvät catalogiin. Täydellinen catalog, joka yhdistää ne toisiinsa, näyttää tältä:
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
/Outlines 3 0 R
/Names << /EmbeddedFiles 4 0 R >>
/Metadata 5 0 R
>>
endobj
Neljä tietuetta, neljä itsenäistä alijärjestelmää. /Pages on näkyvä asiakirja; /Outlines on kirjanmerkkipuu; /Metadata osoittaa XMP-virtaan; /Names tavoittaa asiakirjan laajuisen (document-wide) nimisanakirjan, joka pitää sisällään muun muassa upotetut tiedostoliitteet. Jokainen on valinnainen, ja lukija, joka ei löydä niistä mitään, näyttää silti sivut. Tämä valinnaisuus on juuri se syy, miksi navigointikerros on ensimmäinen asia, joka mätänee, kun tiedostoa muokataan työkaluilla, jotka ymmärtävät vain sivuja
Kaksi metatietovarastoa, jotka ovat eri mieltä
PDF kantaa asiakirjan metatietoja kahdessa paikassa yhtaikaa, ja ongelmat alkavat silloin, kun ne sanovat eri asioita. Alkuperäinen mekanismi on asiakirjan Info-sanakirja, johon trailerin /Info viittaa: litteä joukko avain-arvo-pareja (key-value pairs) kentille /Title, /Author, /Subject, /Keywords, /Creator, /Producer ja kahdelle päivämäärälle. Se on yksinkertainen ja jokainen katseluohjelma lukee sen. PDF 2.0 hylkää (deprecates) suurimman osan siitä toisen mekanismin, XMP-metatietovirran, hyväksi
XMP on itsenäinen XML-asiakirja, kirjoitettu RDF:nä, tallennettu virtana, jonka catalog saavuttaa /Metadata-kautta ja joka on merkitty /Type /Metadata /Subtype /XML. Toisin kuin PDF-objektirakenteen sisään haudattu Info-sanakirja, XMP-paketti on suunniteltu sellaiseksi, että PDF:stä mitään tietämättömät työkalut voivat irrottaa ja jäsentää sen itsekseen. Tässä on edustava paketti:
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
Kolme yksityiskohtaa tuossa lohkossa ratkaisevat, selviääkö metatieto kosketuksesta todellisten työkalujen kanssa. xpacket-prosessointiohjeet eivät ole koristeita: ne kehystävät paketin, jotta irrottaja voi löytää sen suuremman tavuvirran sisältä, ja kirjoittaja, joka jättää pois sulkevan <?xpacket end="w"?>:n, tuottaa tiedoston, joka aukeaa hienosti, mutta kompastuttaa tiukat validaattorit. Myös ominaisuuksien tietotyypit merkitsevät. dc:title on kielivaihtoehto (language alternative) käärittynä rdf:Alt-tunnisteeseen, kun taas dc:creator on järjestetty luettelo ja ottaa rdf:Seq:n; kumman tahansa lähettäminen pelkkänä tekstisolmuna on yleisin XMP-virhe, jota useimmat katseluohjelmat sietävät juuri siihen asti, kunnes vastaan tulee se, joka ei siedä. Nimiavaruus-etuliitteet (namespace prefixes) ovat sovittuja käytäntöjä (conventional), mutta URI:t, joihin ne sitoutuvat, ovat normatiivisia: jäsennin tunnistaa URI:n, ei etuliitettä
Kova sääntö kahden varaston kanssa on, että niiden täytyy olla samaa mieltä. Jos /Info sanoo kirjoittajan olevan yksi henkilö ja dc:creator nimeää toisen, olet toimittanut asiakirjan, joka vastaa samaan kysymykseen kahdella tavalla, ja se, kumpi vastaus voittaa, riippuu siitä, kumpaa kenttää kuluttava työkalu lukee. Kirjasto yleensä kirjoittaa molemmat puolestasi, mutta sillä hetkellä kun muokkaat yhtä käsin, tai yhdistät tiedostoja eri generaattoreista, ne ajautuvat erilleen. Käsittele Info-sanakirjaa legacy-yhteensopivuutena ja XMP:tä totuuden lähteenä (source of truth), ja luo molemmat uudelleen yhdestä arvojoukosta sen sijaan, että paikkaisit niitä itsenäisesti. PDF/A:n kohdalla tästä tulee vaatimustenmukaisuusvaatimus: ISO 19005 määrää XMP:n pakolliseksi ja kieltää minkä tahansa Info-ominaisuuden, joka on ristiriidassa sen XMP-vastineen kanssa
Jäsennyspuu kirjanmerkkipaneelin takana
Se, minkä katseluohjelma näyttää kirjanmerkkipaneelina, on tiedostossa kaksisuuntaisesti linkitetty sanakirjapuu (doubly linked tree of dictionaries), jota kutsutaan asiakirjan jäsennykseksi (document outline). Catalog osoittaa juuren jäsennyssanakirjaan /Outlines kautta; juuri osoittaa sen ensimmäiseen ja viimeiseen ylätason (top-level) kohteeseen; ja jokainen kohde (item) on langoitettu naapureihinsa ja sen vanhempaan (parent). Missään ei ole mitään taulukkoa kirjanmerkeistä. Koko rakenne rakennetaan uudelleen seuraamalla viittauksia, mikä on täsmälleen syy siihen, miksi yksi katkennut linkki voi saada koko oksan katoamaan paneelista ilman mitään virhettä
8 0 obj % the outline root
<< /Type /Outlines /Count 4 /First 9 0 R /Last 9 0 R >>
endobj
9 0 obj % top-level: a chapter
<< /Title (Chapter 1: Results)
/Parent 8 0 R /Count 2
/First 12 0 R /Last 15 0 R >>
endobj
12 0 obj % first child
<< /Title (Introduction)
/Parent 9 0 R /Next 15 0 R
/Dest [3 0 R /XYZ 72 720 0] >>
endobj
15 0 obj % second child, last sibling
<< /Title (Methodology)
/Parent 9 0 R /Prev 12 0 R
/Dest [3 0 R /Fit] >>
endobj
Lue linkit, ja invariantit tulevat ilmeisiksi. Jokainen kohde osoittaa takaisin /Parent-vanhempaansa. Sisarukset (siblings) muodostavat ketjun /Prev- ja /Next-kautta, siten että ensimmäinen kohde jättää pois /Prev:n ja viimeinen jättää pois /Next:n. Vanhempi nimeää ensimmäisen ja viimeisen lapsensa /First- ja /Last-kautta, ja lapset siinä välissä ovat saavutettavissa vain kävelemällä sisarusketjua pitkin. Sössi yksi ja epäonnistuminen on hiljainen: vanhentunut /Next katkaisee luvun, vanhempi, jonka /Last ei päätä ketjua, jättää kohteita orvoiksi, ja katseluohjelma renderöi kaiken mihin se vain yltää
/Count-kenttä kantaa mukanaan palan tilaa, joka yllättää ihmiset. Juuressa ja missä tahansa avatussa kohteessa (expanded item) se pitää sisällään niiden jälkeläisten lukumäärän, jotka ovat sillä hetkellä näkyvissä; suljetussa kohteessa se on negatiivinen luku, jonka suuruusluokka on se, kuinka monta jälkeläistä tulisi näkyviin avattaessa. Joten /Count ei ole kiinteä rakenteellinen fakta puusta, se on paneelin tallennettu auki- tai kiinni-tila, ja generaattori, joka kovakoodaa (hard-codes) sen positiiviseksi kokonaismääräksi, avaa uudelleen jokaisen oksan, jonka kirjoittaja tarkoitti jättää kiinni
Jokainen kohde ansaitsee paikkansa osoittamalla jonnekin. /Title on se, mitä paneeli näyttää; /Dest on se, minne klikkaus laskeutuu. Kohde (destination) voi olla inline kohteessa (item), kuten yllä, tai nimi, joka ratkeaa asiakirjan nimisanakirjan kautta, mikä on parempi valinta, kun monet kirjanmerkit ja linkit kohdistuvat samoihin paikkoihin, koska korjaat siirretyn kohteen yhdessä paikassa. Kirjasto yleensä piilottaa tämän puun outline-juuren kahvan (outline-root handle) taakse ja menetelmiin, jotka lisäävät lapsitietueita; HotPDF:ssä asiakirja paljastaa OutlineRoot:n tyyppiä THPDFDocOutlineObject ja langoittaa /Prev-, /Next-, /Parent- ja /Count-linkit puolestasi sitä mukaa kun lisäät kohteita. Siitä kannattaa ottaa hyöty irti, sillä näiden invarianttien ylläpitäminen käsin muokkausten välillä on paikka, jossa jäsennykset (outlines) rikkoutuvat
Kohteet (Destinations): kielioppi sille, minne klikkaus menee
Sekä kirjanmerkit että linkkiannotaatiot osoittavat kohteisiin, ja kohde (destination) on enemmän kuin sivunumero. Se on taulukko (array), joka nimeää sivuobjektin ja sen jälkeen määrittää verbin avulla toisessa paikassa, kuinka katseluohjelman tulisi kehystää (frame) se. Yleisin ja väärinkäytetyin on /XYZ muodossa [page /XYZ left top zoom]. Sen kolme operandia ovat riippumattomia, ja mikä tahansa niistä voi olla null tarkoittaen "jätä tämä sellaiseksi kuin lukijalla se oli". Eli [page /XYZ null null null] hyppää sivulle koskematta vierityspositioon (scroll position) tai zoomiin, mikä on yleensä sitä mitä haluat "siirry sivulle" -linkiltä. Luvut ovat oletuskäyttäjätilassa (default user space), mitattuna vasemmasta alakulmasta y:n kasvaessa ylöspäin, siis samassa koordinaatistossa, jota sivun sisältö käyttää. Kirjoittajat, jotka tulevat ruutuasettelun parista, mittaavat refleksinomaisesti ylhäältä ja lähettävät lukijan sivun väärään päähän
/Fit-perhe vaihtaa tarkan asemoinnin kestävyyteen. [page /Fit] skaalaa koko sivun ikkunaan, [page /FitH top] sovittaa sivun leveyden tietyllä yläreunalla, ja [page /FitR l b r t] zoomaa suorakulmion täyttämään näkymän. Koska nämä laskevat skaalauksen sivun geometriasta eivätkä kiinteistä koordinaateista, /Fit-kohde tekee edelleen järkevän asian sen jälkeen kun sivun kokoa on muutettu, kun taas /XYZ-kohde sisäänleivotulla (baked-in) zoomilla voi jättää lukijan tuijottamaan marginaalia. Sisällysluettelolle, /FitH osion yläkoordinaatilla vanhenee paremmin kuin /XYZ arvatulla zoomilla
Annotaatiot: kaikki interaktiivinen, mikä ei ole sivun sisältöä
Annotaatio on objekti, joka peittää (overlays) sivun olematta osa sen sisältövirtaa (content stream). Linkit, muistilaput (sticky notes), korostukset (highlights), lomakewidgetit (form widgets), tiedostoliitteiden kuvakkeet, leimat: kaikki ovat annotaatioita, lueteltuna /Annots-taulukossa sillä sivulla, jolla ne sijaitsevat. Annotaation poistaminen siitä taulukosta poistaa sen sivulta, vaikka alla oleva sisältö on koskematon. Tämä on koko asian ydin: annotaatiot ovat muokkauskerros, erillään niistä merkeistä (marks), joiden päällä ne istuvat
Jokainen annotaatio jakaa pienen selkärangan (spine). /Subtype nimeää tyypin, /Rect antaa sen rajauslaatikon (bounding box) sivun koordinaateissa, ja /Contents pitää sisällään tekstin, joka toimii samalla saavutettavana (accessible) kuvauksena. Linkkiannotaatio on tapaus, jota kannattaa tutkia, koska sitä on kahdessa muodossa: pelkkä kohde (bare destination), ja toiminto (action)
12 0 obj % link to a 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 % link that runs an action
<< /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 on aktiivinen alue (hotspot); sen sisällä klikkaaminen lähettää lukijan kohteeseen, käyttäen uudelleen samaa kielioppia, jota jäsennys (outline) käyttää. /Border [0 0 0] tekee todellista työtä, tukahduttaen sen ruman oletussuorakulmion, jonka katseluohjelmat piirtävät linkkien ympärille. Toinen muoto vaihtaa pelkän /Dest:n /A-toimintoon (action), jonka /S-alatyyppi (subtype) valitsee käyttäytymisen: /GoTo tämän tiedoston sisällä, /GoToR toiseen tiedostoon, /URI verkko-osoitteeseen, /Launch suorittaakseen ulkoisen ohjelman. Tuo viimeinen ansaitsee epäilyksen. /Launch, joka käynnistää suoritettavan ohjelman, on se käyttäytyminen, joka tekee PDF-tiedostoista haittaohjelmien vektorin, joten standardinmukaiset katseluohjelmat estävät sen tai varoittavat kovaäänisesti ja linkki epäonnistuu useimmilla lukijoilla. Kurkota /URI ja /GoTo:n suuntaan ja jätä /Launch rauhaan
Merkintäannotaatiot (markup annotations), kuten korostukset ja muistilaput, sekä muotoannotaatiot, kuten /Square, lisäävät pienen koukeron: niiden ulkonäkö ruudulla ei ole tyyppinsä perusteella implisiittinen. Katseluohjelma renderöi oman versionsa, ellet kiinnitä ulkoasua ulkoasuvirralla (appearance stream), /AP-tietueella, joka viittaa form XObjectiin, joka pitää sisällään piirto-operaattorit. Ohita se ja sama korostus voi näyttää erilaiselta kahdessa eri lukijassa, tai ennen ja jälkeen editoinnin. Kaikelle, minkä tarkka ulkonäkö on osa asiakirjaa, tarjoa /AP. Tiedostoliitteet, muuten, käyttävät uudelleen tätä samaa koneistoa: upotettu tiedostovirta (embedded file stream) ja tiedostospesifikaatio-sanakirja (file specification dictionary), jotka on tuotu pintaan joko /FileAttachment-annotaationa tai catalogin /Names-määrityksen alla olevan /EmbeddedFiles-nimipuun kautta
Missä tämä kerros hajoaa, ja kuinka saada se kiinni
Toistuva vika näissä kaikissa on roikkuva viittaus (dangling reference). Kirjanmerkit lakkaavat ilmestymästä, kun catalogissa ei ole /Outlines-tietuetta tai sisarusketju (sibling chain) katkeaa kesken puun; metatiedot jätetään huomiotta, kun XMP-virrasta puuttuu sen /Type /Metadata /Subtype /XML-merkintä tai xpacket-kääre on epämuodostunut. Joka kerta sivun sisältö on kunnossa, joten nopea vilkaisu avatessa näyttää oikealta ja vika nousee pintaan vain paneelissa, jota kukaan ei tarkistanut
Kaksi halpaa tapaa saavat kiinni suurimman osan siitä. Avaa valmis tiedosto todellisessa katseluohjelmassa ja klikkaa läpi kirjanmerkkipaneeli ja näyte linkeistä, mikä harjoittaa viitegraafia (reference graph) samalla tavalla kuin lukija tulee sen tekemään. Lue sitten metatiedot takaisin erillisellä työkalulla ja vahvista, että Info-sanakirja ja XMP ovat samaa mieltä, se ainoa erimielisyys, jota mikään määrä klikkailua ei paljasta. Luo tämä kerros kirjaston kautta, joka omistaa linkkien kirjanpidon (link bookkeeping), ja suurin osa näistä ansoista ei koskaan aukea. HotPDF Component Delphille ja C++Builderille paljastaa jäsennys-, annotaatio- ja metatietorakenteet asiakirjatason API:en kautta, joten sinä kuvailet kirjanmerkkihierarkian ja linkit ja annat sen langoittaa viittaukset. Sille objektimallille, johon nämä rakenteet kiinnittyvät, tekninen katsaus PDF-tiedoston rakenteeseen kattaa catalogin ja ristiviitetaulukon (cross-reference table), joista ne ovat riippuvaisia