A Deepfake Can Look Real. It Still Has to Be True.

Looking real is a pixel problem. Being true is everything else.

G
Greg T · Engineering· 17 min read·Oct 31, 2026

The file arrives as a JPEG. A phone screenshot of a bank statement, cropped to the page, slightly tilted. No producer string. No fonts. No revision history. No content stream. Just a flat rectangle of pixels.

A few months ago we wrote a long piece about what a PDF can't hide: the editor markers, the font tables, the incremental saves that keep a file's own edit history. All true. All gone here. This file has nothing left to hide, because whoever made it threw the file structure away.

When the file stops talking, the document still has to.

The wash: print, rescan, regenerate. The rollers strip how the file was made; the figures come through untouched.


Three ways to wash a document

Structural forensics has a blind spot every serious forger has found: it inspects the file's container, and the container is optional.

There are three common ways to throw it away.

  • Print and rescan. The edit happens in a PDF editor, the result goes through a printer and a scanner, and what comes out is an honest scan of a dishonest page. Every structural trace stays on the first computer.
  • Regenerate. Hand the page to an image model with "change the closing balance to 4,820.00". It returns a new image. Not an edited file: a new one, with no history at all.
  • Hop formats. Export to PDF, screenshot it, send the screenshot, let the receiving system wrap it back into a PDF. Each conversion launders a little more.

What comes out in the wash

The same forged statement, delivered four ways. The evidence about how the file was made disappears. What the document claims comes through untouched.

EvidenceEdited PDFPrint & rescanAI regeneratedScreenshot hop
How the file was made
Editor markers in the pagepresentgonegonegone
Fonts, revisions, object historypresentgonegonegone
Producer and metadatapresentscanner'sgoneconverter's
What the document claims
Balances, totals, running sumsintactintactintactintact
IBAN and RIB check digitsintactintactintactintact
Issuer-signed 2D-Doc sealintactintactredrawnintact
The issuer's visual identityintactintactintactintact
fig.1 · container evidence dies in the wash; claims survive it

A detector that only reads the file is stuck with two bad options when a scan arrives. To the file, every scan looks the same: a new producer, no fonts, one image per page, edited or not. So it either treats every scan as suspect, and analysts learn to ignore it, or waves every scan through, and forgers learn to print.

Either way, it's judging the scanner, not the edit.


What survives the wash

Here is the thing a forger can't launder: the claims.

A bank statement claims an opening balance, a list of transactions, a closing balance, and a statement period. It claims an IBAN whose check digits are computed from the rest of it. It claims to come from a specific bank, with that bank's header and layout. A French tax notice may carry a seal that the issuing administration signed with its private key.

Printing doesn't erase any of that. Passing the page through an image model doesn't erase it either: it just makes consistency the forger's problem. So the question flips. Not "how was this file made?" but "can this document be true?"

That shift forced us to change how we weigh evidence. Today, every one of our 92 signals belongs to one of four tiers, and the tier decides what a signal is allowed to do to a verdict.

Not all evidence is allowed to condemn

A signal's tier, not its count, decides what it can do to a verdict. Twenty odd counts still add up to "worth a look".

Contradiction
It can't be true
The document asserts two things that can't both hold, or the issuer's signature fails.
One condemns.
Direct trace
A tool left prints
An editor's marker in the page, text drawn over text, an editing tool in an image's own history.
Two condemn.
Circumstantial
Odd, not wrong
An unusual object count, a metadata date gap, a missing producer string.
Ranks the queue. Never condemns.
Trust
Positive proof
A matched issuer template, a seal that verifies.
Earns "trusted".
fig.2 · the four evidence tiers behind every verdict

Structural forensics lives mostly in the middle two tiers. Washing destroys both. Contradictions are the evidence that survives the wash, so that's where we dug.


Arithmetic, to the cent

A balance equation is the oldest check in the book: opening balance, plus credits, minus debits, equals closing balance. Everyone says they do it. The details decide whether it works or lies to you.

The tolerance. Our extraction pipeline reconciles balances with a €0.02 tolerance, because a model reading a noisy scan will occasionally drop a cent and we don't want to fail an honest statement over it. The fraud check uses €0.005. Two one-cent edits move a balance by exactly €0.02, and a forger who knows your tolerance knows how much they can steal per row.

