Technical Article

PDF failo struktūra: Header, Body, Xref ir Trailer

PDF skaitytuvas nepradeda darbo nuo failo pradžios. Jis pradeda nuo pabaigos. Paskutiniuose keliuose baituose yra visų kitų elementų adresai, o sintaksės analizatorius (parser), kuris nesupranta šios tvarkos, neteisingai perskaitys formatą nuo pat pirmos eilutės. Todėl naudingiausias būdas suprasti PDF struktūrą diske – nagrinėti ją taip, kaip tai daro skaitytuvas: pirmiausia pabaigą, tada grįžti atgal į žemėlapį ir galiausiai pasiekti objektus, į kuriuos tas žemėlapis rodo.

Patys baitai yra lengvai įskaitomi tekstų redagavimo programa, kai niekas nėra suspausta. Minimalus vieno puslapio dokumentas, kuriame išvedamas užrašas „Hello, World!“, užima mažiau nei penkis šimtus baitų, ir jame matomas kiekvienas struktūrinis šio formato elementas. Štai visas failas su pažymėtomis keturiomis dalimis:

%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

Keturios dalys faile visada išdėstytos šia tvarka: antraštė (header), objektų kūnas (body), kryžminių nuorodų lentelė (xref) ir pabaigos blokas (trailer). Tačiau esmė ta, kad jas skaitote beveik atvirkštine tvarka. Standartas ISO 32000-2 §7.5.1 aprašo tą pačią keturių dalių anatomiją, o priežastis, kodėl prieiga vykdoma iš pabaigos į pradžią, yra grynai praktinė: skaitytuvas, kuris šoka tiesiai prie reikiamo objekto, veikia daug greičiau nei tas, kuris skenuoja kiekvieną baitą nuo pat viršaus. Būtent šiai atsitiktinei prieigai (random access) užtikrinti ir buvo sukurti „trailer“ bei kryžminių nuorodų lentelė.

Antraštė susideda iš dviejų eilučių, ir antroji yra labai svarbi

Pirmoji eilutė yra %PDF-1.0. Procento ženklas paverčia ją komentaru sintaksės požiūriu, tačiau skaitytuvai traktuoja ją kaip failo parašą ir iš jos ištraukia versijos numerį. Praktikoje versijų palaikymas yra lankstus. Skaitytuvas, sukurtas PDF 2.0 versijai, be problemų atidarys failą, kuriame nurodyta 1.0 versija, o dauguma skaitytuvų bandys atidaryti failą, net jei jo deklaruota versija yra klaidinga arba versijos eilutė prasideda ne nuo nulinio baito, o yra šiek tiek pastumta giliau į failą. Šis numeris yra užuomina apie tai, kokių funkcijų tikėtis, o ne griežtas ribojimas.

Antroji eilutė yra ta, kurią žmonės netyčia ištrina, o paskui praleidžia pusdienį ieškodami klaidos. Tai taip pat yra komentaras, tačiau jo turinį sudaro keturi baitai, kurių reikšmės viršija ASCII 127. Jie reikalingi tam, kad bet kokia sistema, perkelianti failą „tekstiniu režimu“, atpažintų jį kaip dvejetainį ir nustotų keisti eilučių pabaigos simbolius. PDF faile yra suspaustų srautų, kurių baitai atsitiktinai gali sutapti su karietėlės grąžinimo (carriage return) arba naujos eilutės (line feed) simboliais. Jei perkėlimo įrankis juos pakeičia, žodyne nurodytas srauto ilgis nebesutaps su tikruoju baitų skaičiumi diske, ir failas bus sugadintas. Šis aukštų baitų komentaras yra prieš keturiasdešimt metų sukurtas apsaugos būdas nuo FTP perdavimo ASCII režimu, ir jis vis dar naudojamas kiekviename rimtų įrankių įrašomame faile, nes apsaugo nuo nematomos, bet visiškos dokumento sugadinimo klaidos.

Kūne saugomi objektai, kurių kiekvienas yra sunumeruotas

Viskas, iš ko susideda dokumentas, yra saugoma kūne kaip plokščia netiesioginių objektų seka. Kiekvienas objektas prasideda dviem sveikaisiais skaičiais bei raktiniu žodžiu obj, turi savo turinį ir baigiasi endobj. Pavyzdžiui, aukščiau pateiktame pavyzdyje 1 objektas yra puslapių medžio mazgas: 1 0 obj, tada žodynas ir endobj. Pirmasis sveikasis skaičius yra objekto numeris, antrasis – kartos (generation) numeris. Naujai įrašytame faile kartos numeris beveik visada yra nulis. Jis didėja tik tada, kai objekto numeris yra panaudojamas pakartotinai atliekant redagavimus, o tai nutinka pakankamai retai, todėl nenulinį kartos numerį galite laikyti požymiu, kad failas patyrė dalinių atnaujinimų. Turinys tarp raktinių žodžių šiame pavyzdyje yra žodynas, rašomas tarp << ir >>, tačiau tai gali būti ir skaičius, eilutė, masyvas ar srautas.

