Technical Article

Struktura PDF datoteke: Zaglavlje, telo, xref i trailer

Čitač PDF-a ne počinje od početka datoteke. On počinje od kraja. Poslednjih nekoliko bajtova sadrži adresu svega ostalog, i parser koji ne razume taj redosled pogrešno će protumačiti format već od prve linije. Zato je najkorisniji način da naučite o PDF-u na disku da ga proučite onako kako to radi čitač: prvo rep, zatim skok unazad na mapu, pa potom razrešavanje objekata na koje mapa ukazuje.

Sami bajtovi su dovoljno jasni za čitanje u tekstualnom uređivaču kada ništa nije kompresovano. Minimalni dokument od jedne stranice koji iscrtava "Hello, World!" zauzima manje od petsto bajtova, i svaki strukturni element formata je vidljiv u njemu. Evo cele datoteke sa označenim delovima:

%PDF-1.0                          % Header
%âãÏÓ

1 0 obj                           % Body: the object sequence
<<
/Kids [2 0 R]
/Count 1
/Type /Pages
>>
endobj

2 0 obj
<<
/Rotate 0
/Parent 1 0 R
/Resources 3 0 R
/MediaBox [0 0 612 792]
/Contents [4 0 R]
/Type /Page
>>
endobj

3 0 obj
<< /Font << /F0 << /BaseFont /Times-Italic /Subtype /Type1 /Type /Font >> >> >>
endobj

4 0 obj
<< /Length 65 >>
stream
1. 0. 0. 1. 50. 700. cm BT
  /F0 36. Tf
  (Hello, World!) Tj
ET
endstream
endobj

5 0 obj
<< /Pages 1 0 R /Type /Catalog >>
endobj

xref                              % Cross-reference table
0 6
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000192 00000 n
0000000291 00000 n
0000000409 00000 n

trailer                           % Trailer
<<
/Root 5 0 R
/Size 6
>>
startxref
459
%%EOF

Četiri dela, uvek u ovom redosledu u datoteci: zaglavlje, telo sa objektima, tabela unakrsnih referenci (cross-reference table) i trailer. Kvaka je u tome što ih čitate u skoro obrnutom redosledu. Standard ISO 32000-2 §7.5.1 izlaže istu anatomiju od četiri dela, a razlog za pristup od pozadi ka napred je čisto praktičan: čitač koji skače direktno na objekat koji mu je potreban mnogo je brži od onog koji skenira svaki bajt od vrha, a taj nasumični pristup je upravo ono što trailer i tabela unakrsnih referenci treba da obezbede.

Zaglavlje ima dve linije, i druga je važna

Prva linija je %PDF-1.0. Znak procenta je čini komentarom što se sintakse tiče, ali čitači je tretiraju kao potpis datoteke (signature) i iz nje izvlače broj verzije. Rukovanje verzijama je labavo u praksi. Čitač napravljen za PDF 2.0 rado će otvoriti datoteku koja tvrdi da je 1.0, a većina čitača će pokušati da otvori datoteku čija je deklarisana verzija pogrešna ili čija je linija verzije zakopana malo dublje u datoteci umesto na nultom bajtu. Broj je nagoveštaj o tome koje funkcije treba očekivati, a ne prepreka.

Druga linija je ona koju ljudi slučajno obrišu, a zatim provedu popodne otklanjajući greške. I to je komentar, ali njegov sadržaj su četiri bajta iznad ASCII 127. Oni postoje kako bi svaki alat koji prenosi datoteku u "tekstualnom režimu" prepoznao da je ona binarna i prestao da prepisuje krajeve linija. PDF sadrži kompresovane tokove podataka (streams) čiji bajtovi slučajno mogu odgovarati oznakama za povratak na početak reda (carriage return) ili novi red (line feed); ako ih alat za prenos prepiše, dužina toka zabeležena u rečniku više ne odgovara bajtovima na disku i datoteka je oštećena. Komentar sa visokim bajtovima je četrdeset godina stara odbrana od FTP-a u ASCII režimu, i još uvek se nalazi u svakoj datoteci koju ozbiljan alat upisuje jer je neuspeh koji sprečava tih i potpun.

Telo drži objekte, od kojih je svaki numerisan

Sve što čini dokument živi u telu kao ravan niz indirektnih objekata. Svaki počinje sa dva cela broja i ključnom rečju obj, sadrži svoj sadržaj i zatvara se sa endobj. Objekat 1 u gornjem primeru je čvor stabla stranica (page-tree node): 1 0 obj, zatim rečnik, pa endobj. Prvi ceo broj je broj objekta, a drugi je broj generacije. Generacija je skoro uvek nula u sveže napisanoj datoteci; ona raste samo kada se broj objekta ponovo koristi kroz izmene, što je dovoljno retko da nenultu generaciju možete tretirati kao znak da je datoteka prošla kroz inkrementalna ažuriranja. Sadržaj između ključnih reči je u ovom slučaju rečnik, napisan između << i >>, ali to može biti i broj, string, niz ili tok podataka.

