Technical Article

Struktura datotek PDF: glava, telo, xref in napovednik

Bralnik PDF ne začne brati datoteke na njenem začetku. Začne na koncu. Zadnjih nekaj bajtov vsebuje naslove vseh ostalih delov in parser, ki ne razume tega vrstnega reda, bo napačno prebral format že pri prvi vrstici. Zato je najbolj uporaben način za učenje zgradbe PDF-ja na disku enak bralnikovemu: najprej konec, nato skok nazaj na kazalo in nazadnje razreševanje objektov, na katere kaže kazalo.

Sami bajti so povsem berljivi v urejevalniku besedil, če vsebina ni stisnjena. Minimalen enostranski dokument, ki izriše "Hello, World!", meri manj kot petsto bajtov, v njem pa so vidni vsi strukturni elementi formata. Tukaj je celotna datoteka z označenimi štirimi deli:

%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

Štirje deli si po datoteki vedno sledijo v tem vrstnem redu: glava, telo z objekti, tabela navzkrižnih sklicev (xref) in napovednik (trailer). Past pa je v tem, da jih berete v skoraj obratnem vrstnem redu. Standard ISO 32000-2 §7.5.1 opisuje to enako štiridelno zgradbo, razlog za branje od konca proti začetku pa je povsem praktičen: bralnik, ki skoči neposredno na zahtevani objekt, je veliko hitrejši od tistega, ki pregleduje vsak bajt od začetka. Ta naključni dostop (random access) pa omogočata prav napovednik in tabela navzkrižnih sklicev.

Glava je sestavljena iz dveh vrstic in druga je zelo pomembna

Prva vrstica je %PDF-1.0. Znak za odstotek jo z vidika sintakse spremeni v komentar, vendar jo bralniki obravnavajo kot podpis datoteke in iz nje preberejo različico. Obravnava različic je v praksi ohlapna. Bralnik, zasnovan za PDF 2.0, bo brez težav odprl datoteko, ki navaja različico 1.0, večina bralnikov pa bo poskusila prebrati tudi datoteko z napačno različico ali tisto, katere vrstica z različico se ne nahaja na samem začetku, temveč nekoliko globlje v datoteki. Številka je le namig o tem, katere funkcije pričakovati, in ne ovira.

Druga vrstica je tista, ki jo ljudje pogosto nenamerno izbrišejo in nato cel popoldan iščejo napako. Prav tako gre za komentar, vendar vsebuje štiri bajte z vrednostjo nad ASCII 127. Ti obstajajo zato, da orodja za prenos prepoznajo datoteko kot binarno in je ne prenašajo v "besedilnem načinu", ki spreminja konce vrstic. PDF vsebuje stisnjene tokove, katerih bajti se lahko naključno ujemajo z znakom za vrnitev na začetek vrstice (CR) oz. novo vrstico (LF); če jih prenosno orodje spremeni, se dolžina toka v slovarju ne ujema več z dejanskimi bajti in datoteka se poškoduje. Ta komentar z bajti visoke vrednosti je že štiri desetletja stara obramba pred prenosom FTP v načinu ASCII, in je še vedno prisoten v vsaki datoteki, ki jo ustvari resno orodje, saj je napaka, ki jo preprečuje, tiha in popolna.

Telo vsebuje objekte, od katerih je vsak oštevilčen

Vse, kar sestavlja dokument, živi v telesu kot zaporedje posrednih objektov. Vsak se začne z dvema celima številoma in ključno besedo obj, vsebuje podatke in se zaključi z endobj. Objekt 1 v zgornjem vzorcu je vozlišče drevesa strani: 1 0 obj, slovar in nato endobj. Prvo celo število je številka objekta, drugo pa številka generacije. Številka generacije je v novi datoteki skoraj vedno enaka nič; poveča se le ob ponovni uporabi številke objekta pri urejanju, kar je dovolj redko, da lahko neničelno generacijo smatrate za znak, da je bila datoteka posodobljena postopno. Vsebina med ključnima besedama je v tem primeru slovar, zapisan med << in >>, lahko pa bi bila tudi številka, niz, polje ali tok.

