Bounded Cross-Reference Recovery

HotPDF can recover a real object graph and page tree when a PDF has a missing, invalid, or stale startxref declaration or unusable xref rows

Recovery sequence

  1. The normal spec-compliant loader runs first in the default xrrmOnFailure mode
  2. Recovery scans the source in 1 MiB chunks with a short carry between chunks and records validated indirect objects and xref anchors
  3. Markers nested inside a validated indirect object are rejected, including strings, comments, dictionaries, and binary stream data containing text such as 99 0 obj or xref
  4. Valid xref tables or streams are tried from newest to oldest through the existing incremental, hybrid, and object-stream loader
  5. At most 4,096 xref section anchors are retained, preventing adversarial marker repetition from multiplying parse attempts
  6. If no xref section produces a real page tree, direct-object offsets are rebuilt and duplicate object numbers use the latest physical definition
  7. Recovery succeeds only when a real Catalog, Pages tree, and at least one page leaf resolve

Declarations

type
  THPDFXRefRecoveryMode = (
    xrrmDisabled,
    xrrmOnFailure,
    xrrmForce
  );

  THPDFXRefRecoveryStatus = (
    xrrsNotNeeded,
    xrrsSucceeded,
    xrrsDisabled,
    xrrsScanLimitExceeded,
    xrrsObjectLimitExceeded,
    xrrsXRefLimitExceeded,
    xrrsFailed
  );

  THPDFXRefRecoveryMethod = (
    xrrmethodNone,
    xrrmethodRelocatedSection,
    xrrmethodRebuiltObjectOffsets
  );

  THPDFXRefMergeInfo = record
    HybridRevisionCount: Integer;
    HybridOverlapCount: Integer;
    OlderRevisionShadowedEntryCount: Integer;
    DuplicateEntryCount: Integer;
    NewestRevisionPrecedenceApplied: boolean;
    HybridStreamPrecedenceApplied: boolean;
  end;

property XRefRecoveryMode: THPDFXRefRecoveryMode;
property XRefRecoveryMaxScanBytes: Int64;
property XRefRecoveryMaxObjects: Integer;

function GetLoadedXRefRecoveryInfo(
  out Info: THPDFXRefRecoveryInfo
): boolean;

function GetLoadedXRefMergeInfo(
  out Info: THPDFXRefMergeInfo
): boolean;

function GetLoadedXRefIntegrityInfo(
  out Info: THPDFXRefIntegrityInfo
): boolean;

Configuration

MemberDefaultPurpose
XRefRecoveryModexrrmOnFailureDisables recovery, runs it only after normal parsing fails, or forces it before normal parsing
XRefRecoveryMaxScanBytes8 GiBHard maximum source size accepted by the chunked recovery scan
XRefRecoveryMaxObjects2,000,000Hard maximum validated indirect-object candidates accepted by recovery

Recovery information

GetLoadedXRefRecoveryInfo returns True when recovery was attempted and copies the most recent THPDFXRefRecoveryInfo record

Merge precedence information

GetLoadedXRefMergeInfo returns True when xref-stream or hybrid-reference loading encountered merge state worth reporting

Integrity information

GetLoadedXRefIntegrityInfo reports the active free-list chain, invalid links, cycles, unlinked free entries, generation transition errors, duplicate live definitions, and older entries shadowed by the newest revision

Shadowed detail includes active and rejected entry types, offsets or storage fields, generations, and revision depth, with a fixed 256-entry detail ceiling

Saving recovered documents

A recovered document always uses the full-rewrite path in SaveLoadedDocument, even when no object was edited, so HotPDF never copies damaged source bytes as an apparently repaired output

Example

var
  Info: THPDFXRefRecoveryInfo;
  PDF: THotPDF;
begin
  PDF := THotPDF.Create(nil);
  try
    PDF.XRefRecoveryMode := xrrmOnFailure;
    PDF.XRefRecoveryMaxScanBytes := Int64(16) * 1024 * 1024 * 1024;
    PDF.XRefRecoveryMaxObjects := 3000000;
    if PDF.LoadFromFile('damaged.pdf') > 0 then
    begin
      if PDF.GetLoadedXRefRecoveryInfo(Info) then
        WriteLn(Info.Diagnostic);
      PDF.SaveLoadedDocument('recovered.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

Related APIs