Technical Article

Range Check Errors in Delphi PDF Libraries: Root Causes

Range check errors in Delphi code that handles PDF pages 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. Two things are usually behind it. One is a guard that looks complete but lets a negative index through. The other is an index computed against one page ordering and then used against another, because PDF page objects are not stored in display order

How range checking works in Delphi

With range checking enabled, either under Project Options › Building › Delphi Compiler › Compiling › Range checking or with the {$R+} directive in the unit, the Delphi RTL validates every array index, string subscript, and enumerated assignment at runtime. Neither the Debug nor the Release configuration turns it on by default, so a project can run for years without it and then start raising errors the day someone ticks the box. 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 FDocOpen and (PageIndex < Length(FPages)) and
   (FPages[PageIndex].PageObj <> nil) then

looks safe, but it only guards against an out-of-range index if FDocOpen is True and PageIndex is non-negative. The check PageIndex < Length(FPages) does nothing when PageIndex 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 (PageIndex >= 0) and (PageIndex < Length(FPages)) then
begin
  if FDocOpen and (FPages[PageIndex].PageObj <> nil) then
    Result := FPages[PageIndex].PageObj
  else
    Result := nil;
end
else
  raise ERangeError.CreateFmt(
    'Page index %d is out of range (0..%d)',
    [PageIndex, Length(FPages) - 1]);

This is the mechanical fix. It stops the crash. It does not explain why PageIndex received a value outside the valid range in the first place

Flow diagram of a Delphi range check error in a PDF library where a negative DestIndex bypasses a one-sided bounds check and raises ERangeError at the array access, compared with the corrected guard order that short-circuits safely
Under $R+ the RTL validates each subscript at run time, so a negative index survives a bare upper-bound test and fires at the array access itself. Testing the signed lower bound first lets short-circuit evaluation reject it before any element is touched

Where the bad index usually comes from

When the index arithmetic involves PDF pages, the usual source is a mismatch between two orderings. ISO 32000-1 §7.7.3 defines page order through the Kids arrays of the page tree; object numbers and byte offsets say nothing about it, and object 20 can hold page 1 while object 3 holds page 2. Code that builds its page list by scanning objects in file or object-number order works on every document whose generator happened to write pages in sequence, and goes wrong on the first one that did not: an edited file with a cover page added later, or output from a tool that reorganized the object graph. Indices that callers compute in display order then land on the wrong page, or past the end of the list. The walkthrough on PDF page order pitfalls shows how to confirm the mismatch with qpdf and how to build the page list from the page tree instead

Delphi diagram contrasting PDF object storage order discovered by a cross-reference scan against the display order recovered by walking the page tree Kids array from the catalog
Sequential scans number pages by whatever order the writer chose, while viewers follow the Kids array of the page tree. Building the page array from the catalog traversal keeps caller indices aligned and inside range

Tracking it down

If your own parsing or page-handling code shows 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 order your code assumes. Each one is a data point, not a separate bug. Resist the urge to answer each file with its own mapping (swap two pages here, apply an offset for that generator there); every such patch fits only the files on hand when it was written and fires at the wrong time on the next source, leaving an index that is wrong twice over

For code that calls a PDF library, 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. In the HotPDF Delphi component, LoadFromFile returns the page count of the loaded document and LoadedPageCount reports it afterwards; page indices follow the page tree's display order, so that value is safe to use as the upper bound for any index arithmetic