The short version
Take a PDF containing one photograph. The photo was taken on a phone, so it carries an EXIF block: camera model, the photographer's name, and the latitude and longitude where the shutter fired. Put that PDF through a metadata remover. Then check it with ExifTool, which is what everyone checks with.
ExifTool reports nothing. The document properties are gone. The file looks clean by every measure a careful person would think to apply.
The coordinates are still there. They are inside the image, and ExifTool run against a PDF reports on the PDF, not on the images inside it. Extract the JPEG and the GPS tags come straight back out.
We have since run this against six other tools. None of them removed the GPS. None removed the embedded script. One of the six removed the attachment. See the measured results per tool →
What we planted, and what survived
Guessing at what a tool removes is not evidence, so the test starts from known values. The corpus generator builds seven PDFs with specific strings written into specific places — a camera model, a photographer name, GPS coordinates at a known latitude, a reviewer name on an annotation, an attachment, and a script. Then the verifier looks for exactly those strings.
Image EXIF and GPS
A photo placed in a PDF keeps the EXIF block it arrived with — camera make and model, the photographer name in the Artist tag, and GPS coordinates.
Why it survives: The data lives inside the image stream, not in the document. Deleting document properties does not touch it.
Annotation authorship
Every comment and sticky note carries /T, the commenter name, and /M, when they wrote it.
Why it survives: Annotations are page objects. A clean that rewrites the Info dictionary walks straight past them.
Embedded attachments
Files attached to the PDF, which viewers list in an attachments pane most people never open.
Why it survives: Unlinking the attachment list leaves the file itself in the document unless the objects are deleted too.
Embedded JavaScript
Scripts that run when the document opens.
Why it survives: Removing the /OpenAction reference does not remove the script object it pointed at.
Orphaned objects
Anything unlinked but not deleted. Still in the file, still readable, just no longer referenced.
Why it survives: Most PDF libraries serialize every object they hold, referenced or not.
Why checking your work does not help
The uncomfortable part of this is not that tools miss things. It is that the two obvious ways to verify a cleaned file both agree with the tool.
ExifTool on the PDF reads the document's own metadata. It does not descend into embedded image streams, so a photo full of GPS tags produces no output at all.
qpdf --qdf expands the file so you can read its structure. But qpdf garbage-collects unreferenced objects while it rewrites — so an object that was unlinked but never deleted disappears from the expanded output while remaining in the real file. The check removes the evidence.
Compressed object streams close the last gap: a surviving script or name packed into one is invisible to a plain text search of the bytes. Between the three, a leak can be invisible to every check short of writing a parser.
Lyonite failed this test
This test was written to check our own tool, and it failed. Until 13 August 2026 Lyonite's metadata remover cleaned document properties, XMP, the trailer ID and page-level entries — a more thorough job than most — and left image EXIF, annotation authorship, attachments and scripts entirely alone.
Building the verifier turned up a second problem in our own fix. Deleting the /OpenAction reference to a script removed the reference, not the script: the object stayed registered and was written into the output, unreferenced but fully readable. It took a third check — serializing every parsed object, orphans included — to see it, because the byte scan could not read into the compressed object stream and qpdf had collected the orphan before we grepped.
Worth saying plainly: we published this because our own tool was one of the ones getting it wrong. The corpus and verifier are the same ones we run against ourselves, and the results below include us.
What Lyonite does now
The remover strips EXIF, IPTC and comment segments from embedded JPEGs by walking the marker structure and copying everything it is not deliberately dropping — no decode, no re-encode, so the compressed image data comes out bit-identical and the picture is untouched. It removes annotation authorship and timestamps while keeping the annotations, deletes attachments and their streams rather than unlinking them, and sweeps the object graph for script objects.
Then it re-opens the finished file and reads it back. Claiming a field was removed because we called delete on it is not evidence; opening the downloaded copy and finding nothing is.
Two things it deliberately does not do. Images in encodings it cannot rewrite safely — a Flate-wrapped JPEG, JPEG 2000 — are reported as skipped rather than guessed at, because a corrupted file is worse than an uncleaned one. And the text of comments stays, because it is visible content: a sticky note reading "do not send to counterparty" is on the page, and a metadata tool that silently deleted page content would be a different and more dangerous product. The tool says so in its result rather than leaving you to find out.
Run it yourself
The whole point of publishing a result like this is that you should not have to take our word for it. The corpus generator and the verifier are in the repository, take no arguments, and need nothing installed beyond the project itself.
git clone https://github.com/yorkzap/pdf-leak-corpus cd pdf-leak-corpus && npm install node generate.mjs # writes corpus/*.pdf # put each corpus file through a tool, save the # output into results/<tool>/ under the same name node verify.mjs # prints the matrix
The corpus lives in its own repository rather than inside ours, so testing a tool does not mean cloning a web application. Its only dependency is pdf-lib.
The verifier reports per file and per leak class, and exits non-zero if anything planted is still present. If you test a tool we have not, the numbers are directly comparable to ours because the planted values are identical.
What this does not tell you
This is a test of metadata removal, not of anonymity. A PDF can identify its author through things no metadata tool touches: the writing itself, a signature image, a letterhead, the specific subset of fonts a particular installation embeds, or text sitting under a black rectangle that was drawn rather than applied as a redaction.
If your safety depends on a document not being traced to you, cleaning its metadata is one step in a longer process, and the last step should not be an automated one.
Check this yourself. If we got it wrong, tell us.
Every number here came from files you can download and a checker you can run, so you do not have to believe us — clone the corpus and get your own result.
If it disagrees with ours, or if you build one of the tools named here and we measured it unfairly, out of date, or with a setting you would not have used, send it to hello@lyonite.com.
We reply within 48 hours. If you are right, the page is corrected with the date on it and your correction credited, and the old number stays visible so the change is legible. If a tool has since been fixed, that is the update we most want to publish. Nothing here is worth defending past the point it stops being true.