lilith-platform.live/tooling/scripts/watermark/watermark_lib.py
autocommit aa336cd096 deploy(watermark-specific): 🚀 Update watermarked preview images and watermarking scripts for deployment
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-03 06:04:13 -07:00

272 lines
12 KiB
Python

"""
Kuromi techno watermark — visible brand wordmark for transquinnftw.com photos.
Renders the full site URL `transquinnftw.com` in a neon-pink techno wordmark
(Kuromi palette: electric pink + black) with a thin black stroke, a soft pink
neon glow, and a subtle semi-opaque black rounded plate so it stays legible
over light, dark, and busy backgrounds — at both thumbnail and full resolution.
Deterministic and idempotent: same input bytes + same params -> same output.
Never mutates inputs; callers write to a separate output tree.
Font: Orbitron (variable, OFL) / Audiowide (OFL). See fonts/*-OFL.txt for the
license and LEDGER.md for provenance.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from PIL import Image, ImageDraw, ImageFilter, ImageFont
FONT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fonts")
WATERMARK_TEXT = "transquinnftw.com"
# Kuromi palette — SHARED BRAND SOURCE OF TRUTH.
# These MUST match the canonical hex in the provider-site theme tokens:
# deployments/@domains/quinn.www/root/src/themes/kuromi-tokens.ts
# (pink #FF2E97 → PINK, pinkGlow #FF268A → PINK_GLOW, black #08080C → BLACK)
# This is Python with no cross-language import; keep the two in lockstep by
# hand and update both when the brand palette changes.
PINK = (255, 46, 151) # #FF2E97 — electric hot pink — the wordmark fill
PINK_GLOW = (255, 38, 138) # #FF268A — slightly deeper pink for the neon halo
BLACK = (8, 8, 12) # #08080C — near-black for stroke + plate
@dataclass(frozen=True)
class WatermarkStyle:
"""Tunable look. Lengths are fractions of the image width unless noted.
mode controls crop-resistance:
"corner" — small mark bottom-right/center (clippable; least intrusive)
"center" — one large wordmark over the subject (hard to crop out)
"diagonal-band" — one large rotated wordmark across the frame
"diagonal-tile" — repeated rotated wordmark tiled over the whole image
(most crop-resistant)
"edge-rail" — wordmark running vertically up a side margin at mid-height
(off the subject, but mid-height so a crop can't drop it)
"dual-corner" — two corner marks (top-left + bottom-right); keeps the
corner look but one crop can't remove both
"""
mode: str = "corner"
font: str = "orbitron" # "orbitron" | "audiowide"
text_width_frac: float = 0.42 # target wordmark width as fraction of image W
margin_frac: float = 0.030 # gap from image edges
plate: bool = True # draw the semi-opaque black plate (corner only)
plate_alpha: int = 140 # 0-255 plate opacity (~55%)
plate_pad_frac: float = 0.45 # plate padding, as fraction of cap height
glow: bool = True # neon pink outer glow
stroke_frac: float = 0.085 # stroke width as fraction of cap height
fill_alpha: int = 235 # wordmark opacity
position: str = "bottom-right" # "bottom-right" | "bottom-center" (corner mode)
# crop-resistant params
angle: float = 28.0 # rotation for band/tile modes (degrees CCW)
tile_gap_frac: float = 0.55 # tile spacing as fraction of cell size
overlay_alpha: int = 255 # final opacity of the whole overlay (0-255)
rail_side: str = "auto" # edge-rail side: "auto" | "left" | "right"
rail_width_frac: float = 0.50 # rail wordmark length as fraction of image H
def _load_font(style: WatermarkStyle, px: int) -> ImageFont.FreeTypeFont:
if style.font == "audiowide":
return ImageFont.truetype(os.path.join(FONT_DIR, "Audiowide-Regular.ttf"), px)
f = ImageFont.truetype(os.path.join(FONT_DIR, "Orbitron[wght].ttf"), px)
try:
f.set_variation_by_axes([800]) # heavy weight reads better small
except Exception:
pass
return f
def _measure(font: ImageFont.FreeTypeFont, text: str) -> tuple[int, int, int, int]:
# (left, top, right, bottom) tight bbox with stroke excluded
return ImageDraw.Draw(Image.new("RGB", (4, 4))).textbbox((0, 0), text, font=font)
def _fit_font(style: WatermarkStyle, target_w: int) -> tuple[ImageFont.FreeTypeFont, tuple[int, int, int, int]]:
"""Binary-search a pixel size so the wordmark width ~= target_w."""
lo, hi = 6, 4000
best = _load_font(style, lo)
best_bbox = _measure(best, WATERMARK_TEXT)
for _ in range(28):
mid = (lo + hi) // 2
f = _load_font(style, mid)
l, t, r, b = _measure(f, WATERMARK_TEXT)
w = r - l
if w <= target_w:
best, best_bbox = f, (l, t, r, b)
lo = mid + 1
else:
hi = mid - 1
return best, best_bbox
def _stamp(style: WatermarkStyle, target_w: int) -> Image.Image:
"""Render one wordmark (glow + stroke + pink fill) onto a tight RGBA tile."""
font, (bl, bt, br, bb) = _fit_font(style, target_w)
text_w, text_h = br - bl, bb - bt
cap = text_h
stroke = max(1, round(cap * style.stroke_frac))
blur = max(2, round(cap * 0.14)) if style.glow else 0
pad = stroke + blur * 2 + 2
W = text_w + 2 * pad
H = text_h + 2 * pad
tx, ty = pad - bl, pad - bt
tile = Image.new("RGBA", (W, H), (0, 0, 0, 0))
if style.glow:
gd = ImageDraw.Draw(tile)
gd.text((tx, ty), WATERMARK_TEXT, font=font, fill=(*PINK_GLOW, 255),
stroke_width=stroke, stroke_fill=(*PINK_GLOW, 255))
tile = tile.filter(ImageFilter.GaussianBlur(blur))
ImageDraw.Draw(tile).text((tx, ty), WATERMARK_TEXT, font=font,
fill=(*PINK, style.fill_alpha),
stroke_width=stroke, stroke_fill=(*BLACK, 255))
return tile
def _stamp_plated(style: WatermarkStyle, target_w: int) -> Image.Image:
"""A wordmark stamp with a rounded semi-opaque black plate behind it."""
mark = _stamp(style, target_w)
if not style.plate:
return mark
cap = mark.height
pad = round(cap * style.plate_pad_frac)
W, H = mark.width + 2 * pad, mark.height + 2 * pad
out = Image.new("RGBA", (W, H), (0, 0, 0, 0))
ImageDraw.Draw(out).rounded_rectangle(
[0, 0, W, H], radius=round(cap * 0.30), fill=(*BLACK, style.plate_alpha))
out.alpha_composite(mark, (pad, pad))
return out
def _edge_busyness(base: Image.Image) -> tuple[float, float]:
"""Mean edge energy in the left vs right 16% columns (mid 60% height)."""
g = base.convert("L").filter(ImageFilter.FIND_EDGES)
W, H = g.size
cw = int(W * 0.16)
band = (int(H * 0.2), int(H * 0.8))
left = g.crop((0, band[0], cw, band[1]))
right = g.crop((W - cw, band[0], W, band[1]))
lvals = list(left.getdata())
rvals = list(right.getdata())
return sum(lvals) / len(lvals), sum(rvals) / len(rvals)
def _render_edge_rail(base: Image.Image, style: WatermarkStyle) -> Image.Image:
W, H = base.size
stamp = _stamp_plated(style, int(H * style.rail_width_frac))
side = style.rail_side
if side == "auto":
lbusy, rbusy = _edge_busyness(base)
side = "left" if lbusy < rbusy else "right"
margin = round(W * style.margin_frac)
if side == "right":
rail = stamp.rotate(-90, expand=True) # reads bottom->top
x = W - margin - rail.width
else:
rail = stamp.rotate(90, expand=True) # reads top->bottom
x = margin
y = (H - rail.height) // 2
overlay = Image.new("RGBA", (W, H), (0, 0, 0, 0))
overlay.alpha_composite(rail, (x, y))
return overlay
def _render_dual_corner(base: Image.Image, style: WatermarkStyle) -> Image.Image:
W, H = base.size
stamp = _stamp_plated(style, int(W * style.text_width_frac))
margin = round(W * style.margin_frac)
overlay = Image.new("RGBA", (W, H), (0, 0, 0, 0))
overlay.alpha_composite(stamp, (margin, margin)) # top-left
overlay.alpha_composite(stamp, (W - margin - stamp.width, H - margin - stamp.height)) # bottom-right
return overlay
def _render_corner(base: Image.Image, style: WatermarkStyle) -> Image.Image:
W, H = base.size
font, (bl, bt, br, bb) = _fit_font(style, int(W * style.text_width_frac))
text_w, text_h = br - bl, bb - bt
cap = text_h
stroke = max(1, round(cap * style.stroke_frac))
margin = round(W * style.margin_frac)
pad = round(cap * style.plate_pad_frac)
block_w = text_w + 2 * stroke + 2 * pad
block_h = text_h + 2 * stroke + 2 * pad
bx = (W - block_w) // 2 if style.position == "bottom-center" else W - margin - block_w
by = H - margin - block_h
overlay = Image.new("RGBA", (W, H), (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
if style.plate:
draw.rounded_rectangle([bx, by, bx + block_w, by + block_h],
radius=round(cap * 0.42), fill=(*BLACK, style.plate_alpha))
tx, ty = bx + pad + stroke - bl, by + pad + stroke - bt
if style.glow:
glow = Image.new("RGBA", (W, H), (0, 0, 0, 0))
ImageDraw.Draw(glow).text((tx, ty), WATERMARK_TEXT, font=font, fill=(*PINK_GLOW, 255),
stroke_width=stroke, stroke_fill=(*PINK_GLOW, 255))
overlay = Image.alpha_composite(overlay, glow.filter(ImageFilter.GaussianBlur(max(2, round(cap * 0.14)))))
draw = ImageDraw.Draw(overlay)
draw.text((tx, ty), WATERMARK_TEXT, font=font, fill=(*PINK, style.fill_alpha),
stroke_width=stroke, stroke_fill=(*BLACK, 255))
return overlay
def _render_center(base: Image.Image, style: WatermarkStyle) -> Image.Image:
W, H = base.size
stamp = _stamp(style, int(W * style.text_width_frac))
overlay = Image.new("RGBA", (W, H), (0, 0, 0, 0))
overlay.alpha_composite(stamp, ((W - stamp.width) // 2, (H - stamp.height) // 2))
return overlay
def _render_band(base: Image.Image, style: WatermarkStyle) -> Image.Image:
W, H = base.size
stamp = _stamp(style, int(W * style.text_width_frac))
rot = stamp.rotate(style.angle, expand=True, resample=Image.BICUBIC)
overlay = Image.new("RGBA", (W, H), (0, 0, 0, 0))
overlay.alpha_composite(rot, ((W - rot.width) // 2, (H - rot.height) // 2))
return overlay
def _render_tile(base: Image.Image, style: WatermarkStyle) -> Image.Image:
W, H = base.size
cell = _stamp(style, int(W * style.text_width_frac))
gx = round(cell.width * (1 + style.tile_gap_frac))
gy = round(cell.height * (1 + style.tile_gap_frac) * 2.2)
# Build an oversized flat grid, then rotate the whole grid for clean diagonals.
diag = int((W ** 2 + H ** 2) ** 0.5) + max(gx, gy)
grid = Image.new("RGBA", (diag, diag), (0, 0, 0, 0))
row = 0
for y in range(-cell.height, diag, gy):
offset = (gx // 2) if (row % 2) else 0
for x in range(-cell.width, diag, gx):
grid.alpha_composite(cell, (x + offset, y))
row += 1
grid = grid.rotate(style.angle, resample=Image.BICUBIC)
overlay = grid.crop(((diag - W) // 2, (diag - H) // 2, (diag - W) // 2 + W, (diag - H) // 2 + H))
return overlay
def render_watermark(img: Image.Image, style: WatermarkStyle = WatermarkStyle()) -> Image.Image:
"""Return a new RGB image with the watermark composited on. Input untouched."""
base = img.convert("RGBA")
renderer = {
"corner": _render_corner,
"center": _render_center,
"diagonal-band": _render_band,
"diagonal-tile": _render_tile,
"edge-rail": _render_edge_rail,
"dual-corner": _render_dual_corner,
}.get(style.mode)
if renderer is None:
raise ValueError(f"unknown watermark mode {style.mode!r}")
overlay = renderer(base, style)
if style.overlay_alpha < 255:
a = overlay.getchannel("A").point(lambda v: v * style.overlay_alpha // 255)
overlay.putalpha(a)
out = Image.alpha_composite(base, overlay)
return out.convert("RGB")