Technical Article

PDF puslapių medžiai: kodėl puslapių tvarka nesutampa su objektų tvarka

PDF dokumento 1 puslapis nėra objektas 1. Šis skirtumas yra dažniausia klaidingo puslapių išgavimo klaida PDF analizatoriuose, o sprendimas yra specifikacijos skaitymas, o ne tiesioginis failo baitų skenavimas.

Objektai, nuorodos ir katalogas

PDF failas yra sunumeruotų objektų rinkinys. Kiekvienas iš jo turi unikalų objekto numerį ir kartos (generation) numerį, rašomą kaip N G obj, kur G beveik visada yra 0 faile, kuris nebuvo atnaujintas daliniu būdu. Objektai nurodo vienas kitą naudodami sintaksę N G R, todėl 3 0 R reiškia „dabartinę objekto 3 versiją“. Pabaigos blokas (trailer) rodo į šakninį katalogo objektą, kurio /Pages įrašas nukreipia į puslapių medį. Viskas, kas pasiekiama PDF faile, prasideda nuo šios šaknies, o ne nuo pirmojo failo kūno baito.

Kryžminių nuorodų lentelė (arba kryžminių nuorodų srautas nuo PDF 1.5 versijos) susieja objektų numerius su failo poslinkiais. Jos darbas yra užtikrinti atsitiktinę prieigą, o ne eiliškumą. Programa, kuri dokumentą kuria daliniais atnaujinimais, gali pridėti naujus objektus pabaigoje su didesniais numeriais, nors šie objektai logiškai turėtų būti prieš esamus puslapių sekos objektus. Tai nėra trūkumas – tai numatyta konstrukcijoje.

Puslapių medis (ISO 32000-1 §7.7.3)

Puslapių seka saugoma puslapių medyje. Šakninis katalogas turi /Pages nuorodą, rodančią į /Pages tipo mazgą. Šio mazgo /Kids masyvas išvardija vaikus skaitymo tvarka. Kiekvienas vaikas yra arba galutinis /Page tipo mazgas (lapas), arba kitas tarpinis /Pages mazgas, turintis savo /Kids masyvą. 1 puslapis yra pirmasis galutinis lapas, kurį pasiekiate eidami per Kids masyvus iš kairės į dešinę gilyn. Įrašas /Count kiekviename tarpiniame mazge išsaugo bendrą dukterinių galutinių puslapių skaičių, todėl peržiūros programa gali iškart pereiti į 500 puslapį, neperžiūrėdama viso medžio.

Štai kaip minimalus trijų puslapių medis atrodo grynojoje PDF sintaksėje:

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

Kids masyve įrašyta [20 0 R, 1 0 R, 4 0 R]. Loginis 1 puslapis yra objektas 20, loginis 2 puslapis – objektas 1, loginis 3 puslapis – objektas 4. Bet koks kodas, einantis per objektų numerius nuo 1 į viršų, aptiks juos tokia tvarka: 1, 4, 20 ir sugeneruos seką: 2 puslapis, 3 puslapis, 1 puslapis. Gautas dokumentas bus atvaizduojamas sumaišyta tvarka, kuri gali atrodyti visiškai normaliai medį sekančioje peržiūros programoje, bet bus visiškai sugadinta programoje, kuri to nedaro.

Paveldėjimas

Tarpiniai mazgai gali turėti savybių, kurias paveldi jų dukteriniai objektai. Dažniausiai paveldimi įrašai yra /MediaBox (puslapio matmenys), /CropBox, /Resources (šriftai ir vaizdai) bei /Rotate. Puslapis, kuriame nėra /MediaBox parametro, nėra sugadintas – jis tiesiog perima reikšmę iš artimiausio tėvinio mazgo, kuriame ji yra apibrėžta. Puslapis, kuriame yra nurodytas /MediaBox, pakeičia tėvinio mazgo reikšmę tik tam konkrečiam puslapiui.

Tai yra labai svarbu atliekant analizę (parsing). Skaitant /Page objektą atskirai ir darant prielaidą, kad jo savybės yra išsamios, bus neteisingai nustatyti bet kurio puslapio, kuris priklauso nuo paveldėjimo, matmenys. Teisingai veikiantis skaitytuvas eina /Parent grandine, rinkdamas dar nematytas savybes, kol pasiekia šaknį.

Įdėtiniai medžiai

Specifikacijoje nėra jokių apribojimų, ribojančių medį iki vieno lygio. Dideliame dokumente puslapiai gali būti sugrupuoti po tarpiniais mazgais, kurie atitinka skyrius:

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

Apėjimo algoritmas yra toks pat: iš eilės pereiti per Kids narius, taikyti rekursiją bet kuriam /Pages mazgui ir surinkti galutinius /Page mazgus. Reikšmės /Count leidžia peržiūros programai praleisti ištisą pomedį pereinant prie puslapio, esančio už jo ribų, todėl šie skaičiai turi būti tikslūs. Kai kurie praėjusio amžiaus dešimtojo dešimtmečio pabaigos ir šio amžiaus pradžios PDF redaktoriai jų neperskaičiuodavo po pakeitimų, todėl patikimas analizatorius tikrina /Count reikšmę pagal faktinį puslapių skaičių, o ne aklai ja pasikliauja kurdamas masyvus.

Kur tai pasireiškia praktikoje

Puslapių eiliškumo klaida dažniausiai išryškėja dviem atvejais. Pirmasis – kai naudojamas pasirinktinis analizatorius, kuris skenuoja /Page tipo objektus, užuot sekęs medį. Jis randa kiekvieną puslapį, bet pagal objektų numerius, o ne skaitymo tvarka. Sprendimas visada toks pat: pradėti nuo trailer bloko, surasti šakninį katalogą, sekti /Pages nuorodą ir pereiti Kids masyvus.

Antrasis atvejis – failai su daliniais atnaujinimais. Kai PDF redagavimo programa prideda pakeitimus neperrašydama viso failo, nauji puslapio objektai gauna didelius objektų numerius, nors pradinio medžio Kids masyvas vis tiek valdo jų loginę poziciją. Puslapis, kuris iš pradžių buvo objektas 5, pakeičiamas nauju objektu 143, tačiau Kids masyvas dabar nurodo į 143, o ne į 5, todėl loginis eiliškumas išsaugomas. Einant pagal objekto numerį, pakeistas puslapis atsidurtų netinkamoje sekos vietoje.

Linearizuoti (interneto naršyklėms optimizuoti) PDF failai prideda trečią variantą: failas yra fiziškai pertvarkomas taip, kad pirmojo puslapio turinys būtų failo pradžioje greitam rodymui lėto ryšio sąlygomis. Puslapių medžio struktūra vis tiek išlieka autoritetingu eiliškumo šaltiniu, tačiau kryžminių nuorodų lentelė nukreipia į pakeistus poslinkius. Analizatorius, kuris remiasi fizine vieta faile, o ne xref lentele, neteisingai perskaitys net pirmąjį linearizuoto failo puslapį.

Mūsų „HotPDF“ komponentas pats atlieka puslapių medžio apėjimą, paveldėjimo sprendimą ir dalinių atnaujinimų xref sujungimą. Dirbant tiesiogiai su jo puslapių objektais, Kids masyvo eiliškumas jau yra pritaikytas – puslapių indeksai rodo į loginius puslapius, o ne į objektų numerius.