Šią seką grafu, o ne sąrašu paverčia nuorodos elementas (reference token) 2 0 R. Tai reiškia: „2 objektas, 0 karta, kad ir kurioje failo vietoje jis būtų“. Puslapių medžio mazgas neturi savyje paties puslapio – jis rodo į 2 objektą, kuris tuo pačiu mechanizmu rodo į savo išteklius bei turinio srautą. Kūnas išdėstomas tokia tvarka, kokia buvo patogi kūrimo programai, o nuorodos sujungia jį į medį, kurio šaknis yra katalogas (catalog). Vieta faile neturi jokios reikšmės. Identitetas nustatomas pagal objekto numerį, o tikroji vieta – pagal kryžminių nuorodų lentelę.

Kryžminių nuorodų lentelė yra baitų poslinkių indeksas

Kryžminių nuorodų lentelė (xref) yra tai, kas objekto numerius paverčia vietomis faile. Būtent dėl šios lentelės skaitytuvas gali atidaryti tūkstančio puslapių dokumentą ir atvaizduoti 850 puslapį, neanalizuodamas prieš tai einančių 849 puslapių. Kiekviename įraše fiksuojama, kur tiksliai prasideda atitinkamas objektas, skaičiuojant baitais nuo failo pradžios:

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

Fiksuotas plotis yra pasirinktas sąmoningai. Kiekvieną įrašą sudaro lygiai dvidešimt baitų: dešimties skaitmenų poslinkis, tarpas, penkių skaitmenų kartos numeris, tarpas, vieno simbolio tipas ir dviejų baitų eilutės pabaigos žymė. Kadangi eilutės yra vienodos, skaitytuvas gali pasiekti objekto n įrašą atlikdamas paprastus aritmetinius veiksmus, o ne skenuodamas failą, todėl pati lentelė, suteikianti atsitiktinę prieigą prie objekto kūno, yra pasiekiama atsitiktine tvarka. Eilutė 0 6 yra poskyrio antraštė: ji nurodo, kad toliau einantys įrašai aprašo šešius objektus pradedant numeriu 0.

Objektas 0 yra ypatingas ir visada yra faile. Jo tipas yra f (laisvas – free), karta – 65535, ir jis pradeda susietąjį laisvų objektų numerių sąrašą. Faile, kuris niekada nebuvo redaguotas, laisvųjų objektų sąrašą sudaro tik šis vienas įrašas (tai tik formalumas). Jis tampa naudingas atliekant dalinius atnaujinimus, kai ištrinant objektą jo numeris pridedamas prie šio sąrašo, kad vėlesnis redagavimas galėtų jį panaudoti iš naujo. Kiti įrašai yra n tipo (naudojami – in-use), o jų dešimties skaitmenų skaičius nurodo poslinkį, kurį reikia pasiekti, norint perskaityti to objekto apibrėžimą.

Trailer yra įėjimo taškas, esantis failo pabaigoje

Pabaigos blokas (trailer) yra pirmasis dalykas, kurį skaitytuvas iš tikrųjų apdoroja, nors jis ir yra įrašomas paskutinis. Analizatorius atidaro failą, nukreipia rodyklę į pabaigą ir eina atgal, ieškodamas žymės %%EOF. Tiesiai virš jos yra startxref raktinis žodis ir vienas skaičius, kuris nurodo kryžminių nuorodų lentelės pradžios vietą baitais (poslinkį). Turėdamas šią reikšmę, skaitytuvas šoka tiesiai prie kryžminių nuorodų lentelės, neanalizavęs nė vieno objekto:

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

Trailer žodyne pateikiamos dvi reikšmės, kurių skaitytuvui reikia prieš pradedant bet kokį kitą darbą. Nuoroda /Root rodo į dokumento katalogą (šiuo atveju 5 objektą), kuris yra objektų grafo viršūnė ir kelias į puslapių medį. Nuoroda /Size nurodo kryžminių nuorodų lentelės įrašų skaičių, kuris yra vienetu didesnis už didžiausią objekto numerį dėl laisvojo įrašo nulinėje pozicijoje. Nuo žymos %%EOF prasideda visa skaitymo seka: randama žymė, perskaitoma startxref reikšmė lentelės vietai nustatyti, įkeliama lentelė, kad būtų žinoma, kur yra kiekvienas objektas, perskaitoma /Root nuoroda katalogui rasti, ir iš ten pagal poreikį pasiekiami objektai. Viršuje esanti antraštė beveik nenaudojama iki pat vėlyvųjų etapų. Žemėlapis apačioje yra tai, ko skaitytuvui reikia pirmiausia.

Dalinis atnaujinimas prideda antrą žemėlapį, o ne perrašo failą

