Technical Article

Ručna izrada minimalnog PDF-a: Pet objekata koji su vam potrebni

PDF je u svojoj suštini kontejner sa običnim tekstom. Otvorite većinu datoteka u heksadecimalnom uređivaču (hex editor) i gornji deo je čitljiv: komentar o verziji, zatim niz numerisanih objekata, pa mali indeks i pokazivač na samom dnu koji čitaču govori odakle da počne. Ako uklonite kompresiju, format je dovoljno pristupačan da možete ukucati funkcionalan dokument u tekstualnom uređivaču i otvoriti ga u pregledaču. Ako to uradite jednom, naučićete više o tome kako se PDF drži zajedno nego iz bilo kog čitanja specifikacija, jer morate ručno da povežete objekte jedne sa drugima, a datoteka odbija da se otvori dok ne uspostavite ispravno povezivanje.

Ovaj vodič gradi najmanji PDF koji zapravo nešto iscrtava: jednu stranicu sa rečima „Hello, World!“ u ugrađenom fontu, na papiru formata US Letter. Gotova datoteka zahteva tačno pet objekata i nekoliko redova pratećih informacija oko njih. Prvo ćemo napisati objekte, a zatim sastaviti zaglavlje, tabelu unakrsnih referenci (cross-reference table) i trailer koji ih povezuju u datoteku koju će čitač prihvatiti.

Pet objekata koje pregledač obavezno zahteva

Čitač ne skenira PDF od vrha do dna tražeći sadržaj. On počinje od trailer-a, prati referencu do kataloga dokumenta i odatle prolazi kroz lanac objekata. Svaki objekat u tom lancu mora postojati ili otvaranje neće uspeti. Za dokument od jedne stranice lanac je kratak, a svaka karika ima po jedan zadatak:

  • Catalog (Katalog) je koren. To je objekat na koji trailer ukazuje, a njegov jedini obavezni unos ovde jeste referenca na stablo stranica (page tree).
  • Pages (Stranice) je čvor stabla stranica. On navodi stranice u dokumentu i prijavljuje koliko ih ima.
  • Page (Stranica) opisuje jednu fizičku stranicu: njenu veličinu, resurse pomoću kojih se iscrtava i koji tok sadržaja (content stream) je oslikava.
  • Content stream (Tok sadržaja) sadrži operatore crtanja, odnosno postfiksne komande koje postavljaju tekst i grafiku na tu stranicu.
  • Font deklariše tip fonta na koji se tok sadržaja poziva. Koristite jedan od 14 standardnih fontova i nećete morati ništa da ugrađujete.

Svaki objekat je numerisan i adresibilan. Indirektni objekat se piše kao N 0 obj ... endobj, gde je N broj objekta, a 0 je njegov broj generacije (uvek 0 u novonapisanoj datoteci). Na bilo kom drugom mestu u datoteci ukazujete na taj objekat pomoću reference: 5 0 R označava „objekat 5“. Te reference predstavljaju povezivanje. Katalog sadrži 2 0 R u našoj numeraciji kako bi stigao do stabla stranica, stablo stranica sadrži referencu nazad na stranicu, i tako dalje. Ako pogrešite broj, čitač će pratiti nevažeći pokazivač u prazno.

Nazivi, rečnici i tokovi

Tri dela sintakse nose skoro sve. Naziv (name) počinje sa kosom crtom: /Type, /Page, /F0. Nazivi su identifikatori osetljivi na velika i mala slova, a ne obični tekstualni nizovi, i PDF ih koristi za ključeve rečnika i za označavanje onoga što objekat jeste. Rečnik (dictionary) je skup parova ključ-vrednost obavijenih dvostrukim uglastim zagradama, gde je svaki ključ naziv: << /Type /Page /MediaBox [0 0 612 792] >>. Vrednosti mogu biti brojevi, nazivi, nizovi u uglastim zagradama, reference ili ugnježdeni rečnici. Većina PDF objekata su rečnici.

