think
16px
820px

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 cleandegraded 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).