Erorile de verificare a intervalului din bibliotecile PDF pentru Delphi și-au câștigat reputația de a fi greu de localizat, pentru că nu urmează un tipar de intrare consecvent. Același document le produce pe o mașină și nu pe alta; aceeași cale de cod declanșează excepția pe un fișier de 3 pagini, dar rulează curat pe unul de 12. Acea inconsecvență se reduce aproape întotdeauna la o singură cauză de fond: obiectele de pagină PDF nu sunt stocate în ordinea din fișier. Dacă biblioteca își construiește tabloul intern de pagini scanând obiectele secvențial, în loc să parcurgă arborele de pagini declarat de catalog, ea construiește un index al cărui interval valid nu corespunde cu ceea ce așteaptă apelanții, iar verificarea intervalului prinde această nepotrivire în cel mai prost moment posibil
Cum funcționează verificarea intervalului în Delphi
Cu directiva de compilare {$R+} activă (valoarea implicită în configurația Debug), RTL-ul Delphi validează la execuție fiecare index de tablou, fiecare indice de șir și fiecare atribuire de tip enumerat. Un acces în afara limitelor ridică ERangeError, în loc să citească în tăcere memoria alăturată. Acel comportament este valoros: scoate la suprafață devreme defectele latente, în loc să le lase să corupă o structură de date care abia peste o sută de linii cedează. Partea frustrantă este că excepția se declanșează la locul accesului, nu în punctul în care indexul a fost calculat greșit. Când stiva de apeluri arată o metodă adânc imbricată dintr-o unitate PDF, greșeala reală se află de obicei cu mai multe cadre în urmă
Condițiile booleene compuse înrăutățesc situația. Delphi evaluează expresiile and de la stânga la dreapta, cu semantică de scurtcircuit, dar scurtcircuitul sare peste evaluare doar când partea stângă este False. O expresie ca:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
pare sigură, dar protejează împotriva unui index în afara intervalului numai dacă FDocStarted este True și DestIndex nu este negativ. Verificarea DestIndex < Length(PageArr) nu face nimic atunci când DestIndex este negativ, pentru că, în aritmetica cu semn, compararea unui întreg negativ cu o lungime nenegativă returnează True, iar accesul la tablou care urmează tot declanșează eroarea de interval. Mutarea verificării limitelor pe poziția cea mai exterioară este corectura potrivită:
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]);
Aceasta este corectura mecanică. Ea oprește căderea. Nu explică de ce DestIndex a primit de la bun început o valoare în afara intervalului valid
Cauza reală: ordinea obiectelor față de ordinea paginilor
ISO 32000-1 §7.7.3 definește arborele de pagini ca pe un arbore de noduri Pages ale căror tablouri Kids listează obiectele de pagină în ordinea de afișare. Fișierul stochează acele obiecte la orice decalaje a nimerit să aleagă generatorul; obiectul cu numărul 20 poate precede fizic obiectul cu numărul 3 în fluxul de octeți. O bibliotecă ce își construiește lista de pagini iterând tabelul de referințe încrucișate în ordinea numerelor de obiect, în loc să urmeze lanțul Kids, va produce o secvență care se abate de la ceea ce așteaptă utilizatorul. Pe documentele în care generatorul a nimerit să scrie paginile în ordine, totul funcționează. Pe documentele în care nu a făcut-o, discrepanța dintre numerotarea paginilor din bibliotecă și numerotarea paginilor la apelant produce indici care cad în afara lui PageArr
Abordarea corectă este să porniți de la catalog, să rezolvați referința indirectă /Pages și să parcurgeți recursiv tabloul Kids. Pentru un document plat, fără noduri Pages intermediare, parcurgerea este directă:
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
// nod intermediar: recursivitate în tabloul lui Kids
BuildPageIndexFromTree(Child.GetArray('/Kids'), PageArr);
end;
end;
end;
După ce rulează aceasta, PageArr[0] este prima pagină pe care ar afișa-o un vizualizator, indiferent unde stă acel obiect în fluxul de octeți. Indicii transmiși de apelanți care presupun ordinea de afișare se mapează acum corect, iar erorile de interval încetează
Soluțiile improvizate scrise direct în cod agravează problema
În bazele de cod în care cauza de fond nu a fost niciodată identificată, se găsesc frecvent petice euristice: schimbă între ele prima și ultima pagină dacă numărul total este 3, rotește indexul pentru documentele venite de la un anumit generator, aplică un decalaj când primul număr de obiect depășește un prag. Fiecare dintre aceste petice se potrivește exact setului de fișiere de test aflate la îndemână în momentul scrierii lui. Adăugați o altă sursă de PDF-uri și unul dintre petice se declanșează la momentul nepotrivit, producând un index acum de două ori greșit: greșit pentru că a fost calculat dintr-un tablou dezordonat și greșit din nou pentru că i s-a aplicat deasupra o mapare inaplicabilă. Verificatorul de interval îl prinde undeva mai jos, iar urma de stivă nu arată spre nimic util
Singura cale productivă este să eliminați fiecare mapare euristică și să înlocuiți construirea tabloului de pagini cu o parcurgere corectă a arborelui. Odată ce indicii sunt corecți prin construcție, nu mai este nevoie de niciun petic, iar verificatorul de interval devine un atu, nu un obstacol
Dacă întrețineți o bibliotecă ce prezintă acest tipar, activați temporar verificarea intervalului într-o compilare Release și rulați-o pe un corpus divers de PDF-uri: documente produse de Word, de LaTeX, de firmware-ul unui scaner, de utilitare de divizare PDF-la-PDF. Fișierele care declanșează excepții sunt cele a căror ordine a obiectelor de pagină se abate de la ordinea de parcurgere presupusă de codul dumneavoastră. Fiecare dintre ele este un punct de date, nu un defect separat
Pentru codul nou care apelează o bibliotecă PDF pentru Delphi, sfatul practic este să tratați numărul de pagini raportat de bibliotecă drept autoritate și să nu transmiteți niciodată un index derivat din calcule pe date externe fără a confirma mai întâi că se încadrează în 0..PageCount - 1. Componenta HotPDF pentru Delphi expune numărul de pagini rezolvat prin THotPDF.PageCount după BeginDoc sau după încărcarea unui document; acea valoare reflectă întotdeauna parcurgerea arborelui de pagini și poate fi folosită în siguranță ca limită superioară pentru orice calcul de indici