Two Documents in One File
A PDF is not one document. It's a picture for you, and a transcript for everything downstream of you.
Nothing in the format requires the two to agree.
Two documents in one file
Think of a stage play.
The audience sees the performance. The script carries stage directions nobody in the seats will ever hear.
Underneath the pixels, a PDF is a list of drawing instructions: set this font, move to these coordinates, draw this string in this ink. A viewer executes them. A text extractor skips the execution entirely and reads the instructions.
BT % begin text
/F1 11 Tf % font, 11pt
1 0 0 1 72 700 Tm % place at x=72, y=700
(Total 12,400.00) Tj % draw this string
ET
Change the ink to the page color, or the size to near zero, or the coordinates to somewhere off the sheet. The Tj is still there. The string is still there.
Same bytes, two readings, and neither program is malfunctioning. The extractor isn't being tricked. It's doing its job perfectly.
That gap in the opening figure, the two entries on the right with nothing beside them on the left? That's this. One file, read twice.
And it isn't a PDF quirk. Hidden columns in a spreadsheet. Speaker notes in a deck. display: none in an HTML email. Alt text. Tracked changes. Revision history.
Anywhere a format separates what it stores from what it shows, there's a seam.
Five ways to hide a line
None of these require a broken file.
- Matched ink. Set the fill to the page colour. The glyphs are drawn; they just have no contrast.
- Zero-size glyphs. A font size near zero takes up no visible room and extracts at full length.
- Off-page placement. Text positioned outside the crop box, which is the region the viewer is told to display. The extractor was never told about the crop box.
- Occlusion. Draw the text, then paint a filled rectangle over it. Painter's order hides the pixels; the operator is still in the stream.
- Never-rendered fields. Metadata, annotation bodies, form values, bookmarks, alt text. Not part of the page at all, and routinely extracted anyway.
The last one is the one people forget.
You can rasterise a page, inspect every pixel with your own eyes, and still miss text that was never on the page to begin with.
Characters with no width
Now shrink the problem to a single character.
A zero-width space is a real codepoint, U+200B, that occupies no horizontal room. So is the zero-width joiner, the byte order mark, the soft hyphen. All legitimate typography: they control line breaking and ligature behaviour in scripts that need it.
They also survive copy, paste, diffs and code review. There is nothing to see.
Then there's the Tags block. U+E0000 through U+E007F is a deprecated range that mirrors ASCII one for one and renders as nothing in essentially every font. A full paragraph fits inside it, in a filename, a document title, a chat message.
The edge, and it matters: whether those codepoints reach a model at all depends on the tokeniser and on whatever normalisation ran upstream. Some pipelines strip them cleanly. Plenty never look.
Your eyes are not the filter. Neither is your terminal. A character count is.
The human audits the wrong artifact
Here is the shape of the problem, and it has nothing to do with PDFs.
A person approves a document by looking at it.
A system ingests that same document by extracting it, then chunks it, indexes it, retrieves it much later, and pastes it into a prompt beside instructions that genuinely are instructions.
The review happened at stage one, on the rendered artifact. The payload entered at stage two, in the extracted one.
And the gap can be enormous. A file indexed once gets retrieved a year on, by a system nobody remembers wiring up, for a question nobody anticipated.
Nobody lied. The two of them looked at different documents.
Nothing in a prompt is privileged
You picture a prompt as having compartments. System rules up here. Retrieved documents in the middle. The user's question at the bottom.
By the time it reaches the model, it's one sequence of tokens.
The boundaries are a convention: markup, a chat template, a role label. Real conventions, and models are trained to respect them. But they are formatting, not memory protection. There is no ring 0.
Which is why a line sitting quietly inside a retrieved invoice can read as a directive. It arrived in the same sequence as everything else, and it looks like everything else.
Whether this is solvable at the model layer at all, or has to be contained at the system layer, is an open argument. Nobody has a clean answer yet.
Read what the human read
Three cuts. None of them complete.
Normalising invisible codepoints is cheap and worth doing on day one. It does nothing about matched ink, because matched ink is made of ordinary letters.
Rasterising the page and running OCR gets you the closest thing to what the human saw. It costs a render pass, mangles tables, and converts OCR errors into data errors.
Treating retrieved text as evidence and never as command is the only one that scales. It's also a policy rather than a mechanism. It holds exactly as far as it's enforced downstream, and not one step further.
The cheap habit that catches a surprising amount: compare the extracted character count against what the rendered page could plausibly hold. A page showing a few hundred characters that extracts a few thousand is telling you something.
Every document your system ingests is two documents.
You reviewed one of them.
The question was never whether the file looks clean. It's which copy you were looking at.