More than one equation. A statement prints the same truth several times. The totals row has to equal the sum of the rows. The header summary has to equal the totals. An intermediate balance printed mid-table (a month-end "SOLDE AU 30/11" line) has to match the running total at that exact line. Change one transaction, and a forger now has four numbers to repair instead of one.

The forger fixed the closing balance. Three other equations still disagree.

A salary credit raised by €1,000, and the closing balance raised to match, so the classic balance check passes. The statement prints the same truth in three more places, and nobody fixed those.

Account statement · 01/10 to 31/10Credits: €1,780.00 · Debits: €977.45
Date · labelDebitCreditBalance
01/10 · Opening balance2,104.30
02/10 · Salary, ACME SAS2,480.00edited
05/10 · Rent850.00
Balance at 15/102,734.30
18/10 · Card payments127.45
24/10 · Incoming transfer300.00
Totals977.451,780.00
31/10 · Closing balance3,906.85edited too
✓Opening + transactions = closing2,104.30 + 2,780.00 − 977.45 = 3,906.85
✗Totals row = sum of the rowscredits printed 1,780.00 ≠ 2,780.00
✗Header summary = the rows€1,780.00 ≠ 2,780.00
✗Mid-month balance = running total2,734.30 ≠ 3,734.30
fig.3 · synthetic statement; one edit, four equations

Dates and identifiers. Every transaction should sit inside the printed period. A period shouldn't end before it starts. An IBAN carries a MOD-97 checksum, and a French one also carries a RIB key. Change one digit and both break, unless the forger recomputes them.

Here is what happened when we forged born-digital statements in place (same font, clean save, no structural trace at all) and ran them with these checks off, then on:

Edits the file can't see, caught by the page

Each of these edits leaves the PDF structurally clean. Before the content checks, all four were caught 0 times.

Largest amount on page 1 changed41 / 47
before: 0 / 47
One transaction line removed40 / 47
before: 0 / 47
One row in the middle of the table changed34 / 47
before: 0 / 47
One digit of the IBAN changed30 / 43
before: 0 / 43
fig.4 · in-place forgeries of genuine statements, content checks withheld vs on

Zero before, because these edits were saved cleanly, in place, in the same font. The file had nothing to say about them. The page did.

The scars

Two things we learned the hard way.

Never trust a year you inferred. Many statements print "31.10" with no year, and every parser guesses it. On one bank's genuine statements, our parser put value dates exactly one year early whenever a transaction crossed a month boundary: 364 days off, on every sample. A date check that trusted that guess would have flagged every honest customer of that bank. A date only counts as evidence when the bank printed its year.

A coherence check is a claim about your reader as much as about the document. When we first ran the balance check across raw production statements, it fired on 19% of them. That wasn't a fraud wave. It was our parser misreading rows, typically finding a row without its amount. An arithmetic contradiction is only evidence if you are certain you read the numbers right. So the check runs only where we read the PDF's own text directly, on born-digital pages, with no OCR in between. And on its own it raises a warning, not a verdict.

You don't need a forger to fail a balance check. You just need a parser bug.


The text that says one thing and draws another

There's a catch in "read the PDF's own text directly". A forger who knows we trust born-digital text can skip the wash entirely and lie to the parser instead. Every extraction pipeline reads the text layer, ours and every LLM pipeline included, which makes the text layer itself an attack surface.

A PDF can attach a /ActualText replacement to a run of glyphs, drawing one figure on screen while feeding another to every text extractor. A font's /ToUnicode map can be rewired so the shape is a 7 and the copied text is a 1. The human underwriter sees one statement. The automated balance check validates another. Both pass, because they are reading two different documents.

One page, two documents

The glyphs and the text layer are separate things in a PDF. Nothing forces them to agree.

What the page draws
9,450.00
What the underwriter sees on screen and on paper.
What every extractor reads
1,450.00
What the balance check, the LLM and copy-paste all get.
BT /F1 11 Tf 412 318 Td
/Span << /ActualText (1,450.00) >> BDC
  (9,450.00) Tj            % the glyphs a human sees
EMC
ET
✗Each digit rendered alone, read back by OCRdrawn 9,450.00 ≠ text layer 1,450.00
fig.5 · a text-layer substitution, and the check that renders the glyphs