A tok (stream) je rečnik praćen blokom bajtova između ključnih reči stream i endstream. Tu žive operatori za iscrtavanje stranice, a u stvarnim datotekama i kompresovane slike i ugrađeni fontovi. Rečnik toka opisuje bajtove; u produkcionoj datoteci on mora da sadrži unos /Length koji daje tačan broj bajtova, a često i /Filter kao što je /FlateDecode kada su podaci kompresovani. Oslonićemo se na alat da popuni /Length, jer je ručno brojanje bajtova deo ove vežbe bez ikakve edukativne koristi i sa velikom šansom za grešku za jedan (off-by-one) koja kvari datoteku.

Pisanje objekata

Evo pet objekata po redu. Detalj o koordinatama koji treba imati na umu pre čitanja toka sadržaja: PDF meri od donjeg levog ugla stranice u tačkama (points), gde je jedna tačka 1/72 inča, a Y raste nagore. Stranica formata US Letter ima dimenzije 612 sa 792 tačke, tako da se tačka 50 700 nalazi blizu gornjeg levog ugla, a ne na dnu.

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 postaje jasna. Objekat 1, katalog, usmerava svoj unos /Pages na objekat 2. Objekat 2, stablo stranica, navodi objekat 3 u /Kids i deklariše /Count 1. Objekat 3, stranica, upućuje svoj /Parent nazad na objekat 2 (stablo i stranica upućuju jedno na drugo, što je obavezno), postavlja sopstvenu veličinu pomoću /MediaBox, izlaže font pod lokalnim nazivom /F0 u svom rečniku /Resources i navodi objekat 5 kao svoj sadržaj. Objekat 4 je font: /BaseFont /Helvetica bira jedan od 14 standardnih fontova koje svaki usaglašeni čitač već ima, tako da nema potrebe za ugradnjom. Objekat 5 je tok sadržaja.

Šta tok sadržaja zapravo govori

Telo toka je mali program u PDF jeziku za opis stranice, koji je postfiksni: operandi dolaze prvi, a zatim operator koji ih koristi. Pet linija obavlja posao. BT i ET otvaraju i zatvaraju tekstualni objekat; sve što pozicionira ili prikazuje tekst mora stajati između njih. /F0 36 Tf postavlja trenutni font na resurs pod nazivom /F0 na veličinu od 36 tačaka (Tf je „postavi tekstualni font i veličinu“). 50 700 Td pomera poziciju teksta na (50, 700) u koordinatama stranice. (Hello, World!) Tj prikazuje tekstualni niz, koji PDF piše kao doslovni tekst u zagradama, koristeći Tj da ga iscrta na trenutnoj poziciji. Izostavite BT/ET i striktan čitač će odbaciti tekstualne operatore; zaboravite da postavite font pre Tj i nećete imati trenutni font za crtanje.

Vrednost /Length 44 u rečniku toka je broj bajtova između ključnih reči stream i endstream, i mora biti tačna. Ovo je vrednost koju vredi prepustiti alatu umesto da ručno brojite prelaze u novi red, posebno zato što to da li vaš uređivač piše završetke redova kao LF ili CRLF menja ukupan broj.

Zaglavlje, xref i trailer

Objekti su sadržao. Tri strukturna dela pretvaraju ih u datoteku. Prvi je zaglavlje (header), odnosno prvi red koji navodi format i verziju:

%PDF-1.7

Znak % započinje komentar u PDF sintaksi, ali čitač tretira ovaj specifični komentar kao potpis formata i iz njega čita verziju. Stvarni generator ga odmah prati drugim redom komentara sa bajtovima visoke vrednosti (high-bit bytes), što je nagoveštaj alatima za prenos datoteka da je datoteka binarna i da se ne sme menjati kao običan tekst.

Na kraju datoteke dolazi tabela unakrsnih referenci (cross-reference table), odnosno indeks koji omogućava nasumičan pristup. Ona beleži bajt-ofset svakog objekta od početka datoteke, tako da čitač može skočiti direktno na objekat 3 bez potrebe da prvo analizira objekte 1 i 2. Tabela je rigidna: unosi su fiksne širine od po 20 bajtova uključujući završetak reda, formatirani kao 10-cifreni ofset, 5-cifrena generacija, ključna reč (n za u upotrebi, f za slobodno) i dvobajtni terminator. Ispravna tabela za naših šest unosa (objekat 0 je uvek glava slobodne liste) 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 ofseti su najosetljiviji deo ručnog pisanja PDF-a. Svaki od njih je tačna pozicija bajta na kojoj počinje odgovarajući N 0 obj, i svaki ofset se pomera onog trenutka kada dodate karakter bilo gde iznad njega. Trailer je ulazna tačka koju čitač koristi poslednju i prvu: /Root 1 0 R imenuje katalog, /Size 6 navodi broj objekata, a startxref 408 daje ofset bajta same reči xref. Čitač otvara datoteku, skače na kraj, čita startxref, traži tabelu unakrsnih referenci i odatle stiže do kataloga i svega ispod njega. Oznaka %%EOF označava poslednji bajt.