Tisto, kar iz tega naredi graf in ne navadnega seznama, je žeton sklica 2 0 R. To pomeni "objekt 2, generacije 0, kjer koli v datoteki se že nahaja". Zgoraj navedeno vozlišče drevesa strani ne vsebuje same strani, temveč kaže na objekt 2, ta pa z enakim mehanizmom kaže na svoje vire in tok vsebine. Telo je zapisano v vrstnem redu, ki je bil priročen za pisalnik, sklici pa ga povežejo v drevo s korenskim objektom v katalogu. Položaj v datoteki nima nobenega pomena. Identiteta izhaja iz številke objekta, lokacija pa iz tabele navzkrižnih sklicev.

Tabela navzkrižnih sklicev (xref) je kazalo bajtnih odmikov

Tabela xref pretvori številke objektov v položaje v datoteki. To je razlog, da lahko bralnik odpre dokument s tisoč stranmi in izriše 850. stran, ne da bi razčlenil 849 strani pred njo. Vsak vnos natančno beleži začetek svojega objekta, šteto v bajtih od zač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 vrstic je namerna. Vsak vnos obsega natanko dvajset bajtov: desetmestni odmik, presledek, petmestna generacija, presledek, enoznakovni tip in dvobajtni zaključek vrstice. Ker so vrstice enako dolge, lahko bralnik z aritmetičnim računom neposredno določi položaj vnosa za objekt *n* brez iskanja po celotni tabeli, kar pomeni, da je sama tabela naključno dostopna. Vrstica 0 6 je glava pododdelka: sporoča, da naslednji vnosi opisujejo šet objektov, začenši s številko 0.

Objekt 0 je poseben in vedno prisoten. Njegov tip je f (free - prost), njegova generacija je 65535, nahaja pa se na čelu povezanega seznama prostih številk objektov. V datoteki, ki nikoli ni bila urejana, je ta seznam le ta ena formalna vrstica. Svoj namen dobi pri postopnih posodobitvah, ko brisanje objekta doda njegovo številko na ta seznam, tako da jo lahko kasnejše urejanje ponovno uporabi. Preostali vnosi so tipa n (in-use - aktiven), njihova desetmestna številka pa je odmik, na katerega morate skočiti, da preberete definicijo tega objekta.

Napovednik (trailer) je vstopna točka in se nahaja na koncu

Napovednik (trailer) je prvi del, ki ga bralnik dejansko prebere, čeprav je zapisan na koncu. Parser odpre datoteko, skoči na njen konec in išče oznako %%EOF nazaj grede. Tik nad njo se nahaja ukaz startxref, ki mu sledi ena številka, ta številka pa je bajtni odmik same ključne besede xref. S to številko bralnik skoči neposredno na tabelo navzkrižnih sklicev, ne da bi prebral en sam objekt:

trailer
<<
/Root 5 0 R          % the document catalog
/Size 6              % one more than the highest object number
>>
startxref
459                  % byte offset of the xref table
%%EOF

Slovar napovednika vsebuje dve vrednosti, ki ju bralnik potrebuje pred začetkom kakršnega koli dela. Vnos /Root kaže na katalog dokumenta (tukaj objekt 5), ki predstavlja vrh grafa objektov in pot do drevesa strani. Vnos /Size določa število vnosov, ki jih mora vsebovati tabela navzkrižnih sklicev, kar je za eno več od najvišje številke objekta zaradi prostega vnosa na mestu nič. Iz oznake %%EOF izhaja celotno zaporedje branja: poišči oznako, preberi startxref za lokacijo tabele, naloži tabelo za lokacije objektov, preberi /Root za dostop do kataloga in razrešuj objekte po potrebi. Glava na vrhu datoteke se uporabi šele kasneje. Kazalo na dnu je tisto, kar bralnik potrebuje najprej.

Postopna posodobitev doda novo kazalo namesto ponovnega zapisovanja

Zasnova branja od konca se obrestuje ob spremembi datoteke. PDF je mogoče urediti brez ponovnega zapisovanja bajtov, ki so že na disku. Novi in spremenjeni objekti se dodajo na konec datoteke, sledita pa jim nova tabela navzkrižnih sklicev in nov napovednik, medtem ko prvotni del datoteke spodaj ostane nedotaknjen. Edini novi strukturni podatek je vnos /Prev v novem napovedniku, ki hrani bajtni odmik prejšnje tabele navzkrižnih sklicev:

