Your file is the specimen · commons

What Your PDF Still Remembers

Drop a PDF and follow only the incremental update sections its own cross-reference data can validate, entirely inside your browser. The shipped qpdf fixture reproduces three linked revisions and recovers Potato 0 before object 33 0 is replaced by Potato 0 new. A missing chain does not mean a file was never edited, a present chain is not a human edit log, and this instrument does not certify safe redaction.

The published anchor runs first
“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.

fixture bytes17,370
reachable xrefs13,801 · 16,059 · 16,940
page 1 textPotato 0 → Potato 0 new

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.

Starting the embedded fallback

What the chain means, and what it does not

No chain proves nothing

A full rewrite, optimization, print-to-PDF operation, sanitization pass, or history-stripping save can leave an edited document with one reachable section.

A chain is not a save count

Signatures, form filling, annotations, metadata, automated software, and repair tools can append sections. One user action can create several.

Reachable is not authentic

A revision that parses is not thereby genuine. Authorship and timing require separate signature and trust-chain verification. Metadata alone settles neither.

Absence is not safe redaction

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.

A Redact annotation is a proposal

/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.

Paint order is neutral

Opaque overlap may be design, an OCR layer, a form control, a highlight, or a reusable template. Geometry cannot recover intent.

Linked is not appended

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.

A chain is not the accusation

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 %%EOF closes 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 startxref and %%EOF at 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

  1. 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
  2. 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
  3. append-page-content.pdf, qpdf test suite at commit 8ff6b5c4fca59e38b147aebddeb54341fc313ed1, by Jay Berkenbilt and Manfred Holger. It is 17,370 bytes with SHA-256 2a70474d0185222b0cb22089de654f8f2a4d207b7f61b646a030b1e563da837b. 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.
  4. 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.

Boundary of the apparatus

This parser supports classic xref tables, xref streams, and hybrid tables, with FlateDecode and ASCIIHexDecode in decoded streams. It validates types 0, 1, and 2 xref entries but the content bench currently skips compressed objects inside /ObjStm, complex font decoding, clipping geometry, transparency groups, and Form XObject recursion. Streams it cannot inspect are listed by object number and reason rather than passed over in silence. Content inspection is bounded by a per-stream byte ceiling, a token ceiling, a text-operation ceiling, and a paint-candidate ceiling, all shown in the choices list; when one is reached the section is labelled a partial reading. The renderer is the browser's local PDF viewer, so visual results can differ by browser. Structural results remain separate.

/Prev is read here as a direct integer. ISO 32000-1:2008 Table 15 says it "shall be an indirect reference", which is a defect in that edition: ISO 32000-2:2020 Table 15 corrects the same entry to "shall be a direct object", and real writers emit a direct integer. Both texts were read for this page. Encrypted files are refused rather than opened, so a document whose permissions merely forbid copying is still refused here even though ordinary extraction tools would read it.

It does not prove whether the PDF was edited, how many times a person saved it, who changed it, when a change occurred, that covered text was intended to be secret, that an apparent black bar is a failed redaction, that an unrecovered string is absent everywhere, or that a reconstructed revision is authentic. It does not certify safe redaction.