Files
fn_registry/dev/gen_app_icons.py
T
egutierrez a03675113a chore: auto-commit (286 archivos)
- .claude/agents/fn-orquestador/SKILL.md
- .claude/commands/fn_claude.md
- .claude/rules/INDEX.md
- .claude/rules/cpp_apps.md
- .claude/rules/ids_naming.md
- CHANGELOG.md
- apps/dag_engine/README.md
- apps/dag_engine/api.go
- apps/dag_engine/dags_migrated/example.yaml
- apps/dag_engine/dags_migrated/example_lineage_tracking.yaml
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 16:33:22 +02:00

119 lines
5.1 KiB
Python

#!/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
<app_dir>/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())