Tehnički članak

Izgradite minimalni PDF ručno: Pet objekata koje trebate

PDF je u svojoj srži spremnik običnog teksta (plain-text container). Otvorite većinu datoteka u hex uređivaču i vrh je čitljiv: komentar s verzijom, zatim niz numeriranih objekata, onda mali indeks i pokazivač na samom dnu koji govori čitaču gdje da počne. Uklonite kompresiju i format je dovoljno pristupačan da možete utipkati radni dokument u uređivač teksta i otvoriti ga u pregledniku. Učinivši to jednom, naučit ćete više o tome kako se PDF drži zajedno od bilo kojeg čitanja specifikacije, jer objekte morate ručno povezati jedan s drugim, a datoteka se odbija otvoriti dok ožičenje ne postavite ispravno

Ovo uputstvo gradi najmanji PDF koji zapravo nešto iscrtava (renders): jedna stranica, riječi "Hello, World!" u ugrađenom fontu, na papiru formata US Letter. Dovršena datoteka treba točno pet objekata i nekoliko redaka knjigovodstva oko njih. Prvo ćemo napisati objekte, zatim sastaviti zaglavlje, tablicu unakrsnih referenci (cross-reference table) i trailer koji ih povezuju u datoteku koju će čitač prihvatiti

Pet objekata na kojima preglednik inzistira

Čitač ne skenira PDF od vrha prema dnu tražeći sadržaj. Počinje kod trailera, slijedi referencu do kataloga dokumenta i od tamo hoda lancem objekata. Svaki objekt na tom lancu mora postojati ili otvaranje ne uspijeva. Za dokument od jedne stranice lanac je kratak, a svaka karika ima jedan posao:

  • Catalog je korijen. To je objekt na koji ukazuje trailer, a njegov jedini obvezni unos ovdje je referenca na stablo stranica (page tree)
  • Pages je čvor stabla stranica. Navodi stranice u dokumentu i prijavljuje koliko ih ima
  • Page opisuje jednu fizičku stranicu: njezinu veličinu, resurse kojima crta i koji ju content stream (tok sadržaja) iscrtava
  • Content stream sadrži operatore za crtanje, postfiks naredbe koje postavljaju tekst i grafiku na tu stranicu
  • Font deklarira obitelj fontova na koju se odnosi content stream. Koristite jedan od 14 standardnih fontova i nećete morati ništa ugrađivati

Svaki je objekt numeriran i adresabilan. Indirektni objekt zapisan je kao N 0 obj ... endobj, gdje je N broj objekta, a 0 broj njegove generacije (uvijek 0 u datoteci koju svježe pišete). Bilo gdje drugdje u datoteci pokazujete na taj objekt pomoću reference: 5 0 R znači "objekt 5". Te su reference ožičenje. Katalog sadrži 2 0 R u našem numeriranju kako bi dosegao stablo stranica, stablo stranica sadrži referencu natrag dolje na stranicu i tako dalje. Pogriješite li broj i čitač slijedi viseći pokazivač u ništa

Imena, rječnici i tokovi (streams)

Tri dijela sintakse nose gotovo sve. Ime (name) počinje s kosom crtom: /Type, /Page, /F0. Imena su identifikatori osjetljivi na velika i mala slova, a ne nizovi (strings), a PDF ih koristi za ključeve rječnika i za označavanje onoga što objekt jest. Rječnik (dictionary) je skup parova ključ-vrijednost omotanih u dvostruke kutne zagrade, pri čemu je svaki ključ ime: << /Type /Page /MediaBox [0 0 612 792] >>. Vrijednosti mogu biti brojevi, imena, nizovi (arrays) u uglatim zagradama, reference ili ugniježđeni rječnici. Većina PDF objekata su rječnici

Stream (tok) je rječnik iza kojeg slijedi blok bajtova između ključnih riječi stream i endstream. Tamo žive operatori za crtanje stranice, a u pravim datotekama tu žive komprimirane slike i ugrađeni fontovi. Stream rječnik opisuje bajtove; u produkcijskoj datoteci mora nositi unos /Length s točnim brojem bajtova, i često /Filter poput /FlateDecode kada su podaci komprimirani. Oslonit ćemo se na alat za popunjavanje unosa /Length, jer je ručno brojanje bajtova dio ove vježbe koji nema obrazovnu dobit i donosi visoku šansu pogreške pomaka za jedan (off-by-one) koja lomi datoteku

