Steganography research — decoding a screenshot of one paragraph
Question: which stego carrier can recover a forensic payload (our 64‑bit keyed‑MAC issuance id)
from a screenshot of a single paragraph, while staying hidden in the document itself?
All families were measured apples‑to‑apples through one screenshot‑channel harness
(research/harness.py) on the same fixtures (a synthetic justified paragraph + a real KAK‑document
paragraph, rendered ~150 DPI). Channel operating points (a screenshot, not a photo — faithful raster
+ antialias/zoom/JPEG, no lens/perspective):
| OP | model |
|---|---|
pristine |
raw render (upper bound) |
clean |
antialias blur 0.6 + dither noise — a faithful PNG screenshot |
typical |
0.85× zoom + JPEG q85 + blur — a normal screenshot |
degraded |
0.6× downscale + JPEG q60 + blur — photo‑grade, arguably out of scope |
Why our current engine can't do it (the root cause, restated)
Our whole engine is geometric text stego — the signal is sub‑pixel position (0.4–0.6 pt ≈ 1 px
baseline/kerning shifts). A screenshot is a sampling+photometric channel: it destroys sub‑pixel
position but preserves intensity and multi‑pixel structure. So per‑unit SNR ≈ 1, and one
paragraph lacks the redundancy to average it out. Match the carrier to the channel.
Measured carrier survival through the channel (measurement noise of each carrier):
| carrier | signal budget (invisible) | noise clean → degraded |
verdict |
|---|---|---|---|
| baseline shift (v1/v2/v3, ours) | ~1 px (sub‑pixel) | ~1 px | SNR≈1 — the wall |
| word‑gap width | ~3 px (hides in justification) | 0.4 px → 1.2 px | SNR 3–7 — strong |
| luminance mid‑freq DCT | a few gray levels | destroyed by blur | ~50% BER — dead (naive) |
Results by family
Family 3 — word‑gap (inter‑word spacing) coding ★ WINNER (buildable/tested)
Encode bits in each line's inter‑word gaps, differentially against the line's mean gap; line width
preserved so it stays justified. Signal is multi‑pixel (robust) and hides in justification's
natural spacing variation. Reuses our whole decode stack (whitening, per‑line stride mapping,
MAC‑gated offset search) + Reed‑Solomon RS(15,8) for ECC.
End‑to‑end MAC‑gated decode of the full 64‑bit forensic payload (δ=1.4 pt, 12 seqs):
| OP | 12 lines | 14 lines | 16 lines | 20 lines |
|---|---|---|---|---|
| clean | 8/12 | 11/12 | 12/12 | 12/12 |
| typical | 5/12 | 9/12 | 12/12 | 12/12 |
| degraded | 0 | 1/12 | 2/12 | 5/12 |
- Reliable full‑payload decode from ~16 lines on realistic (clean/typical) screenshots.
- Amplitude sweet spot δ=1.4 pt (0.8 pt is only ~1/10 robust; 2.0 pt no better, more visible).
- Invisibility: mild. At δ=1.4 pt the word spacing is slightly uneven — reads like imperfect
justification, subtler than a visible watermark but noticeable on careful inspection. - Constraints: justified text only (ragged/left‑aligned has no spacing slack); ~14–16 line floor.
Family 4 — linguistic / canary fingerprint ★ most robust
Encode identity in content choices (synonyms, punctuation, whitespace) decoded by OCR.
OCR (tesseract ind+eng) read the paragraph at 99.9% char accuracy through every OP, including
degraded (823/823 chars clean/typical; one typo degraded). So content‑based bits survive any
legible screenshot. Cost: it alters the document text (often unacceptable for legal/official
docs) and has low bits‑per‑site → needs several sites; collusion (2 copies) localizes it.
Family 1 — luminance / frequency spread‑spectrum
Naive block‑DCT QIM (mid‑freq coefficient) gave ~50% BER through any channel — mild blur wipes the
mid‑frequency coefficient. A proper spread‑spectrum codec (energy across low‑freq + sync template +
ECC, à la Cox 1997 / screen‑shooting watermarks) is far more robust but is a from‑scratch image
codec, and it adds a faint pattern layer (less "in‑the‑text").
Family 2 — deep‑learning screen‑shooting watermark (StegaStamp‑class) ★ highest ceiling
The SOTA for "recover bits from a photo/screenshot of a page" (StegaStamp, CVPR 2020: ~100 bits from a
photo of a screen; HiDDeN; Fang et al. screen‑shooting, TIFS 2019). This is the only family whose
bit‑density could decode a literal single small paragraph invisibly. But: no torch/GPU in this
environment → cannot train in‑session; it needs a trained model + an ONNX decoder sidecar. A real
project, not a session prototype.
Family 5 — glyph‑shape perturbation (FontCode, SIGGRAPH 2018)
Perturb glyph outlines on a learned manifold; in‑text and elegant, but needs a custom embedded font +
manifold, and is fragile to heavy downscaling. Heavy build; not tested.
Ranking & recommendation
| family | screenshot‑robust | invisible | min excerpt (full payload) | build cost | in‑text | doc constraint |
|---|---|---|---|---|---|---|
| word‑gap (3) | clean/typical ✓, degraded ✗ | mild | ~16 lines | light | yes | justified only |
| linguistic (4) | ✓✓ (any legible) | n/a — alters text | few sites | medium | no (content) | any |
| DL / StegaStamp (2) | ✓✓ (photos too) | ✓✓ | ~1 small paragraph | very heavy | no (image) | any |
| luminance‑SS (1) | proper build only | tunable | untested | heavy | no | any |
| FontCode (5) | moderate | good | untested | heavy | yes (font) | custom font |
The information‑theory floor: a sound forensic payload is ~64 MAC‑gated bits; with channel
redundancy that needs ~64–120 recovered carriers. One 6‑line paragraph has ~45 word‑gaps — below the
floor for the full payload, for any carrier. So "single short paragraph, full CONFIRMED payload,
invisibly" is not attainable with an in‑text carrier; it is the regime where only DL's bit‑density
helps.
Recommendation:
1. Build Family 3 (word‑gap) as the new blind/excerpt Traceability channel — it is the best
tested, buildable, in‑text option: reliable from ~16 lines on real screenshots, reuses our stack,
and degrades to our existing channels on non‑justified docs (adaptive selection).
2. Expose a tier knob (the payload size ↔ excerpt‑size tradeoff): full 64‑bit id ⇒ ~16 lines
(CONFIRMED); a compact ~40‑bit id ⇒ ~10 lines (LIKELY, weaker MAC) — mirrors our existing tiers.
3. If a literal single short paragraph must decode, that is a separate DL project (Family 2):
train a StegaStamp‑class model on the screenshot channel + ship an ONNX decoder sidecar. Highest
ceiling, heaviest lift, and an image‑layer (not in‑text) mark.
Prototype code: research/harness.py, research/wordgap_codec.py (RS‑coded, end‑to‑end).