Ši konstrukcija, orientuota į failo pabaigą, pasiteisina keičiantis failui. PDF galima redaguoti neperrašant jau diske esančių baitų. Nauji ir pakeisti objektai pridedami prie pabaigos, po to eina naujas kryžminių nuorodų skyrius bei naujas „trailer“ blokas, o pradinis failo turinys lieka nepaliestas. Vienintelis naujas apskaitos elementas yra įrašas /Prev naujame „trailer“ bloke, kuriame saugomas ankstesnės kryžminių nuorodų lentelės poslinkis baitais:

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

Skaitytuvas vis tiek pradeda darbą nuo galutinės žymos %%EOF, seka startxref nuoroda iki naujausios lentelės, tačiau dabar seka /Prev grandine atgal iki senesnių lentelių ir jas sujungia taip, kad laimėtų naujausias bet kurio objekto numerio įrašas. Kryžminių nuorodų skyriai sudaro susietąjį sąrašą faile, o kiekvienas iš jų pakeičia prieš tai buvusį tų objektų, kuriuos jis paliečia, įrašą. Objektas, kurį pakeitė redagavimas, vis dar visiškai egzistuoja savo senojoje vietoje – jis tiesiog nebėra pasiekiamas, nes vėlesnis kryžminės nuorodos įrašas rodo į naujesnę vietą.

Šis mechanizmas užtikrina pasirašytų PDF dokumentų patikrinimą. Skaitmeninis parašas apima tam tikrą failo baitų rėžį, o kadangi dalinis atnaujinimas tik prideda turinį prie pabaigos, pasirašyti baitai niekada nejuda. Parašas išlieka galiojantis pradiniam rėžiui, o vėlesnės versijos pateikiamos už jo ribų, turėdamos savo kryžminių nuorodų lenteles ir „trailer“ blokus. Dėl šios priežasties PDF dokumentas gali turėti atkuriamos istorijos duomenis: kiekvienas pakeistas objektas vis dar išlieka diske po ankstesniu kryžminių nuorodų skyriumi. Tai naudinga versijų sekimui, tačiau gali tapti problema tiems, kurie manė, kad objekto ištrynimas visiškai pašalina jo baitus.

To kaina yra failo dydžio augimas. Kiekvienas redagavimas prideda duomenis pabaigoje, niekas neatlaisvinama vietoje, todėl daug kartų redaguotame faile kaupiasi nenaudojami objektai ir ilga kryžminių nuorodų skyrių grandinė. Išeitis – visiškas failo perrašymas: dokumentas įkeliamas ir išsaugomas iš naujo, o šio proceso metu išlikę objektai pernumeruojami, nepasiekiami objektai pašalinami ir sukuriama viena švari kryžminių nuorodų lentelė. Šios dvi strategijos konkuruoja tarpusavyje: papildymas yra greitas ir išlaiko parašus bei istoriją, o perrašymas yra lėtesnis, tačiau sukuria kompaktišką failą atsisakant abiejų minėtų dalykų.

Keturių dalių skaitymas praktikoje

Struktūros išmanymo pakanka, kad rankiniu būdu nustatytumėte daugumą problemų, susijusių su neatsidarančiais failais. Jei skaitytuvas atmeta PDF failą, problemos šaltinis dažniausiai būna viename iš dviejų galų, o ne per vidurį. Dėl ne pilnai atsiųsto failo prarandamas „trailer“ blokas, todėl trūksta startxref arba %%EOF žymos, o skaitytuvas neturi įėjimo taško. Tolerantiškesnės programos bando nuskaityti visą failą, kad atkurtų kryžminių nuorodų lentelę (xref), o tai yra lėtasis kelias, kurio lentelė ir turėjo padėti išvengti. Dėl klaidingo perdavimo tekstiniu režimu sugadinami srauto baitai arba poslinkiai nebeatitinka tikrovės, todėl objektai įkeliami iš neteisingų vietų. Kai lentelėje nurodyti poslinkiai neberodo į tikrus raktinius žodžius obj, failas yra struktūriškai pažeistas, net jei kiekvienas objektas atskirai yra tvarkingas.

Kuriant naują kodą, svarbiausia pamoka – leisti bibliotekai tvarkyti baitų apskaitą. Kryžminių nuorodų lentelės poslinkiai turi baito tikslumu sutapti su tikromis kiekvieno objekto pozicijomis, „trailer“ turi rodyti į teisingą lentelę, o daliniai atnaujinimai turi būti teisingai susieti per /Prev nuorodas. Toks komponentas kaip „Delphi“ ir „C++Builder“ skirtas „HotPDF“ komponentas atlieka visus šiuos darbus įrašydamas failą, įskaitant pasirinkimą tarp dalinio atnaujinimo pridėjimo ir pilno kompaktiško failo perrašymo. Jei norite pamatyti, kaip ši struktūra kuriama nuo nulio, o ne analizuojama dalimis, susijęs straipsnis apie PDF dokumento kūrimą nuo nulio nuosekliai parodo antraštės, objektų, kryžminių nuorodų lentelės ir „trailer“ bloko generavimą.