So we stopped trusting the text layer for numbers. We take each digit a font claims to draw, render it alone, read it back with OCR, and compare that with the text layer. In our benchmark this caught 101 of 101 /ActualText swaps, with zero firings across 208 genuine files.

We met a cruder version of the same idea in production, on a statement claiming to come from a major French bank. The forger didn't bother deleting the original transactions: they drew opaque white rectangles over the rows and typed new figures on top, 48,00 sitting directly over 162,00. At the time, our engine scored it normal. Today we check the drawing order for amounts buried under a later opaque fill with new text drawn over them, and it scores high risk. (The check needs both halves. A white fill alone fired on 13 genuine files, because some print drivers shade table cells after drawing the text.)

If your fraud check and your extractor read the same text layer, a forger only has to fool it once.


Cryptography survives the scanner

Every check so far asks whether the document agrees with itself. A careful forger can win that game. If they change a transaction and then carefully update the totals row, the header summary and the running balance, arithmetic alone won't catch them.

Some documents carry a check that no amount of care defeats. French administrations and utilities print a 2D-Doc: a Data Matrix barcode holding the document's key fields (name, address, tax amounts, reference period), signed with the issuer's private key under the ANTS standard.

The issuer's key: a 2D-Doc seal survives the printer, the scanner and the photocopier.

The seal has a property structural evidence never had: it survives the wash. A barcode prints and rescans perfectly well.

Checking a document against its issuer, not against itself

The seal is read from the pixels, so it works on a scan, a photo or a print of a print.

FindLocate the Data Matrix on the page, at scan resolution.
DecodeRead the signed fields: name, address, amounts, period.
VerifyCheck the ECDSA signature against the issuer's certificate.
CompareMatch the sealed fields against the text printed on the page.
criticalThe signature failsThe sealed fields were altered.
highValid seal, different pageThe visible text was edited and the original barcode left alone.
highNothing decodesA barcode font drew a picture of a seal.
fig.6 · 2D-Doc verification, and the three ways it condemns

That last case came from a cluster of tax notices built with a desktop-publishing kit. The barcode looked right. Someone drew a picture of a seal, hoping nobody would scan it.

The strongest coherence check isn't the document agreeing with itself. It's the document agreeing with its issuer's key.


Does it look like who it claims to be?

The cheapest forgery doesn't touch the numbers. It takes a genuine statement from a small bank and pastes a big bank's header on top.

The numbers are honest. Only the letterhead lies. So we compare the page, patch by patch, against verified reference pages for each bank family and ask which one it actually resembles. When the bank it names and the bank it looks like disagree by a clear margin, we flag the mismatch.

On a held-out test set, this caught 75% of header-swap fakes while firing on just 1 of 213 genuine documents. That one false positive is instructive: a file whose first page was the bank's back page, which looks like nobody's header.


And yes, the pixels

None of this means we ignore the image. When a document is regenerated by an image model, the pixels are the only place the model leaves a signature.

Generic AI-image detectors are trained on photos, and documents aren't photos. We tested one on AI-regenerated document pages, at a threshold that flags only 1% of genuine documents (measured on about 825 genuine pages per channel, because a noisy detector is one analysts learn to switch off). It caught 20%. Fine-tuned on document pages, the same architecture caught 90% after our upload conversion, 85% after a trip through WhatsApp, and 95% on pages from an image model it never saw in training.

Here is the catch, and why pixel detection can't stand alone: when a forger cuts out a small patch, regenerates only that, and pastes it back, detection drops to nearly zero. Between 0% and 4%, even at native resolution.

Two layers, two blind spots, no overlap

Share of AI-regenerated pages caught, at a threshold that flags 1% of genuine pages.

Generic AI-image detector20%
Fine-tuned on documents, after upload conversion90%
Same, after a trip through WhatsApp85%
Same, on an image model it never saw95%
Small patch regenerated and pasted back0 to 4%
Pixel detector
Arithmetic, checksums, seal
Whole page regenerated
85 to 95% caught
Every redrawn figure still has to reconcile
Small patch pasted back
0 to 4%
The edited figure still has to reconcile
fig.7 · where the pixel layer stops, the content layer is already waiting

