What this test checks
Checking metadata is not the same as redacting document content, and neither one is a guarantee of anonymity. This test looks at three independent layers of a PDF:
- Effective Info dictionary. The
/Title,/Author,/Producer, and similar keys in the file's current Info dictionary — the values a reader draws on when it displays document properties. Not every reader exposes all of these fields, so read this layer with a tool you control rather than trusting any single viewer. - XMP references. PDFs can carry an XMP metadata stream referenced from the document catalog. A tool can drop the reference while leaving the stream's bytes in the file — the document "no longer has" XMP, but the bytes are still on disk.
- Raw bytes. What a byte-level search of the file actually contains. Strings that survive here are recoverable with any hex editor, regardless of what the document structure references — but the search only means something when the strings you look for are values you know were in the original file.
A result at one layer says nothing about the other two. That is exactly why a download that looks clean is not verification.
The synthetic PDF test: setup, commands, and recorded results
The input file was built offline with Python and pypdf 6.21.4:
- One blank page, 100×100 points — no text, images, fonts, attachments, or signatures.
- Info dictionary set to
/Title QA-PDF-MARKER,/Author Synthetic QA(Producerpypdf). - An independent XMP metadata stream attached to the document catalog, containing the string
QA-XMP-MARKER. - Size: 700 bytes. SHA-256:
840f547312136d63f5c05dcad61f648e77ba5dcbfa7d00ef1eec250d8a48a208.
The cleaned output was the file the tested service returned for this input:
- Size: 954 bytes. SHA-256:
2a5c3fc98decc624a75aa8856edf436f898443dbd44efb53650e9d0959546997.
Both files are from our recorded run; neither the input nor the output is published for download on this page.
Offline verification (Python 3 with pypdf; nothing is uploaded anywhere):
from pathlib import Path
from pypdf import PdfReader
b = Path("tagged.cleaned.pdf").read_bytes()
doc = PdfReader(Path("tagged.cleaned.pdf"))
print(dict(doc.metadata)) # layer 1: effective Info
print("/Metadata" in doc.trailer["/Root"]) # layer 2: XMP reference
print("/Prev" in doc.trailer) # earlier xref revision referenced?
print(b.find(b"QA-XMP-MARKER"), b.find(b"Synthetic")) # layer 3: raw bytes
print(b.count(b"startxref"), b.count(b"%%EOF")) # revision-count clues
sha256sum tagged.pdf tagged.cleaned.pdf
Expected values for this exact pair are listed in the next section — that is our recorded result for this pair, not something a run on different files will or should reproduce. Note what these commands are: checks on specific layers of a specific file pair. The filenames in the commands (tagged.pdf, tagged.cleaned.pdf) are the paths from our run, and the search strings (QA-XMP-MARKER, Synthetic) are the markers we planted; when checking your own files, substitute your own paths and the values you know were in your original. The Info and XMP checks work on any PDF; the raw-byte search is residue evidence only when the strings you search for are values you know were in the original — here, markers we planted in the input before uploading it. On a file whose original values you do not know, a zero-hit search proves nothing.
Check effective Info and XMP references and raw bytes
Results for this test pair, by layer:
| Layer | Check | Result in the downloaded output |
|---|---|---|
| Effective Info | doc.metadata | Still contains /Title QA-PDF-MARKER, /Author Synthetic QA, /Producer pypdf — read from this output's current Info dictionary with pypdf; what any given reader displays depends on the reader, and not every reader exposes these fields |
| XMP reference | "/Metadata" in trailer["/Root"] | False — the catalog no longer references an XMP stream |
| Raw bytes | bytes.find(...) | QA-XMP-MARKER still present at offset 443; Synthetic present at offset 82 — both are values planted in the input, so each hit is direct evidence that this value survived |
| Revision history | "/Prev" in trailer, marker counts | True — the newest trailer references the earlier cross-reference section at offset 505; startxref and %%EOF each counted 2 |
So in this single sample: the output's current Info still carries the original Title and Author, the XMP reference was dropped, and the original XMP text remains in the file bytes. Three layers, three different answers — which is the point of checking all of them.
Why a clean-looking result may still contain old bytes
Marker counts are a clue, not a verdict. Two startxref and two %%EOF markers suggest that a revision was appended, but counts alone cannot establish whether an arbitrary file was rewritten from scratch or incrementally updated. In this sample the evidence goes further: the newest trailer carries a /Prev entry pointing at the earlier cross-reference section (offset 505), which is how PDF incremental updates chain revisions — so this specific output was produced by appending a revision rather than rewriting the file. And appending does not erase: dropping a reference does not remove the bytes it pointed to, which is why the original XMP text is still findable even though the catalog no longer references it.
Two boundaries on what any of this proves:
- A clean raw-byte search does not prove a PDF is residue-free. Objects can be compressed, stored in object streams, or encoded, so old values can survive in forms a plain string search will not find. "The string is gone" is a weaker statement than "the data is gone."
- A marker hit proves only that this value survived. A hit on a string you know was in the original is direct evidence that this value remains in the file. It is not, by itself, a statement about anything else in the file, about other values, or about any other tool's output.
In this test, the raw marker search did find the original XMP text — which is enough to show the bytes survived here, and simultaneously not enough to say anything about compressed or more complex documents.
Limits and a verification checklist
What this test does not cover — treat these as open questions, not as passed:
- One synthetic blank page only. Rich text, fonts, embedded images, attachments, bookmarks, and annotations were not present and therefore not tested.
- No signed or encrypted PDFs; no PDFs with complex revision histories beyond what this pair shows.
- No compressed object streams, no cross-reference streams, no unusual encodings.
- No statement about the tested service's handling of any other file, its server-side storage behavior, or how long it keeps uploads. Nothing here tests deletion on anyone's servers.
- Verified offline on a desktop environment on 2026-11-04; no claim of continuous or ongoing monitoring.
Checklist for a "cleaned" PDF — run these offline, before you share the file:
- Open the output in a PDF library you control and print the current Info dictionary. If a value you wanted gone is still there, the removal did not take effect for that layer. What a reader displays varies, and not every reader exposes these fields.
- Check whether the document catalog still references an XMP metadata stream — and remember a
Falsehere says nothing about the bytes. - Search the raw bytes for specific old values — a name, a project code, a date string you planted or otherwise know was in the original. A hit is a definite fail for that value; a miss is not a pass, because compressed encodings can hide data.
- Inspect the revision structure before drawing conclusions: count
startxrefand%%EOFoccurrences as a first clue, then check whether the newest trailer references an earlier cross-reference section ("/Prev" in doc.trailerwith pypdf). Counts alone do not settle rewrite-versus-incremental. - If your PDF is signed, encrypted, or has a long edit history and the stakes are high, use a dedicated forensic workflow; this checklist is a first-line check, not a guarantee.
Follow the verification checklist above before treating any cleaned PDF as ready to share. If you want background on what file metadata is and why images and documents carry it, see our image-focused overview: What is image metadata. For checking metadata in images (not PDFs), see the Privacy Analyzer — it inspects image files only and does not process PDFs.