Optional module · pdftoolkit/pdf-reader 0.7.0

English | Français

Reader exposes the actual source prefixes of a chronological incremental PDF. These are immutable physical views, with the original bytes, cross-reference sections and effective object identities. They do not establish signature trust, DocMDP/FieldMDP authority or permission to modify the document.

use PDF\Reading\DocumentReader;
use PDF\Reading\FileBytes;

// The application supplies and bounds acquisition of these bytes.
$history = (new DocumentReader())->readRevisionHistory(new FileBytes($bytes));
foreach ($history as $revision) {
    $raw = $revision->rawDocument();
    $exactPrefix = $revision->source();
    $xrefOffset = $revision->crossReference->section->offset->value;
    $physicalTrailer = $revision->crossReference->section->trailer;
    $effectiveTrailer = $raw->trailer();
    $effectiveEntries = $raw->crossReferences()->entries();
}

ReadRevisionHistory is countable and iterable, oldest to newest, with zero-based ordinals and latest(). Its source preserves every supplied byte. Each raw ReadDocument resolves objects using only that prefix's effective index: later replacements cannot leak into earlier graphs; free entries and reused generations retain their existing semantics. PhysicalCrossReference preserves the section, its lexical endOffset, its stream flag and the optional hybrid auxiliary. The table remains the owner of /Prev; the supplement's entries take precedence.

An initial linearized file has two xref sections but only one complete physical revision. Its first-page section owns crossReference; linearized retains the main section, the optional first-page dummy footer, the linearization dictionary's source span and its declared L. physicalParts() returns the exact xref bodies, including any hybrid supplements. A valid L fixes the initial prefix length inside its actual EOF interval, even when updates append more whitespace. This groups physical sections; it does not interpret hint streams or certify optimized network delivery.

Exact EOF and signed lengths

Each RevisionBoundary records five absolute offsets:

Property Meaning
footerOffset First byte of the actual startxref keyword, after any indexed trailing objects.
eofOffset First % of the verified %%EOF marker.
eofMarkerEnd First byte after the five-byte marker; minimum admissible length.
endOffset Conventional prefix end, including the marker's first line ending when present; a linearized initial revision uses its verified L.
maximumEndOffset First subsequent non-whitespace byte, or the source length.

acceptsByteLength($length) checks the inclusive interval from eofMarkerEnd to maximumEndOffset. Only PDF whitespace may lie in that interval. The default source() returns the conventional prefix; source($length) returns exactly that admissible prefix, without trimming, appending or normalizing any byte. Out-of-interval lengths raise ParseException.

A signature verifier must choose the length from the actual ByteRange, bind that exact source and independently validate the range, Contents and CMS. Merely matching an EOF interval proves none of those things. This library operation does not choose a signature's signed bytes or create a writer FileRevision.

Raw objects and authentication

ReadDocument::crossReferences()->entries() exposes effective entries, including free and unavailable entries. objectSpan($identifier) returns an ordinary object's SourceObjectSpan, from its actual header through endobj. It returns null for compressed members, missing/free objects and wrong generations. Compressed members have positions inside decoded containers, not independent source-file spans.

rawDocument()->sourceRevision()->span($string) exposes parser-established string-token provenance, including the delimiters of a signature's Contents. An arbitrary string with the same value does not acquire that provenance. Signature Contents also retain their original span after a separate authenticated read. Ordinary decrypted strings are not claims about original plaintext tokens.

Physical inspection deliberately performs no authentication and applies no source-integrity policy. Raw strings and stream data may be ciphertext; permissions() and mac() remain null. Do not interpret that graph as authenticated content. To interpret a selected prefix, explicitly call a separately configured DocumentReader::read($revision->source($exactLength)) with the required password or recipient capability and integrity policy. Its normal authentication and MAC checks still run. Encrypted object streams are decoded only in that separate authenticated graph. Inspection cannot resolve a structural stream length that itself requires decoding an encrypted object stream.

Bounds and supported physical organization

All views from one history share one ReadBudget: parsed values, xref entries, decoded data and retained bytes accumulate across revisions. Each retained prefix, effective index and ordinary-object span is charged, as are additional exact prefix copies. Cached access to the conventional prefix does not copy it again. ReaderLimits::maxRevisions and all existing parsing/decompression limits apply. An authenticated read() is a separate operation with its own budget; applications must bound how many such operations they request.

Inspection validates every effective ordinary object's physical extent, including historical versions, before returning the history. Extents must not overlap each other, xref bodies or footers. The bytes between known extents must be whitespace or comments; fake object headers in strings, streams or comments cannot claim an independent object. Old revisions cannot address future bytes. Extra unindexed syntax and an appended footer pointing back to an older xref are rejected.

The supported organization is a chronological /Prev chain of tables, streams or hybrid sections. Ordinary indexed objects may follow a primary xref stream before its footer; their actual extents, including indirect stream lengths, are validated before locating startxref. The initial linearized pair is recognized from its actual first object and bounded direct parameters. Its forward /Prev must identify the main section exactly, with no further /Prev; its final footer points back to the first-page section. Arbitrary forward chains, unindexed objects and comments inside the footer are explicitly outside this inspection API. Some are valid PDF layouts; ordinary read() retains its previous behavior. This is a bounded structural inspection service, not a PDF conformance validator or repair operation.

Independent validation

tools/validate_revision_history.py produces table, stream and hybrid histories, both clear and AES-256 encrypted, using unmodified pyHanko APIs. Each has an initial revision, a real CMS approval signature, and a later object update. The hybrid initial source has an actual companion table and /XRefStm; its signature is created afterwards. A separately assembled initial layout adds indexed objects after its xref stream, including a stream with an indirect length; unmodified pyHanko signs and updates that source too. Four initial files are independently linearized with qpdf (tables/streams, clear/encrypted), then signed and updated by pyHanko. PyHanko counts their two initial xref sections separately; the oracle maps them explicitly to one complete physical revision. Five existing pypdf/qpdf fixtures add deletion and generation reuse. All 42 prefixes from 16 sources pass qpdf without recovery. PyHanko independently checks xref revision offsets, object resolution and 22 mathematical signature verifications. Validation is offline with no trusted certificate roots; certificate trust and authorization of later modifications are deliberately not asserted.

Native tests add forged offsets/footers, overlapping object identities, future references, raw encrypted access, Contents provenance and cumulative budgets. CI runs this oracle and the existing checks on PHP 8.3, 8.4 and 8.5. The source clauses and scope are recorded in the normative mapping.

Components · Security · Source MAC

Guide source at the documented commit ↗