Skip to content

ERL watermark v2 — per-document coastline watermark on generated documents (design + prototype)

Why (owner, 2026-07-04 → 2026-07-10)

The owner produced (with another agent) an approved watermark master — THE_ESTABLISHERS_watermark_master.py + RampartOne-Regular.ttfdesignA3_watermark_a4.pdf (A4, 600 dpi): pale-blue "THE ESTABLISHERS ·" microprint flowing in waves, a central Rampart-One "E." lens, wavy corner fades, vignettes. Wanted on ERL-issued documents the app generates. Key owner decisions (verbatim):

"I assume that there should be a seed number … I'm wondering if that seed should be a value kept on GCP Secret or Vercel, so that the seed could be provided at render time so to achieve true anti-copy."

"I'm not specifically keen on rendering the document reference number into the microprint as it sort of breaks the design of it, I was simply thinking that if the watermark has a 'unique' set of coastlines, then maybe we could use it to verify whether it is genuine or not[e]." (→ per-document coastlines KEPT; microprint text stays the pure brand string — no document number woven in.)

"Let's do v2 then" (v2 = runtime-derived watermark, per-document unique) · "Let's forget about moving services to the NAS" · "please confirm with me whether the company chops are stored on the NAS. Cos if so, the watermark master should also be there"

(earlier) "I'm very concerned that the microprint would be blurred if our document rendering dpi is too low, while I'm concerned that a document over 3MB would be already too large"

Verified facts (measured in this sandbox, headless Chromium = the app's real PDF engine)

  • Master structure: deterministic except np.random.default_rng(21) which feeds ONLY the three corner- fade coastline phases. Microtext/lens/vignettes are seed-independent formulas. So "per-document unique" = per-document coastline phases; the rest of the design is static. Seed 21 itself is a design constant, not a secret — the secret in v2 is the HMAC key that derives per-document phases.
  • Anti-copy model: hiding a static seed adds nothing (the rendered watermark ships inside every distributed PDF and is trivially extractable). Per-document phases via seed = HMAC(secret, docRef) give verifiability: ERL can regenerate the expected coastlines for any claimed document and compare; a watermark lifted from doc A onto forged doc B carries provably-wrong coastlines. Verification is ERL-only (key holder), which the owner accepted.
  • Raster size/sharpness (through Chromium print-to-PDF): 300 dpi JPEG q80 → ~1.5 MB final PDF, microtext crisp; 200 dpi acceptable (~0.6 MB); 150 dpi soft. Chromium dedupes an identical background image across pages (3-page ≈ 1-page size). WebP must not be used (Chromium re-encodes; 7× inflation).
  • Full-vector SVG port (prototype built, seed 21): faithful geometry; at 6× zoom crisper than the 600-dpi master. BUT: 4.5 MB per page (text-on-path = per-glyph positioning; ~123K glyphs × 2 tone layers), the text-as-shared-mask variant is worse (glyphs become outlines), pikepdf recompression ~nil, and a 3-page document with inline vector watermark hangs Chromium's print pipeline (>4 min) — full vector per page is unusable for multi-page docs in a Puppeteer pipeline with 60s budgets.
  • Chops custody (owner asked): chops are NOT on the NAS — they live in a private GCS bucket (name via WOPC_ASSETS_BUCKET env; public-access-prevention enforced; see lib/wopc/assets/secureAssets.server.ts + I-009 which describes the NAS-era outage of exactly this GCS read). Watermark-master custody proposal pending owner: same GCS bucket (adjacent to the chops), e.g. doc/watermark/master/.

Architecture (v2 — decided by the measurements above)

Layered watermark, composited in the same order as the master (its final ops are linear, so the split is mathematically identical):

  1. Static base layer (one-time, offline, from the master): microtext + "E." lens without the corner fades/vignettes — exported at 300 dpi JPEG q80 (~1.5 MB, embeds once per document, fast).
  2. Per-document overlay (runtime, tiny): the corner fades + vignettes + tip whiteouts as low-frequency masks/gradients whose coastline phases derive from HMAC(key, documentRef) — a few KB of SVG/PNG mask per document. Key lives in GCP Secret Manager; renders server-side only.
  3. Applied as page background on: WOPC/Payment Confirmations, project invoices, quotations (supplementary Payment pages need a cell-background audit first — their scheme cells paint opaque fills).

The full-vector port is archived as an option for special single-page uses only. Prototype comparison image delivered to the owner (master vs v2 @6×). The port script (gen_svg.py, exact-math derivation incl. the per-seed hook) currently exists only in the session sandbox — where it should permanently live is part of the custody decision below.

Open items (owner input)

  1. Look approval: the v2-derived layers must be eyeballed against the approved PDF before rollout (raster base + overlay = same math, but owner sign-off required).
  2. Master custody: owner's premise ("chops on NAS → master on NAS") doesn't hold — chops are in GCS. Proposal: upload THE_ESTABLISHERS_watermark_master.py + RampartOne-Regular.ttf + designA3_watermark_a4.pdf (+ the v2 port) to the same private bucket. Awaiting owner confirmation.
  3. Which surfaces first (suggested: WOPC + project invoices/quotations v1, supplementary pages after the cell-background audit).

Decision log

2026-07-10 — opened; analysis + measurements + prototype done

  • Attestation (Document Rendering Fine-Tuning): read AGENTS.md; scope-scanned the board — no existing watermark/document-security task. Opened as T-166 (README pointer said T-165 but T-165.md already exists per its own taken-comment; pointer corrected to T-167).
  • Source: Document Rendering Fine-Tuning · https://claude.ai/code/session_01A2tdTGEeYDMDVhrceFpJ8D
  • Proposed by: the owner (watermark master + three Q&A rounds). Approved by: the owner — "Let's do v2 then"; per-document coastlines kept per the quote above; NAS service-migration idea dropped by the owner in the same message.
  • What was done: master analyzed; DPI/size/pipeline measurements (table above); full-vector SVG port prototyped + compared (delivered to owner); multi-page vector hang discovered; layered architecture chosen; chop custody verified (GCS, not NAS).
  • Blast radius (when built): page-background layer on document renders only; no data model changes; one new secret (watermark HMAC key) in Secret Manager; a static asset (~1.5 MB) in the render path.
  • Commits (append-only): this commit (task opened; docs only).