| """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) |
|
|
| |
| |
| 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) |
|
|