Teknisk artikkel

Bygg en minimal PDF for hånd: De fem objektene du trenger

En PDF er innerst inne en vanlig tekst-container. Åpne de fleste filer i en hex-editor, og toppen er lesbar: en versjonskommentar, deretter en rekke nummererte objekter, deretter en liten indeks og en peker helt i bunnen som forteller en leser hvor den skal starte. Skrell bort komprimeringen, og formatet er tilgjengelig nok til at du kan skrive et fungerende dokument i en tekstredigerer og få en seer (viewer) til å åpne det. Å gjøre det én gang lærer deg mer om hvordan PDF henger sammen enn noen mengde lesing av spesifikasjonen, fordi du må koble objektene til hverandre for hånd, og filen nekter å åpne før du får kablingen (wiring) riktig

Denne gjennomgangen bygger den minste PDF-en som faktisk gjengir (renders) noe: én side, ordene "Hello, World!" i en innebygd skrifttype, på US Letter-papir. Den ferdige filen krever nøyaktig fem objekter og et par linjer med bokføring (bookkeeping) rundt dem. Vi vil skrive objektene først, og deretter sette sammen overskriften (header), kryssreferansetabellen, og traileren (trailer) som binder dem inn i en fil en leser vil akseptere

De fem objektene en seer insisterer på

En leser skanner ikke en PDF fra topp til bunn på jakt etter innhold. Den starter ved traileren, følger en referanse til dokumentkatalogen (Catalog), og går en kjede av objekter derfra. Hvert objekt på den kjeden må eksistere, ellers vil åpningen feile. For et ensides-dokument er kjeden kort, og hver lenke har en enkelt jobb:

  • Catalog er roten. Det er objektet traileren peker på, og dens eneste påkrevde oppføring her er en referanse til sidetreet
  • Pages er sidetre-noden. Den lister opp sidene i dokumentet og rapporterer hvor mange det er
  • Page beskriver én fysisk side: dens størrelse, ressursene den tegner med, og hvilken innholdsstrøm som maler den
  • Content stream inneholder tegneoperatørene (drawing operators), postfix-kommandoene som plasserer tekst og grafikk på den siden
  • Font deklarerer skrifttypen innholdsstrømmen refererer til. Bruk en av de 14 standard-skrifttypene, så trenger du ikke å bygge inn noe

Hvert objekt er nummerert og adresserbart. Et indirekte objekt skrives som N 0 obj ... endobj, der N er objektnummeret og 0 er dets generasjonsnummer (alltid 0 i en fil du skriver ny). Alle andre steder i filen peker du på det objektet med en referanse: 5 0 R betyr "objekt 5." De referansene er kablingen (wiring). Katalogen (catalog) inneholder 2 0 R i vår nummerering for å nå sidetreet, sidetreet inneholder en referanse ned igjen til siden, og så videre. Gjør en feil med et nummer, og leseren følger en hengende peker (dangling pointer) ut i intet

Navn, ordbøker og strømmer

Tre biter av syntaks bærer nesten alt. Et navn starter med en skråstrek: /Type, /Page, /F0. Navn er case-sensitive identifikatorer, ikke strenger, og PDF bruker dem for ordboknøkler (dictionary keys) og for å merke (tagging) hva et objekt er. En ordbok er et sett av nøkkel-verdi-par pakket inn i doble vinkelparenteser, der hver nøkkel er et navn: << /Type /Page /MediaBox [0 0 612 792] >>. Verdier kan være tall, navn, arrayer (matriser) i hakeparenteser, referanser, eller nøstede (nested) ordbøker. De fleste PDF-objekter er ordbøker

En strøm (stream) er en ordbok etterfulgt av en blokk med byte mellom nøkkelordene stream og endstream. Det er her side-tegne-operatører bor, og i ekte filer der komprimerte bilder og innebygde skrifttyper bor også. Strøm-ordboken beskriver bytene; i en produksjonsfil må den bære en /Length-oppføring som gir nøyaktig antall byte, og ofte et /Filter slik som /FlateDecode når dataene er komprimert. Vi kommer til å støtte oss på et verktøy til å fylle inn /Length, fordi å telle byte for hånd er den delen av denne øvelsen som ikke har noen utdanningsmessig gevinst og en høy sjanse for en en-av-feil (off-by-one) som ødelegger filen