That's where the layers stop being a checklist and start covering for each other. An image model asked to change one number on a dense page tends to redraw all of it: digits elsewhere shift, words come back slightly garbled. The pixel detector sees the re-render. And a careful forger who regenerates only a small patch to slip past it walks straight into the balance equation, the IBAN checksum and the cryptographic seal.

The patch: regenerate one small window and paste it back. The image looks clean; the totals still have to agree.

Two smaller habits matter here too. We read every image upload before converting it to PDF, because conversion erases the JPEG history, the camera's embedded thumbnail and any provenance claim. Some generators label their own output as AI-made in the file's metadata, and nobody forges that label against themselves. Stripped copies won't carry it, so its absence means nothing. Its presence means a lot.


Not checked is not passed

The single most important rule in our fraud engine is boring: when a check can't run, it says so.

Most scans have no parser that reads them, so the arithmetic can't run. Many documents match no reference template, so the template comparisons can't run. A green dot next to a check means "checked, nothing found". Showing that dot for a check that never ran tells the reviewer a scan was verified when it wasn't even read.

A hollow dot is not a green one

How an analysis reports a scan from a bank our parsers don't read yet.

Editor markers in the pagePassed
IBAN check digitsPassed
Text layer vs drawn glyphsPassed
Balance equationno parser reads this bank's layoutNot checked
Font set vs issuer templateno matching reference templateNot checked
fig.8 · abstentions are shown, with their reason

We learned this the hard way. In production, the exact same file once scored 98.75 (high risk), and three hours later 0.0 (trusted). On the first run, the visual template match came back empty. Instead of abstaining, six checks fell back to a generic bank-statement baseline and flagged an ordinary document as wildly abnormal. Now those checks abstain, the analysis says "not checked" with the reason, and a missing comparison can cost a document its trusted badge but can never condemn an applicant.


Two numbers, not one

A fraud detector has two ways to fail. It can flag honest people, and it can miss forgeries. Most published accuracy figures measure one of them, on a set nobody else can see.

We keep both, and measure both against every change.

Precision, on what customers actually upload. Before any signal change ships, we replay it against stored production analyses. When we tightened our verdict rules to require contradictions instead of piled-up anomalies, the high-risk pile on 224 customer documents from a 30-day window went from 90 to 9, and none of the forgeries we had confirmed by hand left it.

Recall, on forgeries we make ourselves. Nobody hands you a labelled set of the fraud you missed, so we build our own: genuine documents, forged dozens of ways, every fake run through the real pipeline. On 2,080 forgeries, adding the content checks above took the catch rate from 35.4% to 63.6%. Before them, forgeries whose only flaw was that the figures disagreed were caught 5% of the time.

Both directions, measured before every change

Precision · real customer traffic
90 → 9
high-risk flags on 224 customer documents, with every hand-confirmed forgery still flagged
Recall · 2,080 forgeries we made
35.4% → 63.6%
caught, after adding the content checks in this article
fig.9 · precision is replayed on production, recall is benchmarked on forgeries

63.6% means about a third still gets through. We publish that number because the alternative is publishing a better-looking one we couldn't defend. The benchmark also tells us exactly which third: an edited account holder name (0 of 48), statements no parser reads, a careful edit that repairs every total. That list is the roadmap.


Where this leaves us

At Holofin, document fraud is one of the problems we're paid to be paranoid about, and the work above follows a few rules we didn't start with:

  • A verdict rests on a contradiction. Counts and anomalies rank the queue. Only a document that cannot be true gets condemned.
  • Every comparison needs a reference, or it abstains. The issuer's matched template, or what this PDF producer normally emits. Otherwise, "not checked".
  • Read the claim, not just the container. Figures to the cent, dates the bank actually printed, checksums, seals against the issuer's key, the issuer's look.
  • Never trust the text layer for a figure. What a page draws and what it says can differ, on purpose.
  • Measure both directions. Precision on real traffic, recall on forgeries, before every change.

In practice, that's 92 signals across file structure, typography, metadata, pixels, cryptographic seals and financial arithmetic, each able to point at the page, the region and, when it can, the field that failed.

A forger can wash away how a document was made, and make what's left look real. What it says still has to be true: to the cent, against the issuer's key, in the issuer's own look. That part doesn't come out in the wash.

Related Articles

Holofin