“When updating a PDF file incrementally, changes shall be appended to the end of the file, leaving its original contents intact.”
ISO 32000-1:2008 clause 7.5.6 also requires the added trailer to carry /Prev, the byte offset of the previous cross-reference section. The embedded qpdf fixture gives the instrument a specimen whose answer is known before your file is seen.
The live check at right starts from the embedded raw bytes, recomputes the SHA-256, parses each table, follows literal /Prev values, and resolves the winning object entries. Nothing is accepted because these expected numbers are printed here.
The worker is reproducing the embedded anchor before accepting a reader specimen.
Bring the file
The fallback is already running. Replace it with any PDF you possess. The complete file is read through a local FileReader, transferred into a dedicated worker, and never passed to a network API. The parser treats it as hostile bytes. It does not execute embedded JavaScript, follow actions, submit forms, or dereference external URLs.
Local PDF intake
Input ceiling: 100 MiB. Decoded-stream ceiling: 512 MiB. Work is bounded by object, section, nesting, and time limits. Files over a limit are refused with the exact limit and byte location available.
The chain it can earn
/XRefStm is supplemental to its revision, never counted as another revision.Earlier bytes are not only plumbing
The parser slices only at a startxref and EOF pair that points back to the accepted cross-reference section. Each slice goes to the browser's own PDF renderer through a local Blob URL inside a sandboxed frame. This is a separate test from structural recovery. A slice may be structurally reachable and still fail to render because of encryption, unsupported filters, damage, missing external streams, or renderer differences.
The fallback's first two reachable slices render page 1 as Potato 0. The third renders Potato 0 new. If your browser declines PDF rendering in sandboxed frames, the object and text checks below still run independently. Selecting the newest section shows it against its immediate predecessor, because a frame compared with itself would agree no matter what the file contained.
Which definitions won
PDF object identity is an object number together with its generation number. Moving from older to newer reachable sections, the newest entry wins. A same-generation replacement is labelled superseded, a higher-generation return after a free entry is reused, and a free entry is deleted. This is object resolution, not an account of why the software made the change.
A cross-reference entry is a claim about where a definition sits, and a table can name a byte that holds nothing. Each winning entry is therefore read back from the raw bytes and only called a definition when those bytes begin its own object generation obj header. A type 2 entry has to name a container object that itself resolves. Entries that fail are shown as unverified claims, not as definitions that won.
| object + gen | classification | this entry | bytes at that offset | previous winner | xref source |
|---|
When later paint sits over earlier text
A black rectangle does not remove text merely by being drawn later. The bench tokenizes decoded content streams, tracks basic text state and rectangle fills in operator order, and compares their conservative geometry. A hit is labelled only later paint covers earlier text. It is never called a redaction, a secret, or misconduct.
| stream object | best-effort text | raw character codes |
|---|
No paint diagnostic selected.
The geometry-only diagnostic lift suppresses the selected rectangle's fill and retains its dashed outline. It is not a full page renderer. It never rewrites the PDF and never offers a supposedly sanitized copy. Text decoding can be wrong for composite fonts, custom encodings, ligatures, bidirectional text, Type 3 glyphs, clipping text, and missing ToUnicode maps. Raw codes remain beside the guess.
What the chain means, and what it does not
A full rewrite, optimization, print-to-PDF operation, sanitization pass, or history-stripping save can leave an edited document with one reachable section.
Signatures, form filling, annotations, metadata, automated software, and repair tools can append sections. One user action can create several.
A revision that parses is not thereby genuine. Authorship and timing require separate signature and trust-chain verification. Metadata alone settles neither.
A newest graph can be clean while sensitive bytes survive in an older section. Failure to recover an older copy does not certify that the bytes are absent elsewhere.
/Subtype /Redact marks content proposed for removal until an application applies it. ISO 32000-1 clause 12.5.6.23 requires an applying reader to remove all traces, not merely hide them.
Opaque overlap may be design, an OCR layer, a form control, a highlight, or a reusable template. Geometry cannot recover intent.
A linearized file links its first-page table to the main table with /Prev and was never updated. Counting links, /Prev keys, or %%EOF markers would report edits that did not happen. A section is only accepted here when it sits later in the file than what it points back to, so linearized files are refused outright, including ones later signed. That costs you a common kind of file and is the price of not inventing an edit.
Absence of a chain proves nothing, and presence of one proves only that bytes were appended. The instrument is built to make the second mistake as hard as the first, which is why a forward-pointing link is refused rather than counted.
The refusal is part of the instrument
Literal string counting is not used to invent revisions. The byte sequences xref, startxref, %%EOF, and /Prev can occur in comments, strings, and streams. A section's boundary is read at one place only: the byte where that section's own syntax ends, past its trailer dictionary for a classic table or past endstream and endobj for a cross-reference stream, with nothing but whitespace and comments allowed in between. The startxref found there must print that section's own offset. A matching sequence anywhere else in the file, including a perfect startxref, integer and %%EOF sitting inside a content stream, is not a boundary and cannot move a revision's end.
Disclosed change after seeing data: the first build of this instrument searched the whole file for startxref pairs and took the last one that named each section. A specimen built during review put a working startxref 70 %%EOF inside a later object's stream, and that search moved the first revision's end 63 bytes into the second update and cut it mid-stream. The rule above replaced it, and that specimen is now a fixed regression test with the correct slice ends of 160 and 344 bytes. Two further rules were added the same way, after files disagreed with the code: the append-order rule below, and reading back every winning entry's object header.
Constructed failures checked offline
- TOO_SHORT
- Input ends before a PDF header and trailer can exist.
- BAD_MAGIC
- The first 1,024 bytes contain no
%PDF-header, with the actual opening bytes reported. - BAD_STARTXREF_NUMBER
- The final token is not followed by an integer at all, with the bytes that were found instead.
- NO_EOF_BOUNDARY
- The integer parsed but no
%%EOFcloses it. A truncated file is a different failure from a malformed offset and is named as one. - STARTXREF_OUT_OF_RANGE
- The final integer points beyond the supplied file length.
- BOUNDARY_MISSING
- A reachable section is not closed by
startxrefand%%EOFat the byte where its own syntax ends. - BOUNDARY_TARGET
- The boundary that closes a section prints some other offset. The linearized first-page table, whose startxref is 0, is the ordinary case.
- NOT_APPEND_ORDERED
- A section points back to an offset later in the file than itself, so it was not appended after what it points at.
- XREF_ENTRY
- A classic table entry violates the strict 20-byte form, with its object and byte offset reported.
- STREAM_LENGTH
- An xref stream's declared byte length does not land on
endstream. - PREV_CYCLE
- The linked chain returns to an offset it already visited.
- ENCRYPTED
- A reachable trailer contains
/Encrypt. This instrument does not attempt a password.
The verifier builds broken PDFs from bytes, asserts each refusal code together with the byte offset it reports, and also proves that flexible xref spacing fails in strict mode before the explicit recovery switch accepts it.
Containment you can inspect
The policy in this document sets connect-src 'none', so fetch, XHR, WebSocket, EventSource, and beacon connections are blocked. The fallback bytes are embedded in a same-origin module, not fetched. When a specimen is chosen, the page records a Resource Timing baseline and wraps upload-capable browser APIs with visible counters. It separates HTTP network entries from local Blob frame entries, which the browser may also place in Resource Timing. The browser's own Network panel should show no request carrying the file. The Forget button terminates the worker and revokes every Blob URL.
This is inspectable containment evidence. It is not a metaphysical proof that the browser, an extension, or the operating system is benign. The counters observe this page's code paths and the policy constrains this document.
Sources and provenance
- ISO 32000-1:2008, Document management, Portable document format, Part 1: PDF 1.7, clauses 7.5.4 to 7.5.8 and 12.5.6.23. Used for trailers, incremental updates, newest-copy-wins resolution, cross-reference streams, EOF boundaries, and applied redaction. opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf
- ISO 32000-1:2008 catalogue record, edition 1, publication 2008-07. Used to confirm the standard number, title, edition, and publication date. iso.org/standard/51502.html
- append-page-content.pdf, qpdf test suite at commit
8ff6b5c4fca59e38b147aebddeb54341fc313ed1, by Jay Berkenbilt and Manfred Holger. It is 17,370 bytes with SHA-2562a70474d0185222b0cb22089de654f8f2a4d207b7f61b646a030b1e563da837b. Distributed here under Apache License 2.0. The original binary is available locally, and its bytes are also embedded so the strict connection policy can remain closed. - qpdf README and LICENSE.txt at the same commit. Used for Apache License 2.0 provenance and redistribution terms. qpdf is copyright 2005-2021 Jay Berkenbilt and 2022-2026 Jay Berkenbilt and Manfred Holger. The complete Apache License 2.0 text is shipped beside the fixture.