veil-pgd / src /veil_pgd /render /colors.py
Klaus Clawd
Initial public release: VEIL-PGD v0.1
c793f45
Raw
History Blame Contribute Delete
1.71 kB
"""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)