Technical Article

Drevesa strani PDF: zakaj vrstni red strani ni vrstni red objektov

Prva stran PDF-ja ni objekt številka 1. 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.

Objekti, reference in katalog

Datoteka PDF je zbirka oštevilčenih objektov. Vsak ima edinstveno številko objekta in številko generacije, zapisan kot N G obj, pri čemer je G v datotekah, ki niso bile postopno posodobljene, skoraj vedno enak 0. Objekti se sklicujejo drug na drugega z zapisom N G R, tako da 3 0 R pomeni "trenutna različica objekta 3". Napovednik kaže na korenski objekt kataloga, katerega vnos /Pages vodi do drevesa strani. Vse, po čemer lahko krmarimo v PDF-ju, izhaja iz tega korena in ne iz prvega bajta telesa datoteke.

Tabela navzkrižnih sklicev (ali tok navzkrižnih sklicev v PDF 1.5+) preslika številke objektov v odmike datoteke. Njena naloga je naključni dostop in ne določanje vrstnega reda. Pisalnik, ki dokument gradi postopno, lahko doda nove objekte z višjimi številkami na konec, čeprav ti objekti v zaporedju strani logično prehitevajo obstoječe. To ni napaka, ampak je tako zasnovano.

Drevo strani (ISO 32000-1 §7.7.3)

Zaporedje strani se nahaja v drevesu strani. Korenski katalog vsebuje sklic /Pages, ki kaže na vozlišče tipa /Pages. Polje /Kids tega vozlišča navaja otroke v vrstnem redu branja. Vsak otrok je bodisi listno vozlišče tipa /Page bodisi drugo vmesno vozlišče /Pages z lastnim poljem /Kids. Prva stran je prvi list, ki ga dosežemo s prehodom polj Kids po globini od leve proti desni. Vnos /Count na vsakem vmesnem vozlišču predpomni skupno število listnih strani pod njim, tako da lahko pregledovalnik skoči na 500. stran brez prehodenja celotnega drevesa.

Tukaj je primer minimalnega drevesa s tremi stranmi v neobdelani sintaksi PDF:

16 0 obj
<<
  /Type /Pages
  /Count 3
  /Kids [20 0 R  1 0 R  4 0 R]
  /MediaBox [0 0 612 792]
>>
endobj

20 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 21 0 R  /Resources 22 0 R >>
endobj

1 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 2 0 R   /Resources 3 0 R >>
endobj

4 0 obj
<< /Type /Page  /Parent 16 0 R  /Contents 5 0 R   /Resources 6 0 R >>
endobj

Polje Kids vsebuje zapis [20 0 R, 1 0 R, 4 0 R]. Logično prva stran je objekt 20, logično druga stran je objekt 1, logično tretja pa objekt 4. Koda, ki pregleduje objekte od 1 navzgor, bo nanje naletela v zaporedju 1, 4, 20 ter ustvarila zaporedje stran-2, stran-3, stran-1. Nastali dokument se bo prikazal v pomešanem vrstnem redu, kar se v pregledovalniku, ki sledi drevesu, zdi povsem normalno, v pregledovalniku, ki mu ne sledi, pa katastrofalno napačno.

Dedovanje

Vmesna vozlišča lahko vsebujejo lastnosti, ki jih njihovi potomci podedujejo. Najpogostejši podedovani vnosi so /MediaBox (dimenzije strani), /CropBox, /Resources (pisave in slike) in /Rotate. Listna stran, ki nima vnosa /MediaBox, ni poškodovana; vrednost prevzame od najbližjega prednika, ki jo definira. Stran, ki definira lasten /MediaBox, pa prepiše vrednost starša, vendar le za to stran.

To je pomembno pri razčlenjevanju. Branje objekta /Page ločeno in predvidevanje, da so njegove lastnosti popolne, bo javilo napačne dimenzije za vsako stran, ki se zanaša na dedovanje. Pravilen bralnik pregleda verigo staršev, zbira lastnosti, ki jih še ni zaznal, in se ustavi pri korenu.

Gnezdena drevesa

Nič v specifikaciji ne omejuje drevesa na en sam nivo. Obsežen dokument lahko združuje strani pod vmesnimi vozlišči, ki ohlapno ustrezajo poglavjem:

2 0 obj   % root Pages node, Count = 8
<< /Type /Pages  /Count 8  /Kids [3 0 R  4 0 R] >>
endobj

3 0 obj   % first chapter, 5 pages
<< /Type /Pages  /Parent 2 0 R  /Count 5
   /Kids [10 0 R  11 0 R  12 0 R  13 0 R  14 0 R]
   /MediaBox [0 0 612 792] >>
endobj

4 0 obj   % second chapter, 3 pages
<< /Type /Pages  /Parent 2 0 R  /Count 3
   /Kids [20 0 R  21 0 R  22 0 R]
   /MediaBox [0 0 612 792] >>
endobj

Algoritem prehajanja je enak: po vrstnem redu obišči Kids, rekurzivno vstopi v kateri koli vmesni /Pages in zberi listna vozlišča /Page. Vrednosti /Count pregledovalniku omogočajo, da preskoči celotno poddrevo pri skoku na stran, ki se nahaja za njim, zato morajo biti ti števci točni. Nekateri urejevalniki PDF iz poznih 90-ih in začetka 2000-ih teh števcev po urejanju na mestu niso ponovno izračunali, zato previden parser preveri /Count glede na dejansko število listov in mu ne zaupa slepo pri dodeljevanju pomnilnika.

Kje se to pojavi v praksi

Hrošč z vrstnim redom strani se najpogosteje pojavi v dveh scenarijih. Prvi je lasten parser, ki išče objekte tipa /Page, namesto da bi sledil drevesu. Najde sicer vse strani, vendar v zaporedju številk objektov in ne v vrstnem redu branja. Rešitev je vedno enaka: začni pri napovedniku, razreši korenski katalog, sledi /Pages in prehodi polja Kids.

Drugi scenarij je datoteka s postopnimi posodobitvami. Ko urejevalnik PDF doda spremembe brez ponovnega zapisovanja celotne datoteke, dobijo novi objekti strani visoke številke, medtem ko polje Kids v prvotnem drevesu še vedno nadzoruje njihov logični položaj. Stran, ki je bila prvotno objekt 5, se nadomesti z novim objektom 143, vendar polje Kids zdaj namesto na 5 kaže na 143, s čimer se ohrani logični vrstni red. Sledenje po številkah objektov bi nadomestno stran postavilo na napačen položaj v zaporedju.

Linearizirani (spletno optimizirani) PDF-ji dodajajo tretjo različico: datoteka je fizično preurejena tako, da se vsebina prve strani nahaja blizu začetka datoteke za hiter prikaz na počasi povezavi. Vrstni red strani PDF izhaja iz polja Kids v drevesu Pages in ne iz številk objektov. Parserji, ki pregledujejo objekte po številkah, strani zamešajo. Parser, ki se zanaša na položaj v datoteki in ne na tabelo xref, bo napačno prebral že prvo stran linearizirane datoteke.

Knjižnica HotPDF Component interno upravlja prehajanje drevesa strani, razreševanje dedovanja in združevanje delov xref ob postopnih posodobitvah. Pri delu z njenimi objekti strani je vrstni red polja Kids že uporabljen, indeksi strani pa se preslikajo v logične strani in ne v številke objektov.