Files
fn_registry/python/functions/infra/generate_app_icon.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

154 lines
5.5 KiB
Python

"""Genera un icono .ico multi-resolucion para apps C++ del registry.
Rasteriza un icono Phosphor SVG sobre un fondo redondeado del color accent
y exporta un .ico con multiples resoluciones (16, 24, 32, 48, 64, 128, 256).
"""
import io
import os
from pathlib import Path
import cairosvg
from PIL import Image, ImageDraw
DEFAULT_SIZES = [16, 24, 32, 48, 64, 128, 256]
def _find_registry_root() -> Path:
"""Descubre la raiz del registry caminando hacia arriba hasta encontrar registry.db."""
env_root = os.environ.get("FN_REGISTRY_ROOT")
if env_root:
return Path(env_root).resolve()
current = Path(__file__).resolve()
for parent in current.parents:
if (parent / "registry.db").exists():
return parent
raise FileNotFoundError(
"No se encontro registry.db caminando desde el archivo hasta la raiz. "
"Define FN_REGISTRY_ROOT en el entorno."
)
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:
"""Renderiza el SVG Phosphor como glyph blanco sobre fondo transparente."""
svg = svg_path.read_text(encoding="utf-8")
# Phosphor usa fill="currentColor" — forzar blanco.
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:
"""Compone fondo redondeado con color accent + glyph blanco centrado al 70%."""
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% de radio redondeado
draw.rounded_rectangle(
[(0, 0), (size - 1, size - 1)],
radius=radius,
fill=bg_color,
)
# El glyph ocupa ~70% del canvas (padding ~15% en cada lado).
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 generate_app_icon(
phosphor_icon_name: str,
accent_hex: str,
out_ico_path: str,
*,
weight: str = "fill",
sizes: list[int] = None,
phosphor_root: str = None,
) -> str:
"""Genera un icono .ico multi-resolucion a partir de un SVG Phosphor.
Rasteriza el icono Phosphor indicado sobre un fondo redondeado del color
accent y exporta un .ico con multiples resoluciones ordenadas de mayor a
menor para maxima compatibilidad con Windows.
Args:
phosphor_icon_name: Nombre del icono Phosphor sin sufijo de weight
(ej. "chart-bar", "tree-structure", "gauge").
accent_hex: Color de fondo en formato hexadecimal "#RRGGBB"
(ej. "#0ea5e9", "#7c3aed").
out_ico_path: Ruta de salida para el archivo .ico. Puede ser absoluta
o relativa al directorio de trabajo actual. El directorio padre
se crea si no existe.
weight: Variante del icono Phosphor. Default "fill". Otros valores
validos segun el repositorio: "regular", "bold", "light",
"thin", "duotone".
sizes: Lista de resoluciones a incluir en el .ico. Default
[16, 24, 32, 48, 64, 128, 256]. El orden no importa; se
renderiza cada tamano individualmente para maxima crispness.
phosphor_root: Ruta a la carpeta raiz de assets de phosphor-core
(la que contiene subdirectorios "fill", "regular", etc.).
Default: <registry_root>/sources/phosphor-core/assets.
Returns:
Ruta absoluta del archivo .ico generado.
Raises:
FileNotFoundError: Si el SVG del icono no existe en phosphor_root.
ValueError: Si accent_hex no tiene el formato "#RRGGBB".
"""
if sizes is None:
sizes = DEFAULT_SIZES
# Resolver raiz de phosphor
if phosphor_root is None:
registry_root = _find_registry_root()
phosphor_assets = registry_root / "sources" / "phosphor-core" / "assets"
else:
phosphor_assets = Path(phosphor_root)
svg_file = phosphor_assets / weight / f"{phosphor_icon_name}-{weight}.svg"
if not svg_file.exists():
raise FileNotFoundError(
f"Icono Phosphor no encontrado: {svg_file}\n"
f"Asegurate de que sources/phosphor-core/ existe. Si no:\n"
f" git clone --depth=1 https://github.com/phosphor-icons/core.git "
f"sources/phosphor-core"
)
# Validar formato del color
h = accent_hex.lstrip("#")
if len(h) != 6:
raise ValueError(f"accent_hex debe tener formato #RRGGBB, recibido: {accent_hex!r}")
# Renderizar cada resolucion individualmente para crispness en tamanos pequeños
images = {s: _make_icon_image(svg_file, accent_hex, s) for s in sorted(sizes, reverse=True)}
out = Path(out_ico_path)
if not out.is_absolute():
out = Path.cwd() / out
out.parent.mkdir(parents=True, exist_ok=True)
sorted_sizes = sorted(sizes, reverse=True)
biggest = images[sorted_sizes[0]]
others = [images[s] for s in sorted_sizes[1:]]
biggest.save(
out,
format="ICO",
sizes=[(s, s) for s in sorted_sizes],
append_images=others,
)
return str(out.resolve())