Range check errors in Delphi PDF libraries earn a reputation for being difficult to pin down because they don't follow a consistent input pattern. The same document produces them on one machine and not another; the same code path fires the exception on a 3-page file but runs clean on a 12-page one. That inconsistency almost always traces to a single root cause: PDF page objects are not stored in file order. If the library builds its internal page array by scanning objects sequentially rather than walking the page tree declared by the catalog, it constructs an index whose valid range does not match what callers expect, and range checking catches that mismatch at the worst possible moment.
How range checking works in Delphi
With the {$R+} compiler directive active (the default in Debug configuration), the Delphi RTL validates every array index, string subscript, and enumerated assignment at runtime. An out-of-bounds access raises ERangeError rather than silently reading adjacent memory. That behavior is valuable: it surfaces latent bugs early instead of letting them corrupt a data structure that only fails a hundred lines later. The frustrating part is that the exception fires at the access site, not at the point where the index was computed incorrectly. When the call stack shows a deeply nested method in a PDF unit, the real mistake is usually several frames back.
Compound boolean conditions make this worse. Delphi evaluates and expressions left to right with short-circuit semantics, but short-circuiting only skips evaluation when the left side is False. An expression like:
if FDocStarted and (DestIndex < Length(PageArr)) and
(PageArr[DestIndex].PageObj <> nil) then
looks safe, but it only guards against an out-of-range index if FDocStarted is True and DestIndex is non-negative. The check DestIndex < Length(PageArr) does nothing when DestIndex is negative, because comparing a negative integer to a non-negative length returns True in signed arithmetic and the subsequent array access still fires the range error. Moving the bounds check to the outermost position is the correct fix:
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]);
This is the mechanical fix. It stops the crash. It does not explain why DestIndex received a value outside the valid range in the first place.
The real cause: object order versus page order
ISO 32000-1 §7.7.3 defines the page tree as a tree of Pages nodes whose Kids arrays list page objects in display order. The file stores those objects at whatever offsets the writer happened to choose; object number 20 can physically precede object number 3 in the byte stream. A library that builds its page list by iterating the cross-reference table in object-number order rather than following the Kids chain will produce a sequence that diverges from what the user expects. On documents where the generator happened to write pages in order, everything works. On documents where it did not, the discrepancy between the library's page numbering and the caller's page numbering produces indices that fall outside PageArr.
The correct approach is to start from the catalog, resolve the /Pages indirect reference, and walk the Kids array recursively. For a flat document with no intermediate Pages nodes, the traversal is straightforward:
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;
After this runs, PageArr[0] is the first page a viewer would display, regardless of where that object sits in the byte stream. Indices passed by callers that assume display order now map correctly, and the range errors stop.
Hard-coded workarounds compound the problem
In codebases where the root cause was never identified, it is common to find heuristic patches: swap first and last page if total count equals 3, rotate the index for documents from a specific generator, apply an offset when the first object number exceeds a threshold. Each of those patches fits exactly the set of test files that were on hand when it was written. Add a different PDF source and one of the patches fires at the wrong time, producing an index that is now doubly wrong: wrong because it was computed from an out-of-order array, and wrong again because an inapplicable mapping was applied on top. The range checker catches it somewhere downstream and the stack trace points nowhere useful.
The only productive path is to remove every heuristic mapping and replace the page array construction with a proper tree walk. Once the indices are correct by construction, no patches are needed and the range checker becomes an asset rather than an obstacle.
If you are maintaining a library that exhibits this pattern, enable range checking in a Release build temporarily and run it against a diverse corpus of PDFs: documents produced by Word, by LaTeX, by scanner firmware, by PDF-to-PDF split utilities. The files that trigger exceptions are the ones whose page object order diverges from the traversal order your code assumes. Each one is a data point, not a separate bug.
For new code that calls a Delphi PDF library, the practical advice is to treat the library's page count as authoritative and never pass an index derived from arithmetic on external data without first confirming it falls within 0..PageCount - 1. The HotPDF component exposes the resolved page count through THotPDF.PageCount after BeginDoc or after loading a document; that value always reflects the pages tree traversal and is safe to use as the upper bound for any index arithmetic.