Objektas numeris 1 nėra 1 puslapis. Šis vienintelis faktas sukelia daugiau sunkumų PDF apdorojimo programiniam kodui nei bet kuris kitas šio formato aspektas. Norint suprasti, kodėl taip yra, reikia žvelgti giliau nei tai, ką rodo peržiūros programa, ir išanalizuoti objektų grafą, kurį ši programa nuskaito iš tikrųjų.
PDF failas yra sunumeruotų netiesioginių objektų rinkinys. Puslapiai yra vieni iš šių objektų, tačiau jų rodymo seka neturi nieko bendro su tuo, kurioje failo vietoje jie yra įrašyti arba kokius numerius turi. Rodymo tvarką visiškai nustato /Pages medis – susieta struktūra, kurios šaknis yra dokumento katalogas (catalog). Jei ignoruosite šį medį ir skenuosite objektus skaičių tvarka, daugelyje realių dokumentų surinksite puslapius neteisingu eiliškumu.
Puslapių medis: kas iš tikrųjų nustato eiliškumą
Kiekvienas PDF dokumentas prasideda nuo katalogo (ISO 32000-2 §7.7.2). Kataloge yra įrašas /Pages, rodantis į puslapių medžio šakninį mazgą. Šis šakninis mazgas yra žodynas su reikšme /Type /Pages, kuriame yra netiesioginių nuorodų masyvas /Kids ir po juo esančių galutinių puslapių skaičių nurodantis /Count parametras. Rodymo tvarka nustatoma einant per šį medį iš kairės į dešinę gilyn (depth-first traversal) ir niekaip kitaip.
Šį principą puikiai iliustruoja minimalus trijų puslapių failas:
%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
Masyve /Kids įrašyta [20 0 R 4 0 R 9 0 R], todėl objektas 20 yra 1 puslapis, objektas 4 – 2 puslapis, o objektas 9 – 3 puslapis. Objektų numeracija čia nesvarbi. Bet koks kodas, kuris eina per objektus skaitine tvarka ir renka tuos, kurių tipas yra /Type /Page, šiame faile sugeneruos neteisingą puslapių seką.
Kodėl PDF generatoriai sukuria nenuoseklius puslapių išdėstymus? Priežasčių yra keletas. Biblioteka, kuri iš anksto rezervuoja objektų numerius visiems puslapiams prieš įrašydama jų turinį, sunumeruos juos kūrimo tvarka, o tikruosius baitus įrašys tokia seka, kokia patogi serializatoriui. Sujungimo įrankis (merge tool), sujungiantis kelis dokumentus, pernumeruoja kiekvieno šaltinio dokumento objektus, kad išvengtų konfliktų; pernumeruoti puslapių objektai atsiduria išmėtyti bendroje objektų lentelėje, o naujasis šakninis /Kids masyvas išlaiko teisingą rodymo seką. Be to, daliniai atnaujinimai prideda naujus objektus su naujais numeriais failo pabaigoje, todėl peržiūros metu pridėtas puslapis bus įrašytas baitų srauto pabaigoje, net jei rodymo tvarkoje jis turėtų būti pirmoje pozicijoje.
Plokšti medžiai ir įdėtiniai pomedžiai
Specifikacija leidžia du puslapių medžio struktūros tipus. Paprasti generatoriai sukuria plokščią struktūrą: vieną šakninį /Pages mazgą, kurio /Kids masyve yra tik galutiniai /Page objektai. Tokį medį apeiti paprasta: vienas lygis, vienas praėjimas.
Dideliuose dokumentuose paprastai naudojamas subalansuotas medis. Šakninio /Pages mazgo /Kids masyve yra tarpiniai /Pages mazgai, kurių kiekvienas savo ruožtu turi savo /Kids masyvą. Kiekvieno tarpinio mazgo /Count nurodo bendrą puslapių skaičių jo pomedyje (subtree), todėl peržiūros programa gali praleisti ištisus pomedžius, kai pereina prie konkretaus puslapio pagal indeksą, neanalizuodama kiekvieno objekto. 1 000 puslapių dokumente, kuris suformuotas kaip subalansuotas medis su 10 puslapių kiekviename mazge, 750 puslapį galima rasti atliekant dvejetainę paiešką per tris ar keturis žodyno nuskaitymus, užuot peržiūrėjus 750 /Kids įrašų.
Pasekmė apdorojimo kodui: negalite daryti prielaidos, kad pirmajame /Kids lygmenyje yra tik /Page objektai. Kiekvieną vaiką reikia patikrinti. Jei jo tipas yra /Type /Pages, turite taikyti rekursiją. Jei jo tipas yra /Type /Page, tai yra galutinis lapas. Sustojimas ties pirmuoju lygmeniu nepastebimai praras ištisus pomedžius dokumentuose, kuriuose generatorius pasirinko naudoti įdėtinius mazgus.
Paveldimi puslapio atributai
Puslapių medis taip pat atlieka išteklių dalijimosi funkciją. Tam tikri puslapio atributai (/MediaBox, /CropBox, /Resources ir /Rotate) yra paveldimi (ISO 32000-2 §7.7.3.4). Jei /Page žodyne trūksta vieno iš jų, skaitytuvas kyla /Parent grandine aukštyn, kol randa reikiamą atributą arba pasiekia šaknį. Pavyzdžiui, bendro šriftų žodyno patalpinimas šakniniame /Pages mazge, užuot jį kopijavus į kiekvieną puslapį, gali pastebimai sumažinti dokumento failo dydį, jei jame visur naudojami tie patys šriftai.
Paveldėjimo taisyklė reikalauja atidumo programuojant. Bandymas nuskaityti /MediaBox tiesiai iš /Page objekto ir trūkstamo rakto traktavimas kaip klaida yra neteisingas – šis raktas gali būti tiesiog paveldėtas. Kodas, kuris teisingai nustato puslapio geometriją, privalo sekti tėvinių objektų grandine. Taip pat reikalinga apsauga nuo ciklų: sugadintame faile /Parent nuoroda gali rodyti atgal į jau aplankytą mazgą, o tai sukeltų begalinį ciklą, jei nebūtų tikrinami aplankyti objektai.
Kryžminių nuorodų lentelė ir srautai
Netiesioginių objektų paieška vykdoma naudojant kryžminių nuorodų lentelę (xref) arba kryžminių nuorodų srautą, pasirodžiusį PDF 1.5 versijoje. Kryžminių nuorodų lentelė susieja kiekvieno objekto numerį su jo poslinkiu baitais faile. Standartą atitinkanti peržiūros programa naudoja xref, kad pereitų tiesiai prie bet kurio objekto, užuot skenavusi failą nuosekliai. Ši atsitiktinės prieigos konstrukcija leidžia greitai keisti puslapius: skaitytuvas nuskaito katalogą, per xref išsprendžia /Pages nuorodą, nuskaito šakninį /Pages mazgą, suranda reikiamą /Kids įrašą ir t. t., paliesdamas tik tuos objektus, kurių jam reikia.
Daliniai atnaujinimai prideda naują xref skyrių failo pabaigoje kartu su „trailer“ bloku, kuris nukreipia atgal į ankstesnį skyrių. Objektas, atnaujintas naujoje versijoje, gauna naują įrašą papildytame xref skyriuje; pradiniai baitai lieka savo vietose, tačiau yra pakeičiami naujais. Taip skaitmeniniu parašu pasirašyti PDF failai išlieka galiojantys net po to, kai pridedamos pastabos ar užpildomos formos: pasirašytas baitų rėžis niekada neliečiamas, o naujas turinys įrašomas pabaigoje. Puslapių medis taip pat gali būti atnaujintas, todėl puslapių pridėjimas ar ištrynimas sukuria naują šakninį /Pages mazgą su pakeistu /Kids masyvu, nors senasis šakninis objektas vis dar lieka savo pradinėje vietoje.
Kas nutinka neperėjus per puslapių medį
Objektų skenavimo metodo klaidos dažnai lieka nepastebėtos iš pirmo žvilgsnio. Gautas dokumentas atrodo tvarkingas: jame yra teisingas puslapių skaičius, o kiekviename puslapyje yra matomas turinys. Tačiau puslapių tvarka yra tiesiog klaidinga, ir ši klaida priklauso nuo generatoriaus, atnaujinimų skaičiaus bei to, ar puslapiai buvo sujungti iš kitų šaltinių. Vieno įrankio sukurti failai gali būti sėkmingai apdorojami, tačiau failai iš kito įrankio ar sujungto srauto pateiks klaidų. Šis nenuoseklumas yra priežastis, kodėl euristiniai pataisymai niekada neveikia patikimai.
Failai su daliniais atnaujinimais yra ypač jautrūs šiai problemai, nes puslapiai, pridėti ar perdėlioti vėlesnėse versijose, turi didelius objektų numerius, nors rodymo tvarką valdo atnaujintas /Kids masyvas. Skenavimas, kuris apdoroja objektus skaitine tvarka, patalpins šiuos puslapius su dideliais numeriais pabaigoje, nepriklausomai nuo to, kurioje vietoje pagal medį jie turėtų būti.
Sprendimas nėra sudėtingas. Pradėkite nuo katalogo, suraskite /Pages nuorodą, rekursiškai apeikite /Kids masyvą ir išveskite puslapius ta tvarka, kuria juos sutinkate. Tai yra rodymo tvarka pagal apibrėžimą, nepriklausomai nuo objektų numerių, baitų poslinkių ar failo struktūros. Dauguma subrendusių PDF bibliotekų siūlo puslapių skaičiavimo bei indeksuotos prieigos funkcijas, kurios jau atlieka šį darbą teisingai. Rizika kyla tada, kai programuojant bandoma apeiti bibliotekos puslapių modelį ir dirbama tiesiogiai objektų lygmenyje.
Viena struktūrinė anomalija, kurią verta aprašyti atskirai: malformuotuose failuose /Count reikšmė tarpiniame /Pages mazge gali būti klaidinga. Pasitikėjimas /Count reikšme rėžių tikrinimui ir perėjimo sustabdymas anksčiau laiko gali lemti puslapių praradimą, jei nurodytas skaičius yra mažesnis už tikrąjį. Saugiau naudoti /Count tik kaip našumo užuominą pre-allokacijai arba dvejetainei paieškai, o faktinį skaičių nustatyti atliekant pilną apėjimą.