#!/usr/bin/env python3 """Generate AgentCompany icons from SVG sources using cairosvg. Two icon families: - Web (circle bg): icon.svg → landing favicon + website logo - App (square bg): icon-app.svg → Electron app icon (Apple HIG, OS applies mask) - Tray: rendered from logo.svg for menu bar / notification area """ from __future__ import annotations import io import os import shutil import subprocess from pathlib import Path # ── Ensure cairo library is discoverable on macOS (Homebrew) ────────── for _lib_dir in ("/opt/homebrew/opt/cairo/lib", "/usr/local/opt/cairo/lib"): if Path(_lib_dir).exists(): os.environ["DYLD_LIBRARY_PATH"] = ( _lib_dir + ":" + os.environ.get("DYLD_LIBRARY_PATH", "") ) break import cairosvg # noqa: E402 — must come after DYLD_LIBRARY_PATH setup from PIL import Image # noqa: E402 ROOT = Path(__file__).resolve().parents[1] ASSETS = ROOT / "assets" LANDING = ROOT / "landing" / "public" ICONSET = ASSETS / "icon.iconset" LOGO_SVG = ROOT / "logo.svg" # SVG sources WEB_SVG = ASSETS / "icon.svg" # circle bg — web favicon APP_SVG = ASSETS / "icon-app.svg" # square bg — Electron (Apple HIG) SIZE = 1024 # ── Helpers ─────────────────────────────────────────────────────────── def svg_to_image(svg_path: Path, width: int, height: int | None = None) -> Image.Image: """Render an SVG to a Pillow RGBA Image at the given size.""" h = height or width png_data = cairosvg.svg2png( url=str(svg_path), output_width=width, output_height=h, ) return Image.open(io.BytesIO(png_data)).convert("RGBA") def svg_bytes_to_image(svg_bytes: bytes, width: int, height: int | None = None) -> Image.Image: """Render SVG bytes to a Pillow RGBA Image.""" h = height or width png_data = cairosvg.svg2png( bytestring=svg_bytes, output_width=width, output_height=h, ) return Image.open(io.BytesIO(png_data)).convert("RGBA") # ── Web icons (circle background) ──────────────────────────────────── def generate_web_icons() -> None: """Generate favicon.ico and icon.png for the landing site (circle bg).""" print("Generating web icons from icon.svg (circle bg)…") master = svg_to_image(WEB_SVG, SIZE) # PNG for apple-touch-icon / og web_png = LANDING / "icon.png" master.save(web_png, format="PNG") print(f" → {web_png}") # Copy SVG to landing shutil.copy2(WEB_SVG, LANDING / "icon.svg") print(f" → {LANDING / 'icon.svg'}") # ICO with multiple sizes for favicon favicon = LANDING / "favicon.ico" master.save( favicon, format="ICO", sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], ) print(f" → {favicon}") # ── App icons (square background, Apple HIG) ───────────────────────── def generate_app_icons() -> None: """Generate Electron app icons: PNG, ICO, ICNS (square bg, Apple HIG).""" print("Generating app icons from icon-app.svg (square bg, Apple HIG)…") master = svg_to_image(APP_SVG, SIZE) # Main PNG png_path = ASSETS / "icon.png" master.save(png_path, format="PNG") print(f" → {png_path}") # Windows ICO ico_path = ASSETS / "icon.ico" master.save( ico_path, format="ICO", sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)], ) print(f" → {ico_path}") # macOS ICNS via iconutil generate_icns(master) def generate_icns(master: Image.Image) -> None: """Generate .icns from a master image using macOS iconutil.""" if ICONSET.exists(): shutil.rmtree(ICONSET) ICONSET.mkdir(parents=True, exist_ok=True) for size in (16, 32, 64, 128, 256, 512): master.resize((size, size), Image.Resampling.LANCZOS).save( ICONSET / f"icon_{size}x{size}.png", format="PNG", ) master.resize((size * 2, size * 2), Image.Resampling.LANCZOS).save( ICONSET / f"icon_{size}x{size}@2x.png", format="PNG", ) icns_path = ASSETS / "icon.icns" subprocess.run( ["iconutil", "-c", "icns", str(ICONSET), "-o", str(icns_path)], check=True, ) shutil.rmtree(ICONSET) print(f" → {icns_path}") # ── Tray / notification area icons ─────────────────────────────────── def generate_tray_icons() -> None: """Generate tray icons following Apple HIG for menu bar. - Color (Windows/Linux): square bg with logo, 32×32 and 64×64 - Template (macOS): monochrome black logo on transparent, 22×22 and 44×44 """ print("Generating tray icons…") # Color tray icons — render app icon (square bg) at small sizes for name, size in [("tray-icon.png", 32), ("tray-icon@2x.png", 64)]: img = svg_to_image(APP_SVG, size) img.save(ASSETS / name, format="PNG") print(f" → {ASSETS / name}") # macOS template icons — black logo silhouette on transparent # Read logo.svg paths, render as black on transparent logo_svg = LOGO_SVG.read_text() for name, size in [("tray-iconTemplate.png", 22), ("tray-iconTemplate@2x.png", 44)]: # Create SVG with black fill, sized to target template_svg = _make_template_svg(logo_svg, size) img = svg_bytes_to_image(template_svg.encode("utf-8"), size) img.save(ASSETS / name, format="PNG") print(f" → {ASSETS / name}") def _make_template_svg(logo_svg_content: str, target_size: int) -> str: """Create a macOS template SVG: black logo silhouette on transparent bg.""" # Logo viewBox is 0 0 122 126 — scale to fit target with padding padding = max(2, int(target_size * 0.1)) available = target_size - 2 * padding # Scale to fit height (taller dimension) scale = available / 126 logo_w = 122 * scale # Center horizontally offset_x = (target_size - logo_w) / 2 offset_y = padding return f""" """ # ── Main ────────────────────────────────────────────────────────────── def main() -> None: ASSETS.mkdir(parents=True, exist_ok=True) LANDING.mkdir(parents=True, exist_ok=True) generate_web_icons() generate_app_icons() generate_tray_icons() print("\nDone! All icons generated.") if __name__ == "__main__": main()