Skrive objektene

Her er de fem objektene i rekkefølge. Koordinatdetaljen det er verdt å huske før du leser innholdsstrømmen: PDF måler fra nederste venstre hjørne av siden i punkter (points), der ett punkt er 1/72 tomme, og Y vokser oppover. En US Letter-side er 612 ganger 792 punkter, så 50 700 sitter nær toppen-venstre, ikke bunnen

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

Les referansene og strukturen faller på plass. Objekt 1, katalogen, peker sin /Pages-oppføring på objekt 2. Objekt 2, sidetreet, lister opp objekt 3 i /Kids og deklarerer /Count 1. Objekt 3, siden, peker /Parent opp igjen til objekt 2 (treet og siden refererer til hverandre, noe som kreves), dimensjonerer seg selv med /MediaBox, eksponerer skrifttypen under det lokale navnet /F0 i dens /Resources, og navngir objekt 5 som dens innhold. Objekt 4 er skrifttypen: /BaseFont /Helvetica velger ett av de 14 standard fontsnittene som enhver samsvarende leser allerede har, så det er ingenting som må bygges inn. Objekt 5 er innholdsstrømmen

Hva innholdsstrømmen faktisk sier

Strømkroppen (stream body) er et lite program i PDFs side-beskrivelsesspråk (page-description language), som er postfix: operander kommer først, deretter operatøren som konsumerer dem. Fem linjer gjør arbeidet. BT og ET åpner og lukker et tekstobjekt; alt som posisjonerer eller viser tekst må sitte mellom dem. /F0 36 Tf setter den gjeldende skrifttypen til ressursen navngitt /F0 på 36 punkter (Tf er "set text font and size"). 50 700 Td flytter tekstposisjonen til (50, 700) i sidekoordinater. (Hello, World!) Tj viser strengen, som PDF skriver som bokstavelig tekst i parenteser, ved å bruke Tj for å male den på den gjeldende posisjonen. Utelat BT/ET og en streng leser avviser tekstoperatørene; glem å sette en skrifttype før Tj og det er ingen gjeldende skrifttype å tegne med

/Length 44 i strøm-ordboken er tellingen av byte mellom stream og endstream, og den må være nøyaktig. Dette er verdien det er verdt å overlate til et verktøy heller enn å telle linjeskift for hånd, spesielt siden om redigereren din skriver linjeslutt som LF eller CRLF endrer totalsummen

Overskrift (header), xref og trailer

Objektene er innholdet. Tre strukturelle deler gjør dem til en fil. Den første er overskriften, den aller første linjen, som navngir formatet og versjonen:

%PDF-1.7

% begynner en kommentar i PDF-syntaks, men en leser behandler denne spesielle kommentaren som formatets signatur (format signature) og leser versjonen fra den. En ekte skriver lar den umiddelbart etterfølges av en andre kommentarlinje med high-bit byte, et hint for filoverføringsverktøy om at filen er binær og ikke må bli ødelagt som tekst

I slutten av filen kommer kryssreferansetabellen, indeksen som gjør tilfeldig tilgang (random access) mulig. Den registrerer byte-forskyvningen (byte offset) for hvert objekt fra starten av filen, slik at en leser kan søke direkte til objekt 3 uten å parse objektene 1 og 2 først. Tabellen er rigid: oppføringer har fast bredde (fixed-width), 20 byte hver inkludert linjeslutt, formatert som en 10-sifret forskyvning, en 5-sifret generasjon, et nøkkelord (n for i-bruk, f for fri), og en to-byte terminator. En korrekt tabell for våre seks oppføringer (objekt 0 er alltid fri-listen-hodet) ser slik ut:

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

