"""Text color strategies grounded in the typographic-attack literature. Best stealth/legibility trade-off: color the text as the local background mean shifted by +/-20 brightness (direction chosen by local luminance). Near-bg presets (5,5,5) on dark / (250,250,250) on light are the extreme-low-contrast alternative. """ from __future__ import annotations import numpy as np from PIL import Image from veil_pgd.types import RenderSpec def _region_mean_rgb(image: Image.Image, box: tuple[int, int, int, int]) -> np.ndarray: crop = np.asarray(image.convert("RGB").crop(box), dtype=np.float32).reshape(-1, 3) if crop.size == 0: return np.array([128.0, 128.0, 128.0]) return crop.mean(axis=0) def resolve_color( spec: RenderSpec, image: Image.Image, box: tuple[int, int, int, int] ) -> tuple[int, int, int, int]: """Return an RGBA fill for the given spec + placement box.""" alpha = int(round(255 * max(0.0, min(1.0, spec.alpha)))) if spec.color_strategy == "fixed": r, g, b = spec.fixed_rgb return (int(r), int(g), int(b), alpha) mean = _region_mean_rgb(image, box) luminance = float(0.299 * mean[0] + 0.587 * mean[1] + 0.114 * mean[2]) if spec.color_strategy == "near_bg_dark": return (5, 5, 5, alpha) if spec.color_strategy == "near_bg_light": return (250, 250, 250, alpha) # bg_mean_offset: shift away from the background so the VLM can still read it, # but only by a small brightness offset so a human barely notices. direction = -1 if luminance > 127 else 1 rgb = np.clip(mean + direction * spec.brightness_offset, 0, 255).astype(int) return (int(rgb[0]), int(rgb[1]), int(rgb[2]), alpha)