Technical Article

Memory-Mapped PDF Reading in Delphi: Sliding Window

PDFlibPas can open a local PDF through a bounded read-only memory-mapped view: LoadFromMappedFile and DAOpenMappedFile keep exactly one sliding window over the file, remap it on demand, and serve every object slice through absolute-offset reads. The Delphi PDF library never holds the whole source in memory, so address-space use stays flat as the file grows. The design exists for one workload: gigabyte PDFs where the parser has finished loading and is still going back to disk, object by object, stream fragment by stream fragment

Why do sparse reads stay expensive after the PDF is loaded?

Loading a PDF does not finish reading it, and on a multi-gigabyte file that gap is where the time goes. A cross-reference table or cross-reference stream (ISO 32000-1 §7.5.4 and §7.5.8) only records where each indirect object starts. The bytes arrive later, when a page is rendered, a font program is decoded, or an embedded file stream (ISO 32000-1 §7.11.4) is extracted. A 2 GB archive with tens of thousands of objects becomes tens of thousands of small unordered reads, and none of them are known at load time

The path those reads used to take was a shared Seek followed by Read on one positional stream, and it fails in two directions at once. Every fragment pays for a file read even when the page is already resident in the operating-system cache, and the cursor is shared mutable state, so a local file and the byte-range source behind progressive PDF range loading with prefetch could not run the same parser code without fighting over the position. PDFlibPas fixes both by promoting absolute-offset reading from an optimisation to a contract

What does TPDFReadAtStream guarantee?

TPDFReadAtStream guarantees a read at an absolute offset that neither depends on nor disturbs the logical stream cursor. It is an abstract TStream descendant with exactly one virtual method, and both cursor-independent sources in the library derive from it: TReadOnlyMappedFileStream for local files and TByteRangeStream for range-served remote ones. The object-slice reader asks once whether its source is a TPDFReadAtStream and falls back to the old seek-then-read sequence when it is not, so an ordinary file stream or memory stream keeps working unchanged

type
  // Read-only streams whose absolute reads avoid a shared Seek followed by Read
  TPDFReadAtStream = class(TStream)
  public
    function ReadAt(Offset: Int64; var Buffer;
      Count: LongInt): LongInt; virtual; abstract;
  end;

  // Windowed read-only access to one local file
  TReadOnlyMappedFileStream = class(TPDFReadAtStream)
  private
    FMemoryMapped: Boolean;
  public
    constructor Create(const FileName: WideString; WindowSize: Int64 = 0);
    function GetStats: TPDFMappedFileStats;
    function ReadAt(Offset: Int64; var Buffer;
      Count: LongInt): LongInt; override;
    property MemoryMapped: Boolean read FMemoryMapped;
  end;

The distinction matters more than the signature suggests. ReadAt uses the offset it is handed and leaves Position exactly where it was, which is what lets nested parser levels issue reads without a save-and-restore dance around every call. TReadOnlyMappedFileStream still implements Read, Seek and Size like any other TStream, Seek clamps the logical position into the file, and Write always returns 0 because the source is opened read-only

Opening a PDF through a mapped view in Delphi

Two explicit entry points open a mapped source, and neither one changes the behaviour of the entry points you already use. LoadFromMappedFile loads and selects a document; DAOpenMappedFile returns a Direct Access handle over the same file, which is the mode you want when merging and splitting gigabyte PDFs through Direct Access. LoadFromFile and DAOpenFile keep their file sharing, error and compatibility semantics untouched, so nothing shifts under callers that do not opt in. Both mapped entry points take a requested WindowSize in bytes and an Options bitmask, and both accept 0 for either

var
  Pdf: TPDFlib;
  Payload: AnsiString;
  Info: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    // WindowSize 0 selects the 64 MiB default; mapping is mandatory here
    if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
      PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
      raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
        [Pdf.LastErrorCode]);

    // Deferred extraction now walks mapped windows instead of seeking
    Payload := Pdf.GetEmbeddedFileContentToString(1);
    if Pdf.GetMappedFileInfo(Info) = 1 then
      Writeln(Info);
  finally
    Pdf.Free;
  end;
end;

What does PDF_MAPPED_FILE_REQUIRE_MAPPING actually enforce?

PDF_MAPPED_FILE_REQUIRE_MAPPING converts a silent fallback into an immediate, diagnosable failure at open time. With Options left at 0 both entry points accept a read-only file-stream fallback: if the platform has no mapping code, or the mapping call fails, the document still opens and every read goes through a normal file stream. With the flag set, PDFlibPas accepts the input only when the first view was established, and reports refusal through LastErrorCode 401 rather than loading a document that quietly behaves exactly like the old path

