#!/usr/bin/env python3 """ Generador de iconos .ico para apps C++ del registry. Toma SVG phosphor → renderiza con cairosvg → compone fondo redondeado + glyph blanco → exporta .ico multi-resolucion (16,24,32,48,64,128,256) en /appicon.ico. Mapping: APPS = [(app_id, dir, phosphor_icon, accent_hex)] """ import io import os import sys from pathlib import Path import cairosvg from PIL import Image, ImageDraw REGISTRY_ROOT = Path(__file__).resolve().parent.parent PHOSPHOR_FILL = REGISTRY_ROOT / "sources/phosphor-core/assets/fill" APPS = [ ("altsnap_jitter_test", "apps/altsnap_jitter_test", "arrows-clockwise", "#dc2626"), ("chart_demo", "apps/chart_demo", "chart-bar", "#0ea5e9"), ("dag_engine_ui", "apps/dag_engine_ui", "tree-structure", "#7c3aed"), ("data_factory", "apps/data_factory", "factory", "#f97316"), ("engine_smoke", "apps/engine_smoke", "game-controller", "#16a34a"), ("graph_explorer", "projects/osint_graph/apps/graph_explorer", "graph", "#0891b2"), ("navegator_dashboard", "projects/navegator/apps/navegator_dashboard", "compass", "#2563eb"), ("odr_console", "projects/online_data_recopilation/apps/odr_console", "terminal-window", "#52525b"), ("primitives_gallery", "apps/primitives_gallery", "shapes", "#db2777"), ("registry_dashboard", "projects/fn_monitoring/apps/registry_dashboard", "gauge", "#059669"), ("runtime_test", "apps/runtime_test", "flask", "#9333ea"), ("shaders_lab", "apps/shaders_lab", "palette", "#ea580c"), ("text_editor_smoke", "apps/text_editor_smoke", "note-pencil", "#0d9488"), ] ICON_SIZES = [16, 24, 32, 48, 64, 128, 256] RENDER_SIZE = 256 # canvas of reference, downscaled to each .ico size def hex_to_rgb(h: str) -> tuple[int, int, int]: h = h.lstrip("#") return tuple(int(h[i : i + 2], 16) for i in (0, 2, 4)) def render_glyph_white(svg_path: Path, size: int) -> Image.Image: """Render phosphor SVG as white-on-transparent at given size.""" svg = svg_path.read_text() # phosphor uses fill="currentColor". Force white. svg = svg.replace('fill="currentColor"', 'fill="#ffffff"') png_bytes = cairosvg.svg2png( bytestring=svg.encode("utf-8"), output_width=size, output_height=size, ) return Image.open(io.BytesIO(png_bytes)).convert("RGBA") def make_icon_image(svg_path: Path, accent_hex: str, size: int) -> Image.Image: """Compose: rounded-square accent background + centered white glyph.""" bg_color = hex_to_rgb(accent_hex) + (255,) canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(canvas) radius = max(2, size // 6) # ~16% rounded corners draw.rounded_rectangle( [(0, 0), (size - 1, size - 1)], radius=radius, fill=bg_color, ) # Glyph occupies inner ~70% (padding ~15% all around). glyph_size = int(size * 0.7) if glyph_size < 8: glyph_size = max(8, size - 2) glyph = render_glyph_white(svg_path, glyph_size) off = ((size - glyph_size) // 2, (size - glyph_size) // 2) canvas.alpha_composite(glyph, dest=off) return canvas def build_ico(app_id: str, app_dir: Path, phosphor_name: str, accent_hex: str) -> Path: svg_file = PHOSPHOR_FILL / f"{phosphor_name}-fill.svg" if not svg_file.exists(): raise FileNotFoundError(f"phosphor icon not found: {svg_file}") # Render the highest-quality image (256) and let Pillow downscale via `sizes`. # Using append_images with custom-rendered per-size variants preserves # crispness of the phosphor glyph at small sizes (16/24). images = {s: make_icon_image(svg_file, accent_hex, s) for s in ICON_SIZES} out = app_dir / "appicon.ico" out.parent.mkdir(parents=True, exist_ok=True) biggest = images[max(ICON_SIZES)] others = [images[s] for s in ICON_SIZES if s != max(ICON_SIZES)] biggest.save( out, format="ICO", sizes=[(s, s) for s in ICON_SIZES], append_images=others, ) return out def main() -> int: errors = 0 for app_id, rel_dir, phosphor_name, accent_hex in APPS: app_dir = REGISTRY_ROOT / rel_dir if not app_dir.exists(): print(f"SKIP {app_id}: dir not found ({rel_dir})", file=sys.stderr) errors += 1 continue try: out = build_ico(app_id, app_dir, phosphor_name, accent_hex) print(f"OK {app_id:25s} -> {out.relative_to(REGISTRY_ROOT)} ({phosphor_name}, {accent_hex})") except Exception as e: # pragma: no cover - reporting only print(f"FAIL {app_id}: {e}", file=sys.stderr) errors += 1 return 1 if errors else 0 if __name__ == "__main__": raise SystemExit(main())