Pisanje objekata

Ovdje je pet objekata redom. Detalj koordinata koji trebate imati na umu prije čitanja toka sadržaja (content stream): PDF mjeri od donjeg lijevog kuta stranice u točkama, gdje je jedna točka 1/72 inča, a Y raste prema gore. Stranica US Letter je 612 x 792 točke, tako da 50 700 sjedi blizu gornjeg lijevog dijela, a ne dna

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

Pročitajte reference i struktura ispada. Objekt 1, katalog, usmjerava svoj unos /Pages na objekt 2. Objekt 2, stablo stranica, navodi objekt 3 u /Kids i deklarira /Count 1. Objekt 3, stranica, usmjerava /Parent natrag gore do objekta 2 (stablo i stranica se referenciraju međusobno, što je obvezno), postavlja si veličinu s /MediaBox, izlaže font pod lokalnim imenom /F0 u svojim /Resources i imenuje objekt 5 kao svoj sadržaj. Objekt 4 je font: /BaseFont /Helvetica bira jedan od 14 standardnih obitelji fontova (typefaces) koje svaki sukladni čitač već ima, tako da nema ničega za ugradnju. Objekt 5 je tok sadržaja (content stream)

Što zapravo kaže content stream

Tijelo streama (stream body) je sićušni program u jeziku za opis stranice (page-description language) PDF-a, koji je postfiks: prvo dolaze operandi, a zatim operator koji ih konzumira. Pet redaka obavlja posao. BT i ET otvaraju i zatvaraju tekstualni objekt; sve što pozicionira ili prikazuje tekst mora sjediti između njih. /F0 36 Tf postavlja trenutačni font na resurs s imenom /F0 na 36 točaka (Tf znači "set text font and size"). 50 700 Td pomiče tekstualnu poziciju na (50, 700) u koordinatama stranice. (Hello, World!) Tj prikazuje niz, kojeg PDF zapisuje kao doslovni (literal) tekst u zagradama, koristeći Tj da ga oboji na trenutačnoj poziciji. Izostavite BT/ET i strogi čitač odbija tekstualne operatore; zaboravite postaviti font prije Tj i ne postoji trenutačni font kojim bi crtao

/Length 44 u rječniku streama je broj bajtova između stream i endstream, i mora biti točan. Ovo je vrijednost koju se isplati predati alatu umjesto ručnog brojanja novih redaka (newlines), pogotovo jer hoće li vaš uređivač pisati završetke reda kao LF ili CRLF mijenja ukupan zbroj

Zaglavlje, xref i trailer

Objekti su sadržaj. Tri strukturna dijela pretvaraju ih u datoteku. Prvo je zaglavlje, prvi redak, koji imenuje format i verziju:

%PDF-1.7

% započinje komentar u sintaksi PDF-a, ali čitač tretira ovaj određeni komentar kao potpis formata (format signature) i čita verziju iz njega. Pravi pisac odmah ga slijedi drugim redom komentara high-bit bajtova, što je znak alatima za prijenos datoteka (file-transfer tools) da je datoteka binarna i ne smije biti oštećena kao tekst

Na kraju datoteke dolazi tablica unakrsnih referenci (cross-reference table), indeks koji omogućuje nasumični pristup. Zapisuje pomak u bajtovima svakog objekta od početka datoteke, tako da čitač može preskočiti ravno na objekt 3 bez prethodnog raščlanjivanja (parsing) objekata 1 i 2. Tablica je kruta: unosi su fiksne širine, 20 bajtova svaki uključujući kraj retka, formatirani kao 10-znamenkasti pomak (offset), 5-znamenkasta generacija, ključna riječ (n za in-use, f za free) i dvo-bajtni terminator. Točna tablica za naših šest unosa (objekt 0 uvijek je glava besplatne liste (free-list head)) izgleda ovako:

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