On Windows the mapped stream opens a second read-only handle with FILE_SHARE_READ, FILE_SHARE_WRITE and FILE_SHARE_DELETE plus FILE_FLAG_RANDOM_ACCESS, creates a PAGE_READONLY mapping over it, and maps the first window inside the constructor. Mapping eagerly is the whole point: a "mapping required" failure surfaces at LoadFromMappedFile, not at the first lazy object read halfway through a rendering job. Be clear about where the guarantee stops, though. The mapping code is compiled only for Windows targets, and a zero-byte file never attempts a mapping at all, so PDF_MAPPED_FILE_REQUIRE_MAPPING is a request that can legitimately fail rather than a portable promise. A negative WindowSize, or any bit in Options other than the one documented value, is rejected outright with the same error 401

One window, remapped to the allocation granularity

Only one view is ever retained, and that is what keeps address-space use independent of file size. A WindowSize of 0 selects 64 MiB; a value below the system allocation granularity is raised to it; a value above 1 GiB is capped; and the result is rounded up to a whole number of granularity units, 65536 bytes on Windows unless GetSystemInfo reports a different dwAllocationGranularity. When a read lands outside the current view, PDFlibPas unmaps it, aligns the requested offset down to a granularity boundary, and maps a fresh window there. The final window is clamped to the physical file size, so the view never extends past the end of the file

A single read may cross any number of windows: the loop copies whatever the current view can supply, remaps, and continues, and a request that runs off the end returns a short count instead of failing. What PDFlibPas deliberately does not do is hand you a pointer into the view, because the next cross-window read invalidates it and no caller could reasonably defend against that. Mapped bytes are copied straight into parser-owned destination buffers, which removes the extra file input buffer and the position switching, but the library makes no zero-copy claim about final parser storage. Windowing on the read side also composes with the write side, since byte-level reference shifting during a fast PDF merge streams object bytes out while the mapped source streams them in. The window-size trade-off is the obvious one: a smaller window holds less address space and remaps more often, which is usually the right call inside a 32-bit process

What the lock protects, and what GetMappedFileInfo reports

One critical section covers the mapped view, the fallback file cursor, the logical position and the statistics, and the split between the two read methods follows straight from it. ReadAt takes the lock and calls the lock-free internal reader; Read takes the same lock, calls the same internal reader at the current logical position, then advances it. Reusing the internal function instead of the public ReadAt is what avoids recursive locking, and holding the lock across the entire copy loop is what keeps a single-window remap correct under concurrent calls. One Free Pascal detail is worth knowing before you port: the FPC Windows unit declares its own record named TCriticalSection, so the field and its construction must be written as SyncObjs.TCriticalSection. Delphi compiles the unqualified form happily; FPC resolves it to a record with no Create, Enter or Leave

var
  Pdf: TPDFlib;
  Handle, PageRef: Integer;
  Info: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    Handle := Pdf.DAOpenMappedFile('archive-2026.pdf', '',
      16 * 1024 * 1024, PDF_MAPPED_FILE_REQUIRE_MAPPING);
    if Handle = 0 then
      Exit;
    try
      PageRef := Pdf.DAFindPage(Handle, 1);
      Writeln(Pdf.DAExtractPageText(Handle, PageRef, 0));

      // {"memoryMapped":true,"fileSize":...,"remapCount":...}
      if Pdf.DAGetMappedFileInfo(Handle, Info) = 1 then
        Writeln(Info);
    finally
      Pdf.DACloseFile(Handle);
    end;
  finally
    Pdf.Free;
  end;
end;
  • memoryMapped is false whenever the portable file-stream fallback is active, and it is the one field that proves a mapping was never established
  • windowSize is the effective aligned window rather than the value you requested, and mappedBytes is smaller than it in the tail window
  • mappedOffset is the allocation-aligned start of the retained view, or -1 when no view is currently active
  • readCalls counts successful in-range read requests, bytesRead counts bytes copied to callers, and remapCount includes the initial view

Targeted regressions cover cross-window absolute reads, logical cursor preservation, short reads at the tail, invalid offsets, rejected writes, remapping between separated windows, deferred extraction of a 220 KB incompressible attachment, and statistics going invalid after DACloseFile; the Win32 and Win64 headless suites each discovered 1467 tests and passed all of them with no ignored, failed, errored or leaked results. If you work with gigabyte PDFs in Delphi or C++Builder and your profiler keeps pointing at file reads rather than parsing, the mapped-file entry points are worth an afternoon of measurement, and GetMappedFileInfo will tell you whether you really got a mapping. The full API reference and a trial build are on the PDFlibPas Delphi PDF library page