PDF on ytimeltään vain pelkkä tekstitiedosto. Avaa useimmat tiedostot heksaeditorissa ja yläosa on luettavissa: versiokommentti, sitten sarja numeroituja objekteja, pieni hakemisto ja aivan pohjalla oleva osoitin, joka kertoo lukijalle mistä aloittaa. Riisu pakkaus ja muoto on riittävän lähestyttävä, jotta voit kirjoittaa toimivan asiakirjan tekstieditorissa ja saada katseluohjelman avaamaan sen. Tämän tekeminen kerran opettaa sinulle enemmän siitä, miten PDF pysyy koossa, kuin mikään määrä spesifikaation lukemista, koska joudut johdottamaan objektit toisiinsa käsin, eikä tiedosto suostu avautumaan ennen kuin saat johdotuksen oikein
Tämä läpikäynti rakentaa pienimmän PDF:n, joka todella renderöi jotain: yhden sivun, sanat "Hello, World!" sisäänrakennetulla fontilla, US Letter -paperilla. Valmis tiedosto vaatii täsmälleen viisi objektia ja muutaman rivin kirjanpitoa niiden ympärille. Kirjoitamme objektit ensin, sitten kokoamme otsikon (header), ristiviitetaulukon (cross-reference table) ja trailerin, jotka sitovat ne tiedostoksi, jonka lukija hyväksyy
Viisi objektia, joita katseluohjelma vaatii
Lukija ei lue PDF:ää ylhäältä alas etsien sisältöä. Se aloittaa trailerista, seuraa viitettä asiakirjan luetteloon (document catalog) ja kulkee objektien ketjua siitä eteenpäin. Jokaisen ketjun objektin on oltava olemassa, tai avaus epäonnistuu. Yksisivuisessa asiakirjassa ketju on lyhyt, ja jokaisella linkillä on yksi tehtävä:
- Catalog on juuri. Se on objekti, johon traileri osoittaa, ja sen ainoa pakollinen tietue tässä on viittaus sivupuuhun (page tree)
- Pages on sivupuun solmu. Se listaa asiakirjan sivut ja ilmoittaa niiden määrän
- Page kuvaa yhtä fyysistä sivua: sen koon, sen käyttämät resurssit, ja minkä sisältövirran (content stream) avulla se piirretään
- Content stream sisältää piirto-operaattorit, postfix-komennot, jotka sijoittavat tekstiä ja grafiikkaa kyseiselle sivulle
- Font ilmoittaa kirjasintyypin, johon sisältövirta viittaa. Käytä yhtä 14 vakiokirjasimesta, niin sinun ei tarvitse upottaa mitään
Jokainen objekti on numeroitu ja osoitteistettavissa. Epäsuora objekti kirjoitetaan muodossa N 0 obj ... endobj, missä N on objektin numero ja 0 sen sukupolvinumero (generation number) (aina 0 tuoreeltaan kirjoittamassasi tiedostossa). Missä tahansa muualla tiedostossa osoitat tuohon objektiin viittauksella: 5 0 R tarkoittaa "objekti 5". Nämä viittaukset ovat sitä johdotusta. Catalog sisältää numerointimme mukaan 2 0 R päästäkseen sivupuuhun, sivupuu sisältää viittauksen takaisin alas sivuun, ja niin edelleen. Jos saat numeron väärin, lukija seuraa roikkuvaa osoitinta tyhjyyteen
Nimet, sanakirjat ja virrat (Streams)
Kolme syntaksin osaa kantaa melkein kaikkea. Nimi (name) alkaa kauttaviivalla: /Type, /Page, /F0. Nimet ovat kirjainkoolla merkityksellisiä tunnisteita, eivät merkkijonoja, ja PDF käyttää niitä sanakirjojen (dictionary) avaimina sekä merkitsemään, mikä objekti on kyseessä. Sanakirja (dictionary) on joukko avain-arvo-pareja käärittynä kaksinkertaisiin kulmasulkeisiin, joissa jokainen avain on nimi: << /Type /Page /MediaBox [0 0 612 792] >>. Arvot voivat olla numeroita, nimiä, taulukoita hakasulkeissa, viittauksia tai sisäkkäisiä sanakirjoja. Useimmat PDF-objektit ovat sanakirjoja
Virta (stream) on sanakirja, jota seuraa tavulohko avainsanojen stream ja endstream välissä. Siellä sivunpiirto-operaattorit asuvat, ja aidoissa tiedostoissa siellä asuvat myös pakatut kuvat ja upotetut fontit. Stream-sanakirja kuvaa tavut; tuotantotiedostossa sen on sisällettävä /Length-tietue, joka antaa tarkan tavumäärän, ja usein /Filter, kuten /FlateDecode, kun data on pakattua. Tukeudumme työkaluun /Length-arvon täyttämisessä, koska tavujen käsin laskeminen on osa tätä harjoitusta, jossa ei ole opetuksellista hyötyä ja suuri mahdollisuus yhden poikkeaman virheelle, joka rikkoo tiedoston
Objektien kirjoittaminen
Tässä ovat viisi objektia järjestyksessä. Yksityiskohta koordinaateista, joka on hyvä pitää mielessä ennen sisältövirran lukemista: PDF mittaa sivun vasemmasta alakulmasta pisteissä, missä yksi piste on 1/72 tuumaa, ja Y kasvaa ylöspäin. US Letter -sivu on 612 kertaa 792 pistettä, joten 50 700 sijaitsee lähellä vasenta yläkulmaa, ei alhaalla
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]
/Resources << /Font << /F0 4 0 R >> >>
/Contents 5 0 R
>>
endobj
4 0 obj
<< /Type /Font
/Subtype /Type1
/BaseFont /Helvetica
>>
endobj
5 0 obj
<< /Length 44 >>
stream
BT
/F0 36 Tf
50 700 Td
(Hello, World!) Tj
ET
endstream
endobj
Lue viittaukset ja rakenne selviää. Objekti 1, catalog, osoittaa /Pages-tietueellaan objektiin 2. Objekti 2, sivupuu, listaa objektin 3 kohdassa /Kids ja ilmoittaa /Count 1. Objekti 3, sivu, osoittaa /Parent-kohdassa takaisin ylös objektiin 2 (puu ja sivu viittaavat toisiinsa, mikä on pakollista), mitoittaa itsensä /MediaBox-arvolla, paljastaa fontin paikallisella nimellä /F0 kohdassa /Resources, ja nimeää objektin 5 sisällökseen. Objekti 4 on fontti: /BaseFont /Helvetica valitsee yhden niistä 14 vakiokirjasimesta, jotka jokaisella standardinmukaisella katseluohjelmalla jo on, joten mitään ei tarvitse upottaa. Objekti 5 on sisältövirta
Mitä sisältövirta oikeasti sanoo
Virran runko on pienenpieni ohjelma PDF:n sivunkuvauskielellä, joka on postfix-muotoinen: operandit tulevat ensin, sitten niitä kuluttava operaattori. Viisi riviä tekee työn. BT ja ET avaavat ja sulkevat tekstiobjektin; kaiken, mikä sijoittaa tai näyttää tekstiä, on sijaittava niiden välissä. /F0 36 Tf asettaa nykyiseksi fontiksi resurssin, jonka nimi on /F0 ja kooksi 36 pistettä (Tf on "set text font and size"). 50 700 Td siirtää tekstisijainnin sivukoordinaatteihin (50, 700). (Hello, World!) Tj näyttää merkkijonon, jonka PDF kirjoittaa kirjaimellisena tekstinä sulkeissa, käyttäen Tj-operaattoria sen maalaamiseen nykyiseen sijaintiin. Jätä BT/ET pois ja tiukka lukija hylkää teksti-operaattorit; unohda asettaa fontti ennen Tj-operaattoria ja piirtämiseen ei ole nykyistä fonttia
Stream-sanakirjassa oleva /Length 44 on tavumäärä stream- ja endstream-avainsanojen välillä, ja sen on oltava tarkka. Tämä on arvo, joka kannattaa siirtää työkalun hoidettavaksi sen sijaan, että laskisi rivinvaihtoja käsin, varsinkin koska se, kirjoittaako editorisi rivinvaihdot muodossa LF vai CRLF, muuttaa loppusummaa
Otsikko (Header), xref ja traileri
Objektit ovat sisältö. Kolme rakenteellista palaa muuttaa ne tiedostoksi. Ensimmäinen on otsikko (header), aivan ensimmäinen rivi, joka nimeää muodon ja version:
%PDF-1.7
% aloittaa kommentin PDF-syntaksissa, mutta lukija käsittelee tätä tiettyä kommenttia muotosignatuurina ja lukee siitä version. Oikea kirjoittaja seuraa sitä välittömästi toisella kommenttirivillä, joka koostuu high-bit -tavuista, mikä on vihje tiedostonsiirtotyökaluille, että tiedosto on binääriä eikä sitä saa turmella tekstinä
Tiedoston lopussa on ristiviitetaulukko (cross-reference table), hakemisto, joka mahdollistaa satunnaisen pääsyn. Se kirjaa jokaisen objektin tavupoikkeaman (byte offset) tiedoston alusta, jotta lukija voi hakea suoraan objektin 3 ilman, että sen tarvitsee jäsentää objekteja 1 ja 2 ensin. Taulukko on jäykkä: tietueet ovat kiinteälevyisiä, 20 tavua kukin lukien mukaan rivinvaihdon, muotoiltuna 10-numeroisena poikkeamana, 5-numeroisena sukupolvena, avainsanana (n käytössä olevalle (in-use), f vapaalle (free)), ja kahden tavun päättäjänä (terminator). Oikea taulukko kuudelle tietueellemme (objekti 0 on aina vapaiden listan (free-list) pää) näyttää tältä:
xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000235 00000 n
0000000308 00000 n
trailer
<< /Size 6
/Root 1 0 R
>>
startxref
408
%%EOF
Nämä poikkeamat ovat käsin kirjoitetun PDF:n hauras osa. Jokainen niistä on tarkka tavusijainti, josta vastaava N 0 obj alkaa, ja jokainen poikkeama siirtyy heti kun lisäät merkin minne tahansa sen yläpuolelle. Traileri on sisääntulopiste, jota lukija käyttää viimeisenä ja ensimmäisenä: /Root 1 0 R nimeää catalogin, /Size 6 ilmoittaa objektien lukumäärän, ja startxref 408 antaa tavupoikkeaman itse sanalle xref. Lukija avaa tiedoston, hyppää loppuun, lukee startxref, etsii ristiviitetaulukon, ja sieltä se tavoittaa catalogin ja kaiken sen alapuolella. %%EOF merkitsee viimeisen tavun
Anna työkalun korjata tavumäärät
Yllä olevat poikkeamat ovat havainnollistavia; käytännössä ne ovat vääriä siihen mennessä kun lopetat kirjoittamisen, koska ne riippuvat tiedostosi tarkasta tavuasettelusta. Sen sijaan, että laskisit ne uudelleen, kirjoita rakenne paikkamerkkiarvoilla ja anna apuohjelman rakentaa uudelleen ristiviitetaulukko ja virtojen pituudet. Ilmainen, monialustainen pdftk tekee tämän yhdellä kerralla:
pdftk hello-draft.pdf output hello.pdf
Se jäsentää objektisi, laskee uudelleen jokaisen tavupoikkeaman, täyttää oikeat /Length-arvot, kirjoittaa validin xref-taulukon ja trailerin, ja tulostaa hello.pdf. Avaa se missä tahansa katseluohjelmassa, niin saat yhden sivun, jolla lukee "Hello, World!" 36 pisteen Helveticalla lähellä yläreunaa. Qpdf tekee saman työn, ja monet katseluohjelmat myös korjaavat hieman virheellisen tiedoston lennossa. Työkaluun nojaamisen tarkoitus tässä ei ole laiskuus; kyse on siitä, että poikkeamamatematiikka on ainoa osa muotoa, jossa ei ole käsitteellistä sisältöä ja jossa on suurin virheprosentti, joten sen automatisointi antaa rakenteen pysyä asiana, jota olet oppimassa
Miksi tämä skaalautuu aitoihin asiakirjoihin
Mikään satasivuisessa raportissa ei muuta juuri rakentamaasi muotoa. Catalog istuu edelleen juuressa, sivupuu kerää edelleen sivut, ja jokainen sivu osoittaa edelleen resursseihinsa ja sisältövirtaan. Se mikä kasvaa on laajuus, ei selkäranka: sivupuu haarautuu, jotta lukija voi hypätä kokonaisten alipuiden yli, sisältövirrat kantavat satoja operaattoreita viiden sijaan, fontit upotetaan ominaan stream-objekteina leveystaulukoiden ja koodausten kera, ja kuvat saapuvat stream-objekteina kuvakohtaisin suodattimin. Nykyaikaisilla tiedostoilla on myös tapana pakata monia objekteja pakattuihin objektivirtoihin ja korvata yksinkertainen xref-taulukko ristiviitevirralla (cross-reference stream), minkä vuoksi oikean PDF:n avaaminen tekstieditorissa näyttää yleensä binäärimuurilta. Alla oleva malli on identtinen käsin tekemäsi tiedoston kanssa. Laajemmalle objektigraafille ja sille, miten catalog, sivupuu ja resurssisanakirjat suhteutuvat toisiinsa laajemmassa asiakirjassa, PDF-asiakirjarakenteen syvällinen kierros jatkaa siitä, mihin tämä jää, ja tiedostorakenteen yleiskatsaus kattaa inkrementaaliset päivitykset ja sen, miten traileri ketjutetaan yli revisioiden
Käsin kirjoittamisesta kirjastoon
Objektien kirjoittaminen käsin on oppimisharjoitus, ei tuotantotekniikka. Heti kun tarvitset oikeita fontteja, rivitettyä tekstiä, kuvia tai jotain triviaalia sivua monimutkaisempaa, tavujen kirjanpito, jonka pdftk paikkasi puolestasi, muuttuu koko työksi, ja haluat kirjaston, joka hoitaa sen. Samat viisi objektia kirjoitetaan yhä, mutta kirjasto laskee jokaisen poikkeaman, hallitsee fontti- ja resurssisanakirjoja ja pakkaa sisältövirrat ilman, että sinun tarvitsee seurata yhtäkään tavua. Delphissä ja C++Builderissä HotPDF Component pelkistää tämän koko tiedoston kouralliseksi kutsuja: määritä asiakirja, kutsu BeginDoc, SetFont ja TextOut sijoittamaan sama tervehdys, ja sitten EndDoc kirjoittamaan oikea catalog, sivupuu, xref ja traileri. Alla olevien objektien ymmärtäminen antaa sinun päätellä tulosteesta, kun asiakirja ei renderöidy odottamallasi tavalla