Teknisk artikel

Bygg en minimal PDF för hand: De fem objekt du behöver

En PDF är i grunden en behållare i klartext. Om du öppnar de flesta filer i en hex-redigerare är toppen läsbar: en versionskommentar, sedan en serie numrerade objekt, därefter ett litet index och en pekare längst ner som talar om för en läsare var den ska börja. Ta bort komprimeringen och formatet är tillräckligt lättillgängligt för att du ska kunna skriva in ett fungerande dokument i en textredigerare och få en visare att öppna det. Att göra det en gång lär dig mer om hur PDF hänger ihop än någon mängd läsning av specifikationen, eftersom du måste koppla ihop objekten med varandra för hand och filen vägrar öppnas förrän du får till kopplingen rätt

Den här genomgången bygger den minsta PDF:en som faktiskt renderar något: en sida, orden "Hello, World!" med ett inbyggt typsnitt, på US Letter-papper. Den färdiga filen behöver exakt fem objekt och några rader med bokföring (bookkeeping) runt dem. Vi kommer att skriva objekten först och sedan sätta ihop header, korsreferenstabell (cross-reference table) och trailer som binder samman dem till en fil som en läsare accepterar

De fem objekt som en visare insisterar på

En läsare skannar inte en PDF uppifrån och ner på jakt efter innehåll. Den börjar vid trailern, följer en referens till dokumentkatalogen (catalog) och vandrar längs en kedja av objekt därifrån. Varje objekt i den kedjan måste existera, annars misslyckas öppningen. För ett ensidigt dokument är kedjan kort, och varje länk har en enda uppgift:

  • Catalog är roten. Det är objektet som trailern pekar på, och dess enda obligatoriska post här är en referens till sidträdet
  • Pages är sidträdets nod. Den listar sidorna i dokumentet och rapporterar hur många det finns
  • Page beskriver en fysisk sida: dess storlek, de resurser den ritar med och vilken innehållsström (content stream) som målar den
  • Content stream håller ritningsoperatorerna, postfix-kommandona som placerar text och grafik på den sidan
  • Font deklarerar typsnittet som innehållsströmmen hänvisar till. Använd ett av de 14 standardtypsnitten så behöver du inte bädda in någonting

Varje objekt är numrerat och adresserbart. Ett indirekt objekt skrivs som N 0 obj ... endobj, där N är objektnumret och 0 är dess generationsnummer (alltid 0 i en fil du skriver från början). Någon annanstans i filen pekar du på det objektet med en referens: 5 0 R betyder "objekt 5". Dessa referenser är själva kopplingarna. Katalogen innehåller 2 0 R i vår numrering för att nå sidträdet, sidträdet innehåller en referens tillbaka ner till sidan, och så vidare. Får du ett nummer fel så följer läsaren en hängande pekare rakt ut i intet

Names, dictionaries och streams

Tre syntaxdelar bär nästan allting. Ett namn (name) börjar med ett snedstreck: /Type, /Page, /F0. Namn är skiftlägeskänsliga identifierare, inte strängar, och PDF använder dem för nycklar i dictionaries (ordlistor) och för att tagga vad ett objekt är. Ett dictionary är en uppsättning nyckel-värde-par inslagna i dubbla vinkelparenteser, där varje nyckel är ett namn: << /Type /Page /MediaBox [0 0 612 792] >>. Värden kan vara siffror, namn, arrayer inom hakparenteser, referenser eller nästlade dictionaries. De flesta PDF-objekt är dictionaries

En ström (stream) är ett dictionary följt av ett block med bytes mellan nyckelorden stream och endstream. Det är där sidritande operatorer bor, och i verkliga filer även där komprimerade bilder och inbäddade typsnitt bor. Strömmens dictionary beskriver byten; i en produktionsfil måste den ha en /Length-post som ger det exakta antalet bytes, och ofta ett /Filter såsom /FlateDecode när datan komprimeras. Vi kommer att förlita oss på ett verktyg för att fylla i /Length, eftersom att räkna bytes för hand är den del av denna övning som saknar pedagogisk vinning och som har en hög risk för felräkning som gör sönder filen