Disse forskyvningene er den skjøre delen av å skrive PDF for hånd. Hver av dem er den nøyaktige byte-posisjonen hvor den tilhørende N 0 obj begynner, og hver forskyvning forskyves det øyeblikket du legger til et tegn et sted ovenfor den. Traileren er inngangspunktet en leser bruker sist og først: /Root 1 0 R navngir katalogen, /Size 6 oppgir antall objekter, og startxref 408 gir byte-forskyvningen til selve ordet xref. En leser åpner filen, hopper til enden, leser startxref, søker til kryssreferansetabellen, og når derfra katalogen og alt under den. %%EOF markerer den siste byten

La et verktøy fikse byte-tellingen

Forskyvningene (offsets) ovenfor er illustrative; i praksis vil de være feil innen du er ferdig med å skrive, fordi de avhenger av den nøyaktige bytelayouten (byte layout) for filen din. I stedet for å beregne dem på nytt, skriv strukturen med plassholder-verdier, og la et verktøy (utility) bygge kryssreferansetabellen og strøm-lengdene på nytt. Den gratis, kryssplattform pdftk gjør dette i én gjennomkjøring:

pdftk hello-draft.pdf output hello.pdf

Det parser objektene dine, beregner hver byte-forskyvning på nytt, fyller inn de riktige /Length-verdiene, skriver en gyldig xref-tabell og trailer, og spytter ut hello.pdf. Åpne den i en hvilken som helst seer (viewer), og du får én side med "Hello, World!" i 36-punkts Helvetica nær toppen. Qpdf gjør samme jobb, og mange seere vil også reparere en litt misdannet (malformed) fil i farten (on the fly). Poenget med å støtte seg på et verktøy her er ikke latskap; det er at offset-aritmetikken (offset arithmetic) er den ene delen av formatet som har null konseptuelt innhold, og den høyeste feilraten, så å automatisere det lar strukturen forbli det du lærer om

Hvorfor dette skalerer til virkelige dokumenter

Ingenting ved en hundresiders rapport endrer fasongen du nettopp bygget. Katalogen (catalog) sitter fortsatt ved roten, sidetreet (page tree) samler fortsatt sidene, og hver side peker fortsatt på dens ressurser og en innholdsstrøm. Det som vokser er bredden, ikke ryggraden: sidetreet forgreiner seg, slik at en leser kan hoppe over hele undertrær, innholdsstrømmer bærer hundrevis av operatører (operators) i stedet for fem, skrifttyper bygges inn som sine egne strøm-objekter (stream objects) med breddetabeller og kodinger, og bilder kommer som strømmer med bilde-spesifikke filtre. Moderne filer pleier også å pakke mange objekter inn i komprimerte objektstrømmer, og bytter ut den vanlige xref-tabellen med en kryssreferansestrøm (cross-reference stream), noe som er grunnen til at å åpne en ekte PDF i et tekstredigeringsprogram vanligvis bare viser en vegg av binærdata. Modellen under er identisk med den i din håndlagde fil. For den bredere objektgrafen (object graph) og hvordan katalogen, sidetreet og ressursordbøkene relaterer seg på tvers av et større dokument, fortsetter den dybdegående touren av PDF-dokumentstruktur der dette slutter, og den filstrukturoversikten dekker inkrementelle oppdateringer og hvordan traileren lenker (chains) over revisjoner

Fra håndskriving til et bibliotek

Å skrive objekter for hånd er en læringsøvelse, ikke en produksjonsteknikk. Det sekundet du trenger ekte skrifttyper (fonts), pakket (wrapped) tekst, bilder, eller noe mer enn en triviell side, blir den byte-bokføringen (byte bookkeeping) som pdftk patchet (patched) for deg, hele jobben, og du vil ha et bibliotek som eier det. De samme fem objektene blir fremdeles skrevet, men et bibliotek beregner hver forskyvning, administrerer skrifttype- og ressursordbøkene, og komprimerer innholdsstrømmene uten at du sporer en eneste byte. I Delphi og C++Builder reduserer HotPDF Component hele denne filen til en håndfull kall: sett opp dokumentet, kall BeginDoc, SetFont og TextOut for å plassere den samme hilsenen, og deretter EndDoc for å skrive en korrekt katalog, sidetre, xref og trailer. Å forstå objektene på undersiden er det som lar deg resonnere rundt utfallet (output) når et dokument ikke gjengis slik du forventet