Pustite alat da ispravi ofsete

Gornji ofseti su ilustrativni; u praksi će biti pogrešni do trenutka kada završite kucanje, jer zavise od tačnog rasporeda bajtova u vašoj datoteci. Umesto da ih ponovo izračunavate, napišite strukturu sa privremenim vrednostima i pustite pomoćni program da ponovo izgradi tabelu unakrsnih referenci i dužine toka. Besplatni, višeplatformski program pdftk radi ovo u jednom prolazu:

pdftk hello-draft.pdf output hello.pdf

On analizira vaše objekte, ponovo izračunava svaki bajt-ofset, popunjava ispravne vrednosti /Length, upisuje važeću xref tabelu i trailer i emituje datoteku hello.pdf. Otvorite je u bilo kom pregledaču i dobićete jednu stranicu sa tekstom „Hello, World!“ u 36-point Helvetica fontu blizu vrha. Program qpdf radi isti posao, a mnogi pregledači će takođe popraviti blago neispravnu datoteku u letu. Poenta oslanjanja na alat ovde nije lenjost; stvar je u tome što je aritmetika ofseta jedini deo formata sa nula konceptualnog sadržaja i najvećom stopom grešaka, pa automatizacija omogućava da struktura ostane ono što zapravo učite.

Zašto se ovo može primeniti i na stvarne dokumente

Ništa u izveštaju od sto stranica ne menja oblik koji ste upravo izgradili. Katalog se i dalje nalazi u korenu, stablo stranica i dalje okuplja stranice, a svaka stranica i dalje ukazuje na svoje resurse i tok sadržaja. Ono što raste jeste širina, a ne kičma dokumenta: stablo stranica se grana kako bi čitač mogao da preskoči čitava podstabla, tokovi sadržaja nose stotine operatora umesto pet, fontovi se ugrađuju kao sopstveni objekti toka sa tabelama širina i kodiranjima, a slike stižu kao tokovi sa specifičnim filterima. Moderne datoteke takođe imaju tendenciju da pakuju mnogo objekata u kompresovane tokove objekata i zamene običnu xref tabelu tokovima unakrsnih referenci, zbog čega otvaranje stvarnog PDF-a u tekstualnom uređivaču obično prikazuje zid binarnih znakova. Model ispod je identičan onom u vašoj ručno napravljenoj datoteci. Za širi graf objekata i način na koji se katalog, stablo stranica i rečnici resursa odnose u većem dokumentu, detaljan obilazak strukture PDF dokumenata nastavlja tamo gde se ovo završava, a pregled strukture datoteka pokriva inkrementalna ažuriranja i povezivanje trailera kroz revizije.

Od ručnog pisanja do biblioteke

Ručno kucanje objekata je vežba za učenje, a ne produkciona tehnika. Onog trenutka kada vam zatrebaju stvarni fontovi, prelomljeni tekst, slike ili više od jedne jednostavne stranice, vođenje evidencije bajtova koje je pdftk popravio za vas postaje ceo posao, i tai želite biblioteku koja preuzima kontrolu. Istih pet objekata se i dalje upisuje, ali biblioteka izračunava svaki ofset, upravlja rečnicima fontova i resursa i kompresuje tokove sadržaja bez potrebe da pratite ijedan bajt. U Delphi-ju i C++Builder-u, HotPDF Component svodi celu ovu datoteku na nekoliko poziva: podesite dokument, pozovete BeginDoc, SetFont i TextOut da biste postavili isti pozdrav, a zatim EndDoc da biste upisali ispravan katalog, stablo stranica, xref i trailer. Razumevanje objekata koji se nalaze ispod jeste ono što vam omogućava da razumete izlaz kada se dokument ne iscrta onako kako ste očekivali.