Att skriva objekten

Här är de fem objekten i ordning. Den koordinatdetalj du bör ha i åtanke innan du läser innehållsströmmen: PDF mäter från sidans nedre vänstra hörn i punkter, där en punkt är 1/72 tum, och Y växer uppåt. En US Letter-sida är 612 x 792 punkter, så 50 700 sitter nära övre vänstra hörnet, inte i botten

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

Läs referenserna så faller strukturen på plats. Objekt 1, katalogen, pekar sin /Pages-post mot objekt 2. Objekt 2, sidträdet, listar objekt 3 i /Kids och deklarerar /Count 1. Objekt 3, sidan, pekar /Parent tillbaka upp till objekt 2 (trädet och sidan refererar till varandra, vilket krävs), storleksbestämmer sig själv med /MediaBox, exponerar typsnittet under det lokala namnet /F0 i sina /Resources, och anger objekt 5 som sitt innehåll. Objekt 4 är typsnittet: /BaseFont /Helvetica väljer ett av de 14 standardtypsnitt som varje överensstämmande läsare redan har, så det finns ingenting att bädda in. Objekt 5 är innehållsströmmen

Vad innehållsströmmen faktiskt säger

Strömkroppen är ett litet program i PDF:s sidbeskrivningsspråk, som är postfix: operanderna kommer först, sedan operatorn som konsumerar dem. Fem rader gör jobbet. BT och ET öppnar och stänger ett textobjekt; allt som placerar eller visar text måste sitta mellan dem. /F0 36 Tf sätter det aktuella typsnittet till resursen vid namn /F0 på 36 punkter (Tf betyder "sätt textens typsnitt och storlek"). 50 700 Td flyttar textens position till (50, 700) i sidkoordinater. (Hello, World!) Tj visar strängen, som PDF skriver som bokstavlig text inom parenteser, och använder Tj för att måla den på den aktuella positionen. Utelämnar du BT/ET avvisar en strikt läsare textoperatorerna; glömmer du att ställa in ett typsnitt före Tj finns det inget aktuellt typsnitt att rita med

/Length 44 i strömmens dictionary är antalet bytes mellan stream och endstream, och det måste vara exakt. Detta är värdet som är värt att överlämna till ett verktyg snarare än att räkna radbrytningar för hand, särskilt eftersom summan ändras beroende på om din redigerare skriver radslut som LF eller CRLF

Header, xref och trailer

Objekten är innehållet. Tre strukturella delar gör dem till en fil. Den första är headern, allra första raden, som namnger formatet och versionen:

%PDF-1.7

Tecknet % inleder en kommentar i PDF-syntax, men en läsare behandlar denna speciella kommentar som formatsignaturen och läser ut versionen från den. En riktig skrivare följer omedelbart upp med en andra kommentarsrad bestående av hög-bit-bytes (high-bit bytes), ett tips till filöverföringsverktyg att filen är binär och inte får massakreras som text

I slutet av filen kommer korsreferenstabellen, indexet som gör slumpmässig åtkomst (random access) möjlig. Den registrerar byte-förskjutningen för varje objekt från början av filen, så att en läsare kan söka (seek) direkt till objekt 3 utan att behöva tolka (parse) objekten 1 och 2 först. Tabellen är rigid: posterna har fast bredd, 20 bytes vardera inklusive radslut, formaterade som en 10-siffrig förskjutning, en 5-siffrig generation, ett nyckelord (n för i bruk, f för ledig) och en två-bytes avslutare. En korrekt tabell för våra sex poster (objekt 0 är alltid huvudet i ledig-listan) ser ut så här:

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

