Objekt številka 1 ni prva stran. Ta razlika je najpogostejši vir napak z napačno izvoženo stranjo v parserjih za PDF, rešitev pa je v branju specifikacije in ne samih bajtov datoteke.
Datoteka PDF je zbirka oštevilčenih posrednih objektov. Strani so del teh objektov, vendar njihov vrstni red prikaza nima nobene povezave s tem, kje v datoteki se nahajajo ali katere številke nosijo. Vrstni red prikaza določa izključno drevo /Pages, to je povezana struktura s korenskim objektom v katalogu dokumenta. Če drevo prezrete in objekte pregledujete po številčnem zaporedju, boste pri velikem delu dejanskih datotek strani sestavili v napačnem vrstnem redu.
Drevo strani: kaj dejansko določa vrstni red
Vsak PDF se začne s katalogom dokumenta (ISO 32000-2 §7.7.2). Katalog vsebuje vnos /Pages, ki kaže na korensko vozlišče drevesa strani. To korensko vozlišče je slovar z vrednostmi /Type /Pages, poljem /Kids s posrednimi sklici in števcem /Count, ki določa skupno število listnih strani pod njim. Vrstni red prikaza je določen izključno s prehodom tega drevesa po globini od leve proti desni.
Minimalna datoteka s tremi stranmi to nazorno prikazuje:
%PDF-1.7
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [20 0 R 4 0 R 9 0 R] /Count 3 >>
endobj
% Object 4 is stored third in the file but is page 2 in display order
4 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 5 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
% Object 9 is stored fourth but is page 3
9 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 10 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
% Object 20 is stored last but is page 1; Kids[0] decides, not object number
20 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 21 0 R /Resources << /Font << /F1 6 0 R >> >> >>
endobj
Polje /Kids vsebuje zapis [20 0 R 4 0 R 9 0 R], zato je objekt 20 prva stran, objekt 4 druga stran in objekt 9 tretja stran. Številčenje objektov je nepomembno. Koda, ki pregleduje objekte po številčnem vrstnem redu in zbira tiste s tipom /Type /Page, bo pri tej datoteki vrnila napačno zaporedje.
Zakaj generatorji ustvarjajo ne-zaporedne postavitve? Razlogov je več. Knjižnica, ki vnaprej dodeli številke objektov za vse strani pred izpisom njihove vsebine, jih bo oštevilčila po vrstnem redu ustvarjanja, nato pa zapisala bajte v poljubnem vrstnem redu, ki ustreza serializatorju. Knjižnica za združevanje (merge tool), ki sestavlja dokumente, ponovno oštevilči objekte vsakega izvornega dokumenta, da prepreči trke; preštevilčeni objekti strani se tako razpršijo po celotni tabeli objektov, medtem ko novo korensko polje /Kids hrani pravilno zaporedje prikaza. Postopne posodobitve dodajo nove objekte na konec datoteke z novimi številkami, zato se stran, dodana pri urejanju, nahaja blizu konca toka bajtov, čeprav morda pripada na prvo mesto vrstnega reda prikaza.
Ravna drevesa in gnezdena poddrevesa
Specifikacija dovoljuje dve obliki drevesa strani. Preprosti generatorji ustvarijo ravno strukturo: eno korensko vozlišče /Pages, katerega polje /Kids vsebuje le listne objekte /Page. To je enostavno prehoditi: en nivo globine, en prehod.
Večji dokumenti pa običajno uporabljajo uravnoteženo drevo. Polje /Kids korenskega vozlišča /Pages vsebuje vmesna vozlišča /Pages, od katerih ima vsako svoje polje /Kids. Števec /Count na vsakem vmesnem vozlišču sporoča skupno število listnih strani v njegovem poddrevesu, tako da lahko pregledovalnik preskoči celotna poddrevesa pri skoku na stran po indeksu, ne da bi razčlenil vsak objekt. Dokument s tisoč stranmi, organiziran kot uravnoteženo drevo z 10 stranmi na listno vozlišče, lahko najde 750. stran z dvojiškim iskanjem prek treh ali štirih poizvedb v slovarjih, namesto s pregledovanjem 750 vnosov v polju /Kids.
Posledica za programsko kodo: ne smete predvidevati, da prva raven polja /Kids vsebuje le objekte /Page. Vsakega otroka je treba preveriti. Če je njegov tip /Type enak /Pages, morate rekurzivno vstopiti vanj. Če je tip /Page, je to listna stran. Zaustavitev na prvi ravni bo tiho izpustila celotna poddrevesa v vsakem dokumentu, kjer se je generator odločil za gnezdenje.
Podedovani atributi strani
Drevo strani vsebuje tudi mehanizem za deljenje virov. Nekateri atributi strani (/MediaBox, /CropBox, /Resources in /Rotate) so dedni (ISO 32000-2 §7.7.3.4). Če slovar /Page enega od njih izpusti, bralnik prehodi verigo /Parent navzgor, dokler ne najde atributa ali doseže korena. Postavitev skupnega slovarja pisav v korensko vozlišče /Pages namesto kopiranja na vsako listno stran lahko opazno zmanjša velikost datoteke pri dokumentih, ki uporabljajo enake pisave skozi celotno vsebino.
Pravilo dedovanja prinaša posebno zahtevo za kodo, ki bere lastnosti strani. Branje ključa /MediaBox neposredno iz objekta /Page in obravnava njegove odsotnosti kot napaka je napačna; ključ je lahko preprosto podedovan. Koda, ki pravilno določa geometrijo strani, mora slediti verigi staršev. Potrebuje tudi zaščito pred zankami (cycle guard): poškodovana datoteka lahko vsebuje sklic /Parent, ki kaže na že obiskano vozlišče, kar bi brez preverjanja obiskanih objektov povzročilo neskončno zanko.
Tabela xref in tokovi navzkrižnih sklicev
Iskanje posrednih objektov poteka prek tabele navzkrižnih sklicev (ali njenega naslednika, toka navzkrižnih sklicev, uvedenega v PDF 1.5). Tabela xref preslika vsako številko objekta v bajtni odmik znotraj datoteke. Skladen bralnik uporablja tabelo xref za neposreden skok na kateri koli objekt; datoteke ne pregleduje zaporedno. Ta zasnova z naključnim dostopom omogoča hiter skok na želeno stran: pregledovalnik prebere katalog, razreši sklic /Pages prek tabele xref, prebere korensko vozlišče /Pages, razreši vnos /Kids in tako naprej, pri čemer se dotakne le tistih objektov, ki jih potrebuje.
Postopne posodobitve dodajo nov del xref na konec datoteke z napovednikom, ki se povezuje nazaj na prejšnjega. Objekt, posodobljen v reviziji, dobi nov vnos v dodanem delu xref; izvirni bajti ostanejo na mestu, a so nadomeščeni. Na ta način digitalno podpisani PDF-ji ostanejo preverljivi tudi po dodajanju opomb ali izpolnjevanju obrazcev: podpisano bajtno območje se nikoli ne spremeni, nova vsebina pa se nahaja v dodanem delu. Posodobi se lahko tudi drevo strani, zato dodajanje ali brisanje strani v reviziji ustvari nov koren /Pages s spremenjenim poljem /Kids, medtem ko stari korenski objekt še naprej zaseda svoj prvotni položaj v datoteki.
Kaj gre narobe brez prehajanja drevesa
Način odpovedi pri pregledovanju vseh objektov je tih. Izhodni dokument je videti ustrezen: ima pravilno število strani in vsaka stran vsebuje prepoznavno vsebino. Vrstni red pa je napačen, in to na način, ki je odvisen od generatorja, števila revizij in tega, ali so bile katere strani združene iz zunanjih virov. Testna zbirka datotek, ustvarjenih z enim samim orodjem, se lahko izvede brez težav; datoteke iz drugega orodja ali procesa združevanja pa bodo odpovedale. Zaradi te nekonstantnosti hevristični popravki nikoli ne delujejo dolgoročno.
Datoteke s postopnimi posodobitvami so še posebej nagnjene k tej napaki, saj imajo strani, dodane ali prerazporejene v kasnejših revizijah, visoke številke objektov, medtem ko vrstni red prikaza nadzoruje posodobljeno polje /Kids. Skeniranje, ki obdeluje objekte po številčnem zaporedju, bo te kasneje oštevilčene strani postavilo na konec, ne glede na to, kje v drevesu bi morale biti.
Rešitev ni zapletena. Začnite pri katalogu, razrešite sklic /Pages, rekurzivno prehodite polje /Kids in izpišite liste v vrstnem redu, kot jih srečate. To je po definiciji vrstni red prikaza, ne glede na številke objektov, bajtne odmike ali strukturo datoteke. Večina zanesljivih knjižnic za PDF izpostavlja število strani in indeksiran dostop do strani, ki to že opravljata pravilno; tveganje pa obstaja v kodi, ki zaobide knjižnični model strani in se neposredno dotakne plasti objektov.
Ena strukturna nepravilnost, ki jo je smiselno izrecno obravnavati: vrednost /Count na vmesnem vozlišču /Pages je v nekaterih poškodovanih datotekah lahko napačna. Če se zanašate na /Count za preverjanje meja in nato ustavite celoten prehod, boste tiho izpustili strani, če je števec premalo ocenjen. Varnejši vzorec je uporaba /Count le kot namig za hitrost pri dodeljevanju kapacitet ali dvojiškem iskanju, dejansko število pa določite s prehodom celotnega drevesa.