PDF čitač ne počinje od početka datoteke. Počinje od kraja. Posljednjih nekoliko bajtova sadrži adresu svega ostaloga, a parser koji ne razumije taj redoslijed pogrešno će pročitati format već od prve linije. Stoga je najkorisniji način za učenje o PDF-u na disku učenje onako kako to radi čitač: prvo rep, zatim skok unatrag na mapu, a potom razrješavanje objekata na koje mapa upućuje.
Sami bajtovi prilično su jednostavni za čitanje u uređivaču teksta kada ništa nije komprimirano. Minimalni dokument od jedne stranice koji ispisuje "Hello, World!" zauzima manje od petsto bajtova, a u njemu su vidljivi svi strukturni elementi formata. Evo cijele datoteke s označenim četirima dijelovima:
%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 dijela, uvijek ovim redoslijedom niz datoteku: zaglavlje, tijelo objekata, tablica unakrsnih referenci i trailer. Kvaka je u tome što ih čitate gotovo obrnutim redoslijedom. ISO 32000-2 §7.5.1 opisuje istu anatomiju od četiri dijela, a razlog za pristup od kraja prema početku čisto je praktičan: čitač koji skače izravno na objekt koji mu je potreban puno je brži od onog koji skenira svaki bajt od vrha, a taj izravan pristup (random access) upravo je ono što trailer i tablica unakrsnih referenci trebaju osigurati.
Zaglavlje ima dva retka, a drugi je važan
Prvi redak je %PDF-1.0. Znak postotka ga čini komentarom što se tiče sintakse, ali ga čitači tretiraju kao potpis datoteke i iz njega izvlače broj verzije. Rukovanje verzijama je u praksi labavo. Čitač napravljen za PDF 2.0 rado će otvoriti datoteku koja tvrdi da je 1.0, a većina čitača će pokušati otvoriti datoteku čija je deklarirana verzija pogrešna ili čiji je redak verzije malo zakopan u datoteci umjesto na bajtu nula. Broj je samo naznaka koje značajke možete očekivati, a ne prepreka.
Drugi redak je onaj koji ljudi slučajno izbrišu, a zatim provedu poslijepodne u debugiranju. I to je komentar, ali njegov sadržaj su četiri bajta iznad ASCII vrijednosti 127. Oni postoje kako bi bilo koji alat koji prenosi datoteku u tekstualnom načinu rada prepoznao da je ona binarna i prestao prepisivati završetke redaka. A PDF prenosi komprimirane tokove podataka čiji se bajtovi slučajno mogu podudarati s prijelazom u novi red; ako ih alat za prijenos prepiše, duljina toka zabilježena u rječniku više ne odgovara bajtovima na disku i datoteka se ošteti. Komentar s visokim bajtovima je četrdeset godina stara obrana od FTP-a u ASCII načinu rada i još uvijek se nalazi u svakoj datoteci koju ozbiljan alat zapisuje jer je kvar koji sprječava tih i potpun.
Tijelo sadrži objekte, od kojih je svaki numeriran
Sve što čini dokument nalazi se u tijelu kao ravan niz neizravnih objekata. Svaki počinje s dva cijela broja i ključnom riječi obj, sadrži svoj sadržaj i završava s endobj. Objekt 1 u gornjem uzorku je čvor stabla stranica: 1 0 obj, zatimi rječnik, pa endobj. Prvi cijeli broj je broj objekta, a drugi je broj generacije. Generacija je gotovo uvijek nula u svježe zapisanoj datoteci; raste samo kada se broj objekta ponovno koristi kroz uređivanja, što je dovoljno rijetko da se nenulta generacija može tretirati kao znak da je datoteka prošla kroz inkrementalna ažuriranja. Sadržaj između ključnih riječi ovdje je rječnik, zapisan između << i >>, ali to može biti i broj, niz znakova, polje ili tok podataka.
Ono što ovo čini grafom, a ne popisom, jest referentni token 2 0 R. To znači "objekt 2, generacija 0, gdje god se nalazio u datoteci". Gornji čvor stabla stranica ne sadrži svoju stranicu; on upućuje na objekt 2, koji istim mehanizmom upućuje na svoje resurse i tok sadržaja. Tijelo je raspoređeno onim redoslijedom koji je piscu bio prikladan, a reference ga povezuju u stablo čiji je korijen u katalogu. Položaj u datoteci nema nikakvo značenje. Identitet dolazi od broja objekta, a lokacija iz tablice unakrsnih referenci.
Tablica unakrsnih referenci je indeks bajt pomaka
Tablica unakrsnih referenci (xref) je ono što pretvara brojeve objekata u položaje u datoteci. To je razlog zašto čitač može otvoriti dokument od tisuću stranica i prikazati stranicu 850 bez parsiranja 849 stranica prije nje. Svaki unos bilježi točno gdje počinje njegov objekt, računajući 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 namjerna. Svaki unos ima točno dvadeset bajtova: deseteroznamenkasti pomak, razmak, peteroznamenkastu generaciju, razmak, jednoznamenkastu vrstu i dvobajtni završetak retka. Budući da su retci ujednačeni, čitač može aritmetikom doći izravno do unosa za objekt n umjesto skeniranjem, pa je tablica koja daje izravan pristup tijelu i sama izravno dostupna. Redak 0 6 je zaglavlje pododjeljka: kaže da sljedeći unosi opisuju šest objekata koji počinju brojem 0.
Objekt 0 je poseban i uvijek prisutan. Njegova vrsta je f (free - slobodan), njegova generacija je 65535, i on se nalazi na čelu povezane liste slobodnih brojeva objekata. U datoteci koja nikada nije uređivana, lista slobodnih objekata je samo ovaj jedan unos, formalnost. Svoju svrhu ispunjava tijekom inkrementalnih ažuriranja, kada brisanje objekata dodaje njegov broj na tu listu kako bi ga kasnije uređivanje moglo ponovno iskoristiti. Ostali unosi su vrste n (in-use - u upotrebi), a njihov deseteroznamenkasti broj je pomak na koji trebate skočiti kako biste pročitali definiciju tog objekta.
Trailer je ulazna točka i nalazi se na kraju
Trailer je prva stvar koju čitač zapravo konzumira, iako se zapisuje na kraju. Parser otvara datoteku, traži kraj i ide unatrag tražeći %%EOF. Neposredno iznad toga nalazi se startxref iza kojeg slijedi jedan broj, a taj broj je bajt pomak ključne riječi xref. Pomoću njega čitač skače izravno na tablicu unakrsnih referenci bez skeniranja ijednog objekta:
trailer
<<
/Root 5 0 R % the document catalog
/Size 6
>>
startxref
459 % byte offset of the xref table
%%EOF
Rječnik trailera nosi dvije vrijednosti koje čitač treba prije nego što može učiniti bilo što drugo. /Root upućuje na katalog dokumenta, ovdje objekt 5, koji predstavlja vrh grafa objekata i put do stabla stranica. /Size je broj unosa koje tablica unakrsnih referenci treba sadržavati, što je za jedan više od najvećeg broja objekta zbog slobodnog unosa na poziciji nula. Iz %%EOF proizlazi cijeli slijed čitanja: pronađi marker, pročitaj startxref za lociranje tablice, učitaj tablicu kako bi saznao gdje svaki objekt živi, pročitaj /Root za pronalaženje kataloga i od tamo razrješavaj objekte na zahtjev. Zaglavlje na samom vrhu jedva se konzultira do kasne faze. Karta na dnu je ono što čitač treba najprije.
Inkrementalan update dodaje drugu mapu umjesto prepisivanja
Taj dizajn od kraja prema početku isplati se kada se datoteka promijeni. PDF se može uređivati bez prepisivanja ijednog bajta koji je već na disku. Novi i izmijenjeni objekti dodaju se na kraj, nakon čega slijedi novi odjeljak unakrsnih referenci i novi trailer, a izvorna datoteka ispod ostaje netaknuta. Jedina nova stavka u vođenju evidencije je unos /Prev u novom traileru, koji sadrži bajt pomak prethodne tablice 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 posljednjeg %%EOF, i dalje prati startxref do najnovije tablice, ali sada prati lanac /Prev unatrag do starijih tablica, spajajući ih tako da pobjeđuje najnoviji unos za bilo koji broj objekta. Odjeljci unakrsnih referenci čine povezanu listu kroz datoteku, pri čemu svaki nadjačava prethodni za objekte koje dotiče. Objekt koji je uređivanjem zamijenjen i dalje fizički postoji na svom starom pomaku; jednostavno više nije dostupan, jer kasniji xref unos upućuje na novije mjesto.
To je mehanizam koji potpisane PDF-ove čini provjerljivima. Digitalni potpis pokriva raspon bajtova datoteke, a budući da se inkrementalnim ažuriranjem samo dodaje na kraj, potpisani bajtovi nikada se ne miču. Potpis se i dalje potvrđuje u odnosu na izvorni raspon, dok kasnije revizije leže izvan njega, svaka sa svojim xref-om i trailerom. To je također razlog zašto PDF može nositi povijest koja se može oporaviti: svaki zamijenjeni objekt i dalje je na disku pod ranijim odjeljkom unakrsnih referenci, što je prednost za praćenje verzija, a problem za svakoga tko je mislio da "brisanje" znači da su bajtovi nestali.
Cijena je rast datoteke. Svako uređivanje dodaje sadržaj; ništa se ne oslobađa na licu mjesta, pa datoteka koja je mijenjana mnogo puta nakuplja mrtve objekte i dugi lanac xref odjeljaka. Lijek je potpuno prepisivanje: učitajte dokument i spremite ga iznova, što ponovno numerira preživjele objekte, odbacuje one nedostupne i emitira jednu čistu tablicu unakrsnih referenci. Ove dvije strategije izravno se nadopunjuju. Dodavanje je brzo i čuva potpise i povijest; prepisivanje je sporije i odbacuje oboje, u zamjenu za kompaktnu datoteku.
Čitanje četiriju dijelova u praksi
Poznavanje rasporeda dovoljno je za ručno debugiranje većine problema s otvaranjem datoteka. 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 točku; tolerantni čitači vraćaju se na skeniranje cijele datoteke kako bi ponovno izgradili xref, što je upravo spori put koji je tablica trebala izbjeći. Neuspješan prijenos u tekstualnom načinu rada oštećuje bajtove toka ili pomaci prestaju odgovarati stvarnosti, pa se objekti učitavaju s pogrešnog položaja. Kada pomaci u tablici više ne upućuju na stvarne ključne riječi obj, datoteka je strukturalno slomljena čak i ako je svaki objekt pojedinačno u redu.
Za novi kod, pouka ovog rasporeda jest prepustiti knjižnici brigu o bajtovima. Pomaci u tablici unakrsnih referenci moraju se podudarati sa stvarnim položajima svakog objekta u bajt, trailer mora upućivati na ispravnu tablicu, a inkrementalna ažuriranja moraju se ispravno povezivati kroz /Prev. Izvorna komponenta poput HotPDF Component za Delphi i C++Builder upravlja svim time kada zapisuje datoteku, uključujući izbor između dodavanja inkrementalne revizije i zapisivanja kompaktne datoteke. Ako želite vidjeti istu strukturu izgrađenu od nule umjesto seciranu, prateći članak o izgradnji PDF dokumenta od nule vodi vas kroz izdavanje zaglavlja, objekata, xref-a i trailera redom.