"""Render the existing SVG brand sources with Tauri, then export PNG/ICO/ICNS/BMP. Requires the installed desktop Node dependencies and Python's Pillow package. Text is rasterized with Pillow so exports do not depend on the SVG renderer's font discovery. No dependency is added to the application or its build hooks. """ import argparse import copy import io import os from pathlib import Path import shutil import struct import subprocess import tempfile import xml.etree.ElementTree as ET from PIL import Image, ImageDraw, ImageFont DESKTOP = Path(__file__).resolve().parents[1] ICONS = DESKTOP / "src-tauri/icons" INSTALLER = DESKTOP / "src-tauri/installer" SVG = "{http://www.w3.org/2000/svg}" ET.register_namespace("", SVG[1:-1]) def system_font(monospace=False, bold=False): fonts = Path(os.environ.get("WINDIR", "C:/Windows")) / "Fonts" candidates = ( [fonts / ("consolab.ttf" if bold else "consola.ttf"), Path("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf")] if monospace else [fonts / ("msyhbd.ttc" if bold else "msyh.ttc"), Path("/System/Library/Fonts/PingFang.ttc"), Path("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc")] ) for path in candidates: if path.exists(): return path raise RuntimeError("Set --sans-font and --mono-font to installed fonts supporting the artwork's text.") def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--sans-font", type=Path) parser.add_argument("--mono-font", type=Path) args = parser.parse_args() node = shutil.which("node") cli = DESKTOP / "node_modules/@tauri-apps/cli/tauri.js" if not node or not cli.exists(): raise RuntimeError("Install Node and run pnpm install --frozen-lockfile in desktop/ first.") with tempfile.TemporaryDirectory(prefix="rmic-brand-") as temporary: scratch = Path(temporary) def render(root, size): source = scratch / "source.svg" ET.ElementTree(root).write(source, encoding="utf-8", xml_declaration=True) subprocess.run([node, str(cli), "icon", str(source), "--output", str(scratch), "--png", str(size)], cwd=DESKTOP, check=True, capture_output=True) with Image.open(scratch / f"{size}x{size}.png") as image: return image.convert("RGBA") original = ET.parse(ICONS / "icon.svg").getroot() images = {} for size in (16, 20, 24, 32, 48, 64, 128, 256, 512, 1024): root = copy.deepcopy(original) scale = 0.97 if size <= 48 else 0.88 if size <= 128 else 0.82 group = root.find(f"{SVG}g") group.set("transform", group.get("transform").replace("scale(0.82)", f"scale({scale})")) images[size] = render(root, size) for filename, size in (("32x32.png", 32), ("128x128.png", 128), ("128x128@2x.png", 256), ("icon.png", 512)): images[size].save(ICONS / filename) def png(size): stream = io.BytesIO() images[size].save(stream, format="PNG") return stream.getvalue() # Supply every ICO size separately, preserving optical scaling at 16–48px. entries, payloads = [], [] sizes = (16, 20, 24, 32, 48, 64, 128, 256) offset = 6 + 16 * len(sizes) for size in sizes: data = png(size) entries.append(struct.pack("I", 8 + len(data)) + data) body = b"".join(chunks) (ICONS / "icon.icns").write_bytes(b"icns" + struct.pack(">I", 8 + len(body)) + body) for name in ("header", "sidebar"): root = ET.parse(INSTALLER / f"{name}.svg").getroot() width, height = int(root.get("width")), int(root.get("height")) text = [] for element in list(root): if element.tag == f"{SVG}text": text.append(element) root.remove(element) elif element.tag == f"{SVG}image": # Installer art may reference the unframed original mark. # Preserve its own viewBox instead of replacing every image # with the square application icon. source = (INSTALLER / element.get("href")).resolve() logo = ET.parse(source).getroot() for attr in ("x", "y", "width", "height"): logo.set(attr, element.get(attr)) logo.set("preserveAspectRatio", "xMidYMid meet") index = list(root).index(element) root.remove(element) root.insert(index, logo) edge = max(width, height) root.set("width", str(edge)) root.set("height", str(edge)) root.set("viewBox", f"0 0 {edge} {edge}") factor = 4 image = render(root, edge * factor) draw = ImageDraw.Draw(image) for element in text: mono = element.get("font-family") == "monospace" bold = element.get("font-weight") == "700" font_path = (args.mono_font if mono else args.sans_font) or system_font(mono, bold) font = ImageFont.truetype(str(font_path), round(float(element.get("font-size")) * factor)) draw.text((float(element.get("x")) * factor, float(element.get("y")) * factor), element.text, font=font, fill=element.get("fill"), anchor="ls") image = image.crop((0, 0, width * factor, height * factor)).convert("RGB") image.resize((width, height), Image.Resampling.LANCZOS).save(INSTALLER / f"{name}.png") # MUI scales these to the dialog's font/DPI. Supply 2x bitmaps so # Windows does not have to enlarge the raster text on modern screens. image.resize((width * 2, height * 2), Image.Resampling.LANCZOS).save(INSTALLER / f"{name}.bmp") print("Generated rounded PNG/ICO/ICNS icons and 24-bit NSIS header/sidebar artwork.") if __name__ == "__main__": main()