Napake pri preverjanju obsega v knjižnicah Delphi PDF slovijo po tem, da jih je težko odkriti, saj ne sledijo doslednemu vzorcu vnosa. Isti dokument jih povzroči na enem računalniku in ne na drugem; ista koda sproži izjemo pri datoteki s 3 stranmi, a deluje brez napak pri datoteki z 12 stranmi. Ta nedoslednost skoraj vedno izhaja iz enega samega izvirnega vzroka: objekti strani PDF niso shranjeni v vrstnem redu datoteke. Če knjižnica gradi svoje notranje polje strani z zaporednim pregledovanjem objektov, namesto da bi prehodila drevo strani, ki ga razglaša katalog, ustvari indeks, katerega veljavni obseg se ne ujema s pričakovanji klicateljev, preverjanje obsega pa ujame to neskladje v najslabšem možnem trenutku
Kako deluje preverjanje obsega v Delphiju
Z aktivno prevajalno direktivo {$R+} (privzeto v konfiguraciji Debug) Delphi RTL med izvajanjem preveri vsak indeks polja, indeks niza in dodelitev naštevnih vrednosti. Dostop izven obsega sproži izjemo ERangeError, namesto da bi tiho prebral sosednji pomnilnik. To vedenje je dragoceno: zgodaj odkrije skrite hrošče, namesto da bi ti poškodovali podatkovno strukturo, ki odpove šele sto vrstic kasneje. Frustrirajoče pa je, da se izjema sproži na mestu dostopa in ne na točki, kjer je bil indeks napačno izračunan. Ko klicni sklad prikazuje globoko gnezdeno metodo v enoti PDF, je dejanska napaka navadno nekaj okvirjev nazaj
Sestavljeni logični pogoji to še poslabšajo. Delphi ocenjuje izraze and od leve proti desni s semantiko kratkega stika, vendar se ocenjevanje preskoči le, ko je leva stran False. Izraz kot:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
izgleda varen, a ščiti pred indeksom izven obsega le, če je FDocStarted enak True in je DestIndex nenegativen. Preverjanje DestIndex < Length(PageArr) ne naredi ničesar, ko je DestIndex negativen, ker primerjava negativnega celega števila z nenegativno dolžino vrne True v predznačeni aritmetiki, kasnejši dostop do polja pa vseeno sproži napako obsega. Premik preverjanja meja na najbolj zunanjo raven je pravilna rešitev:
if (DestIndex >= 0) and (DestIndex < Length(PageArr)) then
begin
if FDocStarted and (PageArr[DestIndex].PageObj <> nil) then
Result := PageArr[DestIndex].PageObj
else
Result := nil;
end
else
raise ERangeError.CreateFmt(
'Page index %d is out of range (0..%d)',
[DestIndex, Length(PageArr) - 1]);
To je mehanski popravek. Prepreči sesutje. Ne pojasni pa, zakaj je DestIndex sploh prejel vrednost zunaj veljavnega obsega
Dejanski vzrok: vrstni red objektov v primerjavi z vrstnim redom strani
ISO 32000-1 §7.7.3 definira drevo strani kot drevo vozlišč Pages, katerih polja Kids navajajo objekte strani v vrstnem redu prikaza. Datoteka shranjuje te objekte na odmike, ki jih je generator izbral po lastni presoji; objekt številka 20 lahko v bajtnem toku fizično leži pred objektom številka 3. Knjižnica, ki gradi seznam strani z iteracijo po navzkrižni referenčni tabeli v vrstnem redu številk objektov in ne sledi verigi Kids, bo ustvarila zaporedje, ki odstopa od pričakovanj uporabnika. Pri dokumentih, kjer je generator strani zapisal v pravilnem vrstnem redu, vse deluje. Pri dokumentih, kjer tega ni storil, neskladje med oštevilčenjem strani knjižnice in oštevilčenjem klicatelja povzroči indekse, ki padejo izven polja PageArr
Pravilen pristop je, da začnete pri katalogu, razrešite posredni sklic /Pages in rekurzivno prehodite polje Kids. Za raven dokument brez vmesnih vozlišč Pages je prehod preprost:
procedure BuildPageIndexFromTree(
const KidsArray: THPDFArray;
var PageArr: TPageObjArray);
var
i, Idx: Integer;
Child: THPDFObject;
ChildType: string;
begin
for i := 0 to KidsArray.Count - 1 do
begin
Child := KidsArray.GetIndirectObject(i);
if Child = nil then
Continue;
ChildType := Child.GetNameValue('/Type');
if ChildType = 'Page' then
begin
Idx := Length(PageArr);
SetLength(PageArr, Idx + 1);
PageArr[Idx].PageObj := Child;
end
else if ChildType = 'Pages' then
begin
// intermediate node: recurse into its Kids
BuildPageIndexFromTree(Child.GetArray('/Kids'), PageArr);
end;
end;
end;
Ko ta procedura konča, je PageArr[0] prva stran, ki jo bo pregledovalnik prikazal, ne glede na to, kje se ta objekt nahaja v bajtnem toku. Indeksi, ki jih posredujejo klicatelji in predvidevajo vrstni red prikaza, se zdaj pravilno preslikajo, napake obsega pa se prenehajo pojavljati
Trdo kodirane začasne rešitve težavo le še poslabšajo
V kodnih bazah, kjer izvirni vzrok ni bil nikoli ugotovljen, pogosto najdemo hevristične popravke: zamenjaj prvo in zadnjo stran, če je skupno število enako 3; zasukaj indeks za dokumente določenega generatorja; uveljavi odmik, ko številka prvega objekta preseže prag. Vsak od teh popravkov ustreza natanko tistim testnim datotekam, ki so bile pri roki med pisanjem. Ko dodate drug vir PDF, se eden od popravkov sproži ob napačnem trenutku in ustvari indeks, ki je zdaj dvakrat napačen: napačen, ker je bil izračunan iz neurejenega polja, in znova napačen, ker je bila nanj uveljavljena neustrezna preslikava. Preverjevalnik obsega ga ujame nekje kasneje, klicni sled pa ne kaže na nič uporabnega
Edina učinkovita pot je odstranitev vseh hevrističnih preslikav in zamenjava gradnje polja strani s pravilnim prehodom drevesa. Ko so indeksi pravilni že po konstrukciji, popravki niso več potrebni, preverjevalnik obsega pa postane prednost in ne ovira
Če vzdržujete knjižnico, ki kaže ta vzorec, začasno omogočite preverjanje obsega v konfiguraciji Release in jo zaženite na raznolikem naboru PDF-jev: dokumentih, ustvarjenih z Wordom, z LaTeX-om, z vdelano programsko opremo skenerjev, z orodji za razdelitev PDF-jev. Datoteke, ki sprožijo izjeme, so tiste, pri katerih vrstni red objektov strani odstopa od vrstnega reda prehajanja, ki ga predvideva vaša koda. Vsaka je le podatkovna točka in ne ločen hrošč
Za novo kodo, ki kliče knjižnico Delphi PDF, je praktičen nasvet, da število strani knjižnice obravnavate kot merodajno in nikoli ne posredujete indeksa, pridobljenega z aritmetiko na zunanjih podatkih, ne da bi se prej prepričali, da se nahaja v obsegu 0..PageCount - 1. Komponenta HotPDF izpostavi razrešeno število strani prek THotPDF.PageCount po klicu BeginDoc ali po nalaganju dokumenta; ta vrednost vedno odraža prehod drevesa strani in jo je varno uporabiti kot zgornjo mejo za vsako indeksno aritmetiko