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
- The normal spec-compliant loader runs first in the default
xrrmOnFailuremode - Recovery scans the source in 1 MiB chunks with a short carry between chunks and records validated indirect objects and xref anchors
- Markers nested inside a validated indirect object are rejected, including strings, comments, dictionaries, and binary stream data containing text such as
99 0 objorxref - Valid xref tables or streams are tried from newest to oldest through the existing incremental, hybrid, and object-stream loader
- At most 4,096 xref section anchors are retained, preventing adversarial marker repetition from multiplying parse attempts
- If no xref section produces a real page tree, direct-object offsets are rebuilt and duplicate object numbers use the latest physical definition
- 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
| Member | Default | Purpose |
|---|---|---|
XRefRecoveryMode | xrrmOnFailure | Disables recovery, runs it only after normal parsing fails, or forces it before normal parsing |
XRefRecoveryMaxScanBytes | 8 GiB | Hard maximum source size accepted by the chunked recovery scan |
XRefRecoveryMaxObjects | 2,000,000 | Hard maximum validated indirect-object candidates accepted by recovery |
Recovery information
GetLoadedXRefRecoveryInfo returns True when recovery was attempted and copies the most recent THPDFXRefRecoveryInfo record
Statusdistinguishes success, disabled recovery, byte-limit failure, object-limit failure, xref-anchor-limit failure, and structural failureMethodidentifies a relocated xref section or rebuilt direct-object offsetsBytesScanned,CandidateObjectCount,RecoveredObjectCount,DuplicateObjectCount,RejectedCandidateCount, andXRefCandidateCountexpose bounded work and recovery qualitySelectedXRefOffsetidentifies the relocated section when that method succeedsDiagnosticcontains a concise English result description
Merge precedence information
GetLoadedXRefMergeInfo returns True when xref-stream or hybrid-reference loading encountered merge state worth reporting
HybridRevisionCountcounts traditional revisions that contributed a side xref stream through/XRefStmHybridOverlapCountcounts table entries already supplied by the same revision's xref stream, which takes precedenceOlderRevisionShadowedEntryCountcounts entries ignored because a newer revision already defined that object numberDuplicateEntryCountcounts repeated entries inside the newest non-hybrid section- The two precedence flags state whether the corresponding rule was exercised rather than merely available
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;