Грешките при проверка на обхвата (Range check errors) в PDF библиотеките на Delphi имат репутацията на трудни за установяване, защото не следват последователен модел на въвеждане. Същият документ ги произвежда на една машина, а на друга – не; същият път на кода предизвиква изключението при файл с 3 страници, но работи чисто при файл с 12 страници. Тази непоследователност почти винаги води до една единствена основна причина: PDF обектите на страниците не се съхраняват в реда на файла. Ако библиотеката изгражда своя вътрешен масив от страници чрез последователно сканиране на обекти, вместо да обхожда дървото на страниците, декларирано от каталога, тя конструира индекс, чийто валиден обхват не съвпада с това, което очакват извикващите функции, и проверката на обхвата улавя това несъответствие в най-лошия възможен момент
Как работи проверката на обхвата в Delphi
При активна директива на компилатора {$R+} (по подразбиране в конфигурация Debug), Delphi RTL валидира всеки индекс на масив, долен индекс на низ и изброено присвояване по време на изпълнение. Достъп извън границите поражда ERangeError вместо тихо четене на съседна памет. Това поведение е ценно: то разкрива скрити бъгове рано, вместо да им позволи да повредят структура от данни, която се проваля едва сто реда по-късно. Разочароващата част е, че изключението се задейства на мястото на достъпа, а не в точката, където индексът е изчислен неправилно. Когато стекът на извикванията (call stack) показва дълбоко вложен метод в PDF единица, истинската грешка обикновено е няколко рамки назад
Сложните булеви условия влошават това. Delphi оценява and изразите от ляво надясно със семантика на късо съединение (short-circuit), но късото съединение пропуска оценка само когато лявата страна е False. Израз като:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
изглежда безопасен, но предпазва от индекс извън обхвата само ако FDocStarted е True и DestIndex е неотрицателен. Проверката DestIndex < Length(PageArr) не прави нищо, когато DestIndex е отрицателен, защото сравняването на отрицателно цяло число с неотрицателна дължина връща True в аритметика със знак, а последващият достъп до масива все пак задейства грешката за обхват. Преместването на проверката за граници на най-външната позиция е правилната корекция:
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]);
Това е механичната корекция. Тя спира срива. Тя не обяснява защо DestIndex е получил стойност извън валидния обхват на първо място
Истинската причина: ред на обектите срещу ред на страниците
ISO 32000-1 §7.7.3 дефинира дървото на страниците като дърво от Pages възли, чиито масиви Kids изброяват обектите на страниците в реда на показване. Файлът съхранява тези обекти на каквито отмествания е избрал записващият модул; обект номер 20 може физически да предшества обект номер 3 в потока от байтове. Библиотека, която изгражда своя списък от страници чрез итериране на таблицата с кръстосани препратки (cross-reference table) в ред на номерата на обектите, вместо да следва веригата Kids, ще произведе последователност, която се отклонява от това, което очаква потребителят. При документи, където генераторът случайно е записал страниците по ред, всичко работи. При документи, където това не е така, несъответствието между номерирането на страниците в библиотеката и номерирането на страниците от извикващия код произвежда индекси, които попадат извън PageArr
Правилният подход е да се започне от каталога, да се разреши косвената препратка /Pages и масивът Kids да се обходи рекурсивно. За плосък документ без междинни възли Pages обхождането е праволинейно:
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;
След като това се изпълни, PageArr[0] е първата страница, която зрителят (viewer) би показал, независимо къде се намира този обект в потока от байтове. Индексите, подадени от извикващите функции, които предполагат ред на показване, сега се съпоставят правилно и грешките за обхват спират
Твърдо кодираните заобикаляния (hard-coded workarounds) усложняват проблема
В кодови бази, където основната причина никога не е била идентифицирана, обичайно се срещат евристични кръпки (patches): размяна на първа и последна страница, ако общият брой е 3, ротиране на индекса за документи от специфичен генератор, прилагане на отместване, когато първият номер на обект надвишава даден праг. Всяка от тези кръпки съответства точно на набора от тестови файлове, които са били под ръка, когато е била написана. Добавете различен PDF източник и една от кръпките се задейства в грешния момент, произвеждайки индекс, който сега е двойно грешен: грешен, защото е изчислен от неподреден масив, и грешен отново, защото отгоре е приложено неприложимо картографиране. Проверката на обхвата (range checker) го улавя някъде надолу по веригата и трасирането на стека (stack trace) не сочи към нищо полезно
Единственият продуктивен път е да премахнете всяко евристично картографиране и да замените конструирането на масива от страници с правилно обхождане на дървото. След като индексите са правилни по конструкция, не са необходими кръпки и проверката на обхвата се превръща в предимство, а не в пречка
Ако поддържате библиотека, която проявява този модел, активирайте временно проверката на обхвата в Release билд и го стартирайте срещу разнообразен корпус от PDF файлове: документи, създадени от Word, от LaTeX, от фърмуер на скенери, от помощни програми за разделяне (PDF-to-PDF). Файловете, които предизвикват изключения, са тези, чийто ред на обектите на страниците се отклонява от реда на обхождане, който вашият код предполага. Всеки един е точка от данни (data point), а не отделен бъг
За нов код, който извиква Delphi PDF библиотека, практичният съвет е броят на страниците на библиотеката да се третира като авторитетен и никога да не се подава индекс, извлечен от аритметика на външни данни, без първо да се потвърди, че попада в 0..PageCount - 1. Компонентът HotPDF излага разрешения брой страници чрез THotPDF.PageCount след BeginDoc или след зареждане на документ; тази стойност винаги отразява обхождането на дървото на страниците и е безопасна за използване като горна граница за всяка аритметика с индекси