Ti pomaci (offsets) su osjetljivi dio ručnog pisanja PDF-a. Svaki je točna bajt pozicija na kojoj počinje odgovarajući N 0 obj, a svaki se pomak mijenja u trenutku kad dodate znak bilo gdje iznad njega. Trailer je ulazna točka koju čitač koristi i posljednje i prvo: /Root 1 0 R imenuje katalog, /Size 6 navodi broj objekata, a startxref 408 daje pomak u bajtovima same riječi xref. Čitač otvara datoteku, skače na kraj, čita startxref, pretražuje do tablice unakrsnih referenci i od tamo dolazi do kataloga i svega ispod njega. %%EOF označava posljednji bajt

Neka alat popravi brojeve bajtova

Pomaci iznad su ilustrativni; u praksi bit će pogrešni do trenutka kada završite s upisivanjem, jer ovise o točnom bajt rasporedu vaše datoteke. Umjesto da ih ponovno izračunavate, napišite strukturu s vrijednostima (placeholder values) kao rezerviranim mjestima i prepustite alatu da ponovno izgradi tablicu unakrsnih referenci i duljinu streamova. Besplatni, multiplatformski (cross-platform) pdftk to radi u jednom prolazu:

pdftk hello-draft.pdf output hello.pdf

On raščlanjuje vaše objekte, ponovno izračunava svaki bajt pomak, ispunjava ispravne vrijednosti /Length, ispisuje važeću xref tablicu i trailer te ispušta (emits) hello.pdf. Otvorite to u bilo kojem pregledniku i dobit ćete jednu stranicu sa "Hello, World!" u Helvetici od 36 točaka blizu vrha. Qpdf radi isti posao, a mnogi preglednici također će popraviti malo oštećenu datoteku u hodu. Smisao oslanjanja na alat ovdje nije lijenost; smisao je da je aritmetika pomaka onaj dio formata s nula konceptualnog sadržaja i najvećom stopom pogreške, tako da automatiziranje toga omogućuje da struktura ostane stvar koju učite

Zašto se ovo prenosi na prave dokumente

Ništa o izvješću na sto stranica ne mijenja oblik koji ste upravo izgradili. Katalog i dalje sjedi u korijenu, stablo stranica i dalje skuplja stranice, a svaka stranica i dalje usmjerava na svoje resurse i tok sadržaja. Ono što raste je širina, a ne kralježnica (spine): stablo stranica grana se tako da čitač može preskočiti cijela podstabla, tokovi sadržaja nose stotine operatora umjesto pet, fontovi se ugrađuju kao vlastiti stream objekti s tablicama širine i kodiranjima, a slike stižu kao streamovi s filterima specifičnim za sliku. Moderne datoteke također imaju tendenciju pakiranja mnogih objekata u komprimirane streamove objekata i zamjene obične xref tablice streamom unakrsnih referenci (cross-reference stream), zbog čega otvaranje pravog PDF-a u uređivaču teksta obično pokazuje zid binarnih datoteka. Temeljni model je identičan onom u vašoj ručno rađenoj datoteci. Za širi graf objekata (object graph) i kako se rječnici kataloga, stabla stranica i resursa povezuju kroz veći dokument, detaljni pregled strukture PDF dokumenta nastavlja tamo gdje ovo staje, a pregled strukture datoteke pokriva inkrementalna ažuriranja i kako se trailer ulančava u revizije

Od ručnog pisanja do biblioteke

Tipkanje objekata rukom vježba je za učenje, a ne tehnika za produkciju. Onog trenutka kada zatrebate prave fontove, prelomljeni tekst (wrapped text), slike ili više od trivijalne stranice, bajt knjigovodstvo koje je za vas pdftk zakrpao, postaje čitav posao i trebate biblioteku (library) koja to rješava. Istih pet objekata se i dalje pišu, ali biblioteka izračunava svaki pomak (offset), upravlja fontovima i rječnicima resursa i komprimira tokove sadržaja a da vi ne pratite niti jedan bajt. U Delphiju i C++Builderu, HotPDF Component svodi cijelu ovu datoteku na nekolicinu poziva: postavite dokument, pozovite BeginDoc, SetFont i TextOut da postavite isti pozdrav, a zatim EndDoc kako biste ispisali točan katalog, stablo stranica, xref i trailer. Razumijevanje temeljnih objekata (objects underneath) omogućuje vam da argumentirate o izlazu kada se dokument ne prikazuje (render) kako ste očekivali