a03675113a
- .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>
119 lines
5.1 KiB
Python
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())
|