Dessa förskjutningar är den sköra biten när man skriver PDF för hand. Var och en är den exakta byte-position där motsvarande N 0 obj börjar, och varje förskjutning flyttas i det ögonblick du lägger till ett tecken någonstans ovanför den. Trailern är startpunkten en läsare använder sist och först: /Root 1 0 R namnger katalogen, /Size 6 anger antalet objekt, och startxref 408 ger byte-förskjutningen för själva ordet xref. En läsare öppnar filen, hoppar till slutet, läser startxref, söker fram till korsreferenstabellen och når därifrån katalogen och allt under den. %%EOF markerar den allra sista byten

Låt ett verktyg fixa antalet bytes

Förskjutningarna ovan är illustrativa; i praktiken kommer de att vara fel när du har skrivit klart, eftersom de är beroende av den exakta byte-layouten i din fil. Istället för att räkna om dem, skriv strukturen med platshållarvärden och låt ett verktyg bygga om korsreferenstabellen och strömlängderna. Det kostnadsfria, plattformsoberoende verktyget pdftk gör detta i ett enda pass:

pdftk hello-draft.pdf output hello.pdf

Det tolkar dina objekt, räknar om varje byte-förskjutning, fyller i korrekta /Length-värden, skriver en giltig xref-tabell och trailer, och avger hello.pdf. Öppna den i valfri visare så får du en sida med "Hello, World!" i 36-punkters Helvetica nära toppen. Qpdf gör samma jobb, och många visare kommer också att reparera en något missbildad fil i farten. Poängen med att förlita sig på ett verktyg här handlar inte om lathet; det är att förskjutningsaritmetiken är den enda delen av formatet med noll konceptuellt innehåll och högst felfrekvens, så att automatisera det låter strukturen förbli det som du lär dig

Varför detta skalar till riktiga dokument

Ingenting med en hundrasidig rapport förändrar den form du precis byggde. Katalogen sitter fortfarande i roten, sidträdet samlar fortfarande sidorna, och varje sida pekar fortfarande på sina resurser och en innehållsström. Det som växer är bredden, inte ryggraden: sidträdet förgrenar sig så att en läsare kan hoppa över hela underträd, innehållsströmmar bär på hundratals operatorer i stället för fem, typsnitt bäddas in som egna strömobjekt med breddtabeller och kodningar (encodings), och bilder anländer som strömmar med bildspecifika filter. Moderna filer tenderar också att packa många objekt till komprimerade objektströmmar (object streams) och ersätta den vanliga xref-tabellen med en korsreferensström (cross-reference stream), vilket är anledningen till att det vanligtvis visar en vägg av binärdata när man öppnar en riktig PDF i en textredigerare. Modellen inunder är identisk med den i din handgjorda fil. För den bredare objektgrafen och hur katalogen, sidträdet och resursordlistorna relaterar över ett större dokument, tar den djupgående rundturen av PDF-dokumentstrukturen vid där detta slutar, och filstrukturöversikten täcker inkrementella uppdateringar och hur trailern länkas över revisioner

Från handskrivning till ett bibliotek

Att skriva in objekt för hand är en läroövning, inte en produktionsteknik. I det ögonblick du behöver riktiga typsnitt, textbrytning, bilder eller mer än en trivial sida, blir den byte-bokföring som pdftk korrigerade åt dig hela jobbet, och då vill du ha ett bibliotek som äger den processen. Samma fem objekt skrivs fortfarande, men ett bibliotek beräknar varje förskjutning, hanterar typsnitts- och resursordlistor och komprimerar innehållsströmmarna utan att du behöver spåra en enda byte. I Delphi och C++Builder reducerar HotPDF Component hela denna fil till en handfull anrop: ställ in dokumentet, anropa BeginDoc, SetFont och TextOut för att placera samma hälsning, och sedan EndDoc för att skriva en korrekt katalog, sidträd, xref och trailer. Att förstå objekten under huven är det som låter dig resonera om utdatan när ett dokument inte renderas på det sätt du förväntade dig