Ono što ovo čini grafom, a ne listom, jeste referentni token 2 0 R. To znači "objekat 2, generacija 0, gde god da se nalazi u datoteci". Gornji čvor stabla stranica ne sadrži svoju stranicu; on ukazuje na objekat 2, koji istim mehanizmom ukazuje na svoje resurse i tok sadržaja. Telo je raspoređeno u bilo kom redosledu koji je piscu bio zgodan, a reference ga spajaju u stablo čiji je koren u katalogu (catalog). Pozicija u datoteci nema nikakvo značenje. Identitet dolazi iz broja objekta, a lokacija iz tabele unakrsnih referenci.

Tabela unakrsnih referenci je indeks bajt-ofseta

Tabela unakrsnih referenci (xref tabela) je ono što pretvara brojeve objekata u pozicije u datoteci. To je razlog zašto čitač može otvoriti dokument od hiljadu stranica i prikazati stranicu 850 bez analiziranja prethodnih 849 stranica. Svaki unos beleži tačno gde njegov objekat počinje, računato u bajtovima od početka datoteke:

xref
0 6                  % 6 entries, starting at object 0
0000000000 65535 f   % entry 0: head of the free list
0000000015 00000 n   % object 1 begins at byte 15
0000000074 00000 n   % object 2 begins at byte 74
0000000192 00000 n   % object 3 begins at byte 192
0000000291 00000 n   % object 4 begins at byte 291
0000000409 00000 n   % object 5 begins at byte 409

Fiksna širina je namerna. Svaki unos ima tačno dvadeset bajtova: desetocifreni ofset, razmak, petocifrenu generaciju, razmak, jednokarakterni tip i dvobajtni kraj linije. Pošto su redovi ujednačeni, čitač može doći direktno do unosa za objekat n pomoću aritmetike umesto skeniranja, pa je i sama tabela koja omogućava nasumičan pristup telu nasumično dostupna. Linija 0 6 je zaglavlje podsekcije: ona kaže da sledeći unosi opisuju šest objekata počevši od broja 0.

Objekat 0 je poseban i uvek prisutan. Njegov tip je f za slobodan (free), njegova generacija je 65535, i on se nalazi na čelu povezane liste slobodnih brojeva objekata. U datoteci koja nikada nije menjana, lista slobodnih objekata je samo ovaj jedan unos, formalnost. Svoju ulogu dobija tokom inkrementalnih ažuriranja, kada brisanje objekta dodaje njegov broj na tu listu kako bi ga kasnija izmena mogla ponovo iskoristiti. Ostali unosi su tipa n za u upotrebi (in-use), a njihov desetocifreni broj je ofset na koji biste se pozicionirali da biste pročitali definiciju tog objekta.

Trailer je ulazna tačka, i nalazi se na kraju

Trailer je prva stvar koju čitač zapravo konzumira, iako je napisan poslednji. Parser otvara datoteku, pozicionira se na kraj i ide unazad tražeći %%EOF. Neposredno iznad njega nalazi se startxref praćen jednim brojem, a taj broj je bajt-ofset ključne reči xref. Pomoću njega čitač skače direktno na tabelu unakrsnih referenci bez skeniranja ijednog objekta:

trailer
<<
/Root 5 0 R          % the document catalog
/Size 6
>>
startxref
459
%%EOF

Rečnik trailera (trailer dictionary) sadrži dve vrednosti koje su čitaču potrebne pre nego što može bilo šta drugo da uradi. /Root ukazuje na katalog dokumenta, ovde objekat 5, koji predstavlja vrh grafa objekata i put do stabla stranica. /Size je broj unosa koje tabela unakrsnih referenci treba da sadrži, što je za jedan više od najvećeg broja objekta zbog slobodnog unosa na poziciji nula. Od oznake %%EOF proizilazi ceo redosled čitanja: pronađi marker, pročitaj startxref da lociraš tabelu, učitaj tabelu da saznaš gde živi svaki objekat, pročitaj /Root da pronađeš katalog i odatle razrešavaj objekte po potrebi. Zaglavlje, koje se nalazi na vrhu, jedva da se konsultuje do samog kraja. Mapa na dnu je ono što čitaču treba najpre.

Inkrementalno ažuriranje dodaje drugu mapu umesto ponovnog pisanja

Taj dizajn "od repa" se isplati kada se datoteka promeni. PDF se može uređivati bez prepisivanja bajtova koji su već na disku. Novi i izmenjeni objekti se dodaju na kraj, praćeni novim odeljkom unakrsnih referenci i novim trailerom, dok se originalna datoteka ispod ostavlja netaknutom. Jedini novi deo evidencije je unos /Prev u novom traileru, koji sadrži bajt-ofset prethodne tabele unakrsnih referenci:

% ... original file, unchanged, ends here ...

6 0 obj                          % an object added by this edit
<< /Type /Annot /Subtype /Text /Rect [100 700 120 720] >>
endobj

xref                             % a second xref section, for the new object only
6 1
0000000612 00000 n

trailer
<<
/Root 5 0 R
/Size 7
/Prev 459                        % byte offset of the earlier xref table
>>
startxref
680                              % offset of this new xref section
%%EOF

Čitač i dalje počinje od poslednjeg %%EOF, i dalje prati startxref do najnovije tabele, ali sada prati lanac /Prev unazad do starijih tabela, spajajući ih tako da najnoviji unos za bilo koji broj objekta pobeđuje. Odeljci unakrsnih referenci formiraju povezanu listu naniže kroz datoteku, pri čemu svaki odeljak nadjačava prethodni za objekte koje dodiruje. Objekat koji je izmena zamenila i dalje fizički postoji na svom starom ofsetu; on jednostavno više nije dostupan, jer kasniji xref unos ukazuje na novije mesto.

Ovo je mehanizam koji potpisane PDF-ove čini proverljivim. Digitalni potpis pokriva opseg bajtova datoteke, a pošto se inkrementalno ažuriranje samo nadovezuje, potpisani bajtovi se nikada ne pomeraju. Potpis i dalje važi za originalni opseg, dok se kasnije revizije nalaze izvan njega, svaka sa svojim xref-om i trailerom. To je takođe razlog zašto PDF može da nosi istoriju koja se može povratiti: svaki zamenjeni objekat je još uvek na disku pod ranijim odeljkom unakrsnih referenci, što je korisna funkcija za praćenje verzija, ali i problem za svakoga ko je mislio da "brisanje" znači da su bajtovi nestali.

Cena toga je rast datoteke. Svaka izmena se nadodaje; ništa se ne oslobađa na licu mesta, tako da datoteka koja je menjana mnogo puta akumulira mrtve objekte i dug lanac xref odeljaka. Rešenje je potpuno ponovno pisanje: učitajte dokument i sačuvajte ga ponovo, što ponovo numeriše preživele objekte, odbacuje nedostupne i emituje jednu čistu tabelu unakrsnih referenci. Ove dve strategije direktno konkurišu jedna drugoj. Dodavanje je brzo i čuva potpise i istoriju; ponovno pisanje je sporije i odbacuje oboje, u zamenu za kompaktnu datoteku.

Čitanje četiri dela u praksi

Poznavanje rasporeda je dovoljno za ručno otklanjanje većine problema tipa "ova datoteka se ne može otvoriti". Ako čitač odbije PDF, uobičajeni krivci su na krajevima, a ne u sredini. Prekinuto preuzimanje gubi trailer, pa nedostaje startxref ili %%EOF i čitač nema ulaznu tačku; tolerantni čitači se vraćaju na skeniranje cele datoteke kako bi ponovo izgradili xref, što je upravo spora putanja koju je tabela trebalo da izbegne. Loš prenos u tekstualnom režimu oštećuje bajtove toka podataka ili ofseti prestaju da odgovaraju stvarnosti, pa se objekti učitavaju sa pogrešne pozicije. Kada ofseti u tabeli više ne ukazuju na stvarne ključne reči obj, datoteka je strukturno neispravna čak i ako je svaki objekat pojedinačno u redu.

Za novi kod, pouka ovog rasporeda je da biblioteci prepustite vođenje računa o bajtovima. Ofseti u tabeli unakrsnih referenci moraju se u bajt slagati sa stvarnim pozicijama svakog objekta, trailer mora ukazivati na pravu tabelu, a inkrementalna ažuriranja moraju se ispravno povezivati kroz /Prev. Izvorna komponenta kao što je HotPDF komponenta za Delphi i C++Builder upravlja svim tim kada piše datoteku, uključujući izbor između dodavanja inkrementalne revizije i pisanja kompaktne verzije. Ako želite da vidite istu strukturu izgrađenu od nule umesto analizirane, prateći tekst o izgradnji jednostavnog PDF dokumenta od nule vodi vas kroz emitovanje zaglavlja, objekata, xref-a i trailera po redosledu.