% ... 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

Bralnik še vedno začne pri zadnji oznaki %%EOF in sledi startxref do najnovejše tabele, vendar zdaj sledi verigi /Prev nazaj do starejših tabel in jih združi, pri čemer prevlada najnovejši vnos za posamezno številko objekta. Oddelki navzkrižnih sklicev tvorijo povezan seznam skozi celotno datoteko, kjer vsak naslednji prepiše prejšnjega za tiste objekte, ki jih spreminja. Objekt, ki ga je urejanje nadomestilo, fizično še vedno obstaja na svojem starem odmiku, vendar preprosto ni več dosegljiv, saj kasnejši vnos v tabeli xref kaže na novejši položaj.

To je mehanizem, ki omogoča preverjanje podpisanih datotek PDF. Digitalni podpis pokriva določeno bajtno območje datoteke in ker postopna posodobitev le dodaja podatke na konec, se podpisano območje nikoli ne premakne. Podpis ostane veljaven za prvotno območje, medtem ko se kasnejše spremembe nahajajo za njim, vsaka z lastno tabelo xref in napovednikom. To je tudi razlog, zakaj lahko PDF vsebuje obnovljivo zgodovino: vsak nadomeščeni objekt je še vedno na disku pod starejšo tabelo navzkrižnih sklicev. To je koristna lastnost za sledenje različicam in hkrati nevarnost za tiste, ki so mislili, da "izbrisati" pomeni odstraniti bajte iz datoteke.

Cena tega je naraščanje velikosti. Vsako urejanje le dodaja podatke; nič se ne sprosti na mestu samem, zato datoteka, ki je bila večkrat urejana, kopiči neaktivne objekte in dolgo verigo delov xref. Rešitev je celotno ponovno zapisovanje: naložite dokument in ga na novo shranite, kar ponovno oštevilči preživele objekte, zavrže nedosegljive in ustvari eno čisto tabelo navzkrižnih sklicev. Ti dve strategiji si neposredno nasprotujeta: dodajanje je hitro ter ohranja podpise in zgodovino, ponovno zapisovanje pa je počasnejše in oboje zavrže v zameno za kompaktno datoteko.

Branje štirih delov v praksi

Poznavanje te postavitve zadošča za ročno iskanje napak pri večini težav z odpiranjem datotek. Če bralnik zavrne PDF, so običajni krivci na obeh koncih in ne v sredini. Nedokončan prenos izgubi napovednik, zato manjkata startxref ali %%EOF, bralnik pa nima vstopne točke. Bolj tolerantni bralniki v tem primeru preiščejo celotno datoteko in ponovno zgradijo tabelo xref, kar pa je natanko tista počasi pot, ki se ji je tabela želela izogniti. Napačen prenos v besedilnem načinu poškoduje bajte tokov ali pa odmiki ne ustrezajo več realnosti, zato se objekti naložijo z napačnih položajev. Ko odmiki v tabeli ne kažejo več na dejanske ključne besede obj, je datoteka strukturno poškodovana, četudi je vsak objekt posebej povsem v redu.

Za novo kodo pa velja naslednje pravilo: vodenje bajtnih podatkov prepustite namenski knjižnici. Odmiki v tabeli navzkrižnih sklicev se morajo do bajta natančno ujemati z dejanskimi položaji vsakega objekta, napovednik mora kazati na pravilno tabelo, postopne posodobitve pa se morajo pravilno povezovati prek /Prev. Izvorna knjižnica, kot je HotPDF Component za Delphi in C++Builder, poskrbi za vse to ob zapisu datoteke, vključno z izbiro med dodajanjem postopne posodobitve in ponovnim zapisom kompaktne datoteke. Če želite videti isto strukturo zgrajeno od začetka in ne le analizirano, si preberite spremljevalni članek o gradnji dokumenta PDF od začetka, ki vas popelje skozi pisanje glave, objektov, tabele xref in napovednika po vrstnem redu.