Technical Article

PDFium Byte Range Loading for Embedded PDFs in Delphi

With overflow checking off, 2000000000 + 2000000000 evaluates to -294,967,296 when both operands are Delphi Integer values. That number stops being trivia the moment a container parser reads an offset and a length out of a record header and adds them to see whether an embedded PDF fits inside the buffer. The sum wraps negative, the comparison against Length(Data) passes, and the next line takes the address of a byte roughly two gigabytes past the end of the array. Loading a PDF from the middle of a larger buffer needs a range check that never adds, and a clear answer to who owns the bytes once the document is open

This article covers documents that are already in RAM, sitting at a known offset inside something else. Documents too large to hold in memory are a different problem, handled by pulling blocks from disk on demand as described in streaming large PDFs on demand with PDFium VCL. The last section draws the line

An embedded PDF at a known offset

A mail store keeps message bodies and attachments in one record. An archive container concatenates a manifest, a few images and a PDF. A custom wire protocol frames a document behind a length-prefixed header. Suppose the record is 40 MB, the PDF starts at byte 1,182,336 and runs for 312 KB. The habitual code calls Copy(Data, Index, Count) to cut a slice, then hands that slice to a loader that copies it again into its own buffer. Counting the container, the same 312 KB now exists three times, and a mailbox scan repeats that for every message

PDFium Component addresses the window directly. TPdf declares LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean = True), where Index is a zero-based byte position and Count is a byte length. The method validates both numbers, computes a single pointer, and forwards to the pointer overload LoadDocument(Data: Pointer; Size: NativeUInt; Buffered: Boolean), which opens the document through FPDF_LoadMemDocument64. The whole-array form LoadDocument(const Data: TBytes; Buffered: Boolean) is a one-line call to the byte-range form with 0 and Length(Data), so every TBytes load passes through the same validation

Comparison diagram showing the slice-then-load double-copy path for an embedded PDF inside a container in Delphi versus the PDFium Component byte-range LoadDocument overload that addresses the window in place
Slicing with Copy and then loading copies the same 312 KB window twice; the byte-range call addresses it in place and Buffered decides whether anything is copied at all

In code, the slice simply disappears. LocateEmbeddedPdf stands for your own container parser

uses
  System.SysUtils, System.IOUtils, PDFium;

procedure PreviewEmbedded(const RecordFile: string);
var
  Frame: TBytes;           // whole container record, tens of megabytes
  Offset, Size: Integer;
  Pdf: TPdf;
begin
  Frame := TFile.ReadAllBytes(RecordFile);
  LocateEmbeddedPdf(Frame, Offset, Size);   // your parser, not part of the component

  Pdf := TPdf.Create(nil);
  try
    // No Copy(Frame, Offset, Size): the window is addressed in place
    Pdf.LoadDocument(Frame, Offset, Size);  // Buffered defaults to True
    Frame := nil;  // fine here: only the PDF window was copied
    Writeln('Pages: ', Pdf.PageCount);
  finally
    Pdf.Free;      // closes the document
  end;
end;

When the global EnableV8Engine is still False and the PDFium library has not been loaded yet, the memory loaders scan the incoming bytes for XFA markers and switch EnableV8Engine on if they find any, so the V8-capable DLL gets loaded. With the byte-range call that scan sees only the window. A manifest or a second document elsewhere in the same record cannot trigger it

Why does Index plus Count overflow the bounds check?

Because Index and Count are both 32-bit signed Integer values, and adding two large positive ones does not reliably produce a large positive result. Where the offset and length come from a file header, an attacker or a corrupt record controls both. The same trap waits in your own container parser, which computes those numbers first

// A helper for your own container parser (suggested, not part of the component)
function IsValidWindow(const Data: TBytes; Index, Count: Integer): Boolean;
begin
  // WRONG: Index + Count is evaluated as Integer and can wrap negative
  //   Result := (Index >= 0) and (Index + Count <= Length(Data));

  // RIGHT: reject signs, bound Index on its own, then compare Count
  // against the remainder; the only arithmetic is a subtraction that cannot wrap
  Result := (Index >= 0) and (Count >= 0) and
            (Index <= Length(Data)) and
            (Count <= Length(Data) - Index);
end;

Work the failing case through. With Index = 2000000000 and Count = 2000000000, the true sum is four billion, but 32-bit signed arithmetic stores it as -294,967,296. That value is comfortably below any Length(Data), so the wrong check passes and @Data[Index] points far outside the array, with a two-gigabyte length attached. A crash is the good outcome. The bad one is PDFium parsing whatever process memory happens to live there

The correct order never adds. Negative values are rejected before anything is indexed. Index is then bounded against Length(Data) alone, which guarantees that Length(Data) - Index is a non-negative Integer, and only then is Count compared with that remainder. Delphi evaluates and with short-circuit logic by default ({$B-}), so each later term only runs once the earlier ones hold. Do not lean on {$Q+} as the safety net: release builds routinely ship with overflow checking off, and with it on you have only swapped a memory-safety hole for an EIntOverflow escaping from the middle of a validation routine

Flow diagram contrasting the overflowing Index plus Count bounds check with the subtraction-only validation order that keeps PDFium byte-range loading memory safe in Delphi
Adding first wraps past MaxInt and defeats the guard; rejecting signs and bounding terms before one wrap-free subtraction keeps every intermediate representable

The byte-range LoadDocument runs the same four tests in the same order and raises EPdfError with a message naming the failed rule: "PDF byte range index cannot be negative", "PDF byte range count cannot be negative", "PDF byte range index exceeds data length" and "PDF byte range exceeds data length". You can reproduce each one with a four-byte array. Index = 5, Count = 0 trips the third message, Index = 2, Count = 3 trips the fourth, and after every rejection Pdf.Active is still False. The same treatment of untrusted lengths across the native boundary is covered more broadly in hardening the PDFium VCL ABI and memory safety in Delphi

A zero-length window is still a legal call

An empty window passes a nil pointer, because @Data[Index] is not a legal expression for every Index the validation accepts. Index = Length(Data) with Count = 0 is a well-formed empty window at the tail of the buffer, and an empty TBytes gives Index = 0 on an array that has no element zero. Taking the address in either case reads past the end or dereferences a nil dynamic array, and with range checking on, Delphi raises ERangeError before the loader even runs

So the byte-range overload branches: Count = 0 yields nil, any other count yields @Data[Index]. The pointer overload accepts nil only together with a size of zero, PDFium then declines to open an empty buffer, and the call ends in an ordinary EPdfError carrying PDFium's last-error text, or "Cannot load PDF document" when PDFium reports none. A header that claims a zero-byte attachment gets the same catchable exception as any other bad input. Rejecting it in your parser, with the record number in the message, still reads better in a support log

Buffered decides who owns the bytes

Buffered is the only parameter here whose consequences outlast the call. With Buffered = True, the component copies the selected window, and only the window, into its own buffer before loading. In the 40 MB example, 312 KB are copied and the container is left alone. Once LoadDocument returns, you may free, reuse or overwrite the container immediately

With Buffered = False, @Data[Index] goes straight to FPDF_LoadMemDocument64, and PDFium keeps reading through that pointer for as long as the document is open. The entire backing TBytes becomes borrowed. It must stay alive and unmodified until you set Active := False or free the TPdf. The whole array matters, not just the window: a Delphi dynamic array is reference counted as one block, so dropping the last reference anywhere in your code frees the memory PDFium is still reading. SetLength is just as fatal. A reallocation can move the block

Two Delphi details make this easier to get wrong than it looks. Dynamic arrays have no copy-on-write, unlike strings, so Other := FFrame; Other[0] := 0; writes into the very bytes PDFium is parsing. And a receive buffer that your network code reuses for the next frame gets overwritten under the open document. Passing a function result straight in, as in Pdf.LoadDocument(ReadRecord(Id), Offset, Size, False), leaves the array held only by a hidden temporary that dies with the calling routine, while the document lives on

Timeline diagram of the two Buffered modes when loading an embedded PDF from a byte range in Delphi: copying only the PDF window and freeing the container immediately versus borrowing the whole backing TBytes until the document is closed
Buffered = True copies only the window so the container is disposable, while Buffered = False leaves the whole backing array borrowed until the document is closed

Keep Buffered = True for anything that crosses a thread, sits behind a reusable buffer, or outlives the routine that loaded it. The extra cost is one copy of the PDF's own size, and it buys the freedom to drop a 40 MB record the moment the call returns. Borrowing earns its place only when the container is already a long-lived field you hold for the whole session anyway, such as a viewer that keeps an archive record open while the user pages through the attachment. In that case, make the ownership explicit in one class

type
  TFrameSession = class
  private
    FFrame: TBytes;   // owns the backing storage while FPdf has a document open
    FPdf: TPdf;
  public
    constructor Create(const AFrame: TBytes);
    destructor Destroy; override;
    procedure OpenEmbedded(Offset, Size: Integer);
  end;

constructor TFrameSession.Create(const AFrame: TBytes);
begin
  inherited Create;
  FFrame := AFrame;   // shares the array; nobody else may write to it now
  FPdf := TPdf.Create(nil);
end;

procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
  FPdf.Active := False;                            // close any previous window first
  FPdf.LoadDocument(FFrame, Offset, Size, False);  // FFrame must outlive this document
end;

destructor TFrameSession.Destroy;
begin
  FPdf.Active := False;  // 1. release the borrow
  FPdf.Free;
  FFrame := nil;         // 2. only now may the storage go
  inherited;
end;

The constructor shares the caller's array rather than copying it, so the caller has to stop writing to it. If you cannot promise that, pass Copy(AFrame) to the constructor and you are back to one copy, made once per session instead of once per load

Checking that the window starts at %PDF-

A window that is off by a few bytes is the subtle failure, because the document may still open. ISO 32000-1 §7.5.4 defines every cross-reference entry as a byte offset from the beginning of the file, and §7.5.2 puts the %PDF- header at the start. When your parser miscounts a prefix and the window begins eight bytes early, those offsets are eight bytes off. PDFium looks for the header within the first 1024 bytes and compensates for leading junk; a window that starts late or ends early can push it into rebuilding the cross-reference table from the object markers it finds. Either way the page count looks right. The offset in your parser is still wrong, and the next reader of that record may be less forgiving

The overload LoadDocument(const Data: TBytes; Index, Count: Integer; const Options: TPdfLoadOptions; out Report: TPdfLoadReport; Buffered: Boolean = True) makes that visible. Report.HeaderOffset is where %PDF- was found within the first 1024 bytes of the window, or -1 when it was not found. Report.UsedRecovery is True when PDFium did not accept the cross-reference table as valid. All offsets in the report count from the first byte of the window, not from the container. A Count that is too short cuts off the trailing %%EOF, and TPdfLoadOptions.Default(plmStrict) turns on RequireFinalEndOfFileMarker, so strict mode rejects that window outright

var
  Options: TPdfLoadOptions;
  Report: TPdfLoadReport;
begin
  Options := TPdfLoadOptions.Default;   // plmCompatible
  Pdf.LoadDocument(Frame, Offset, Size, Options, Report, True);

  // LogWarning is your own logging routine
  if Report.HeaderOffset > 0 then
    LogWarning(Format('Window starts %d bytes before %%PDF-, check Offset',
      [Report.HeaderOffset]))
  else if Report.HeaderOffset < 0 then
    LogWarning('No %PDF- header in the first 1024 bytes of the window');

  if Report.UsedRecovery then
    LogWarning('Cross-reference table was rebuilt, check Offset and Size');
end;

The audit behind those fields has a cost that matters for borrowed loads. When the window fits within Options.AuditByteLimit (256 MB by default), the audit reads a temporary copy of the window, so Buffered = False combined with a report is no longer copy-free during the load. Setting AuditByteLimit to 0 skips the audit and the copy, and leaves HeaderOffset at -1. Run the audited form while you develop the container parser and against new sources of records; switch to the plain overload once the offsets are trusted. The rest of the report is covered in catching PDF load failures with a PDFium load report

When a byte range is the wrong tool

The byte-range overload assumes the whole container is already in memory, and because Count is an Integer, one window cannot exceed MaxInt bytes. A 6 GB archive on disk, or a socket you cannot rewind, does not fit that shape, and reading everything into TBytes just to address a window inside it defeats the purpose. For those cases LoadCustomDocument(AStream: TStream; AOwnsStream: Boolean) lets PDFium pull blocks from a seekable stream on demand. A small TStream descendant of your own that adds the PDF's starting offset to every Seek presents the embedded document as if it began at position zero, as the on-demand streaming article shows

The other mismatch is content that is not a plain PDF yet. If the embedded bytes are compressed, encrypted by the container, or wrapped in another encoding, a transformed copy is unavoidable, and loading that new array with the default Buffered = True is the straightforward answer. The byte range pays off in one shape: contiguous, unmodified PDF bytes, already resident, at an offset your parser has validated without adding

The byte-range call sits next to file, stream, raw-pointer and on-demand loading in the same TPdf component. The PDFium Component product page lists the supported Delphi and C++Builder versions, licensing options and the rest of the loading API