#!/usr/bin/env python3 """Rasterise icon.svg into the PNG sizes ui.py loads for the window icon. Tk 8.6 reads PNG but not SVG, so the icon ships pre-rendered in assets/. The glyph is laid on a rounded plate in the app's own colours: light on dark keeps it legible against both a light and a dark dock, which the bare SVG (slate grey on nothing) is not. Needs inkscape for the render and Pillow for the downscales. Run it after editing icon.svg, then ./install_desktop.sh to refresh the dock's copies. """ import re import shutil import subprocess import sys from pathlib import Path HERE = Path(__file__).resolve().parent SRC = HERE / "icon.svg" OUT = HERE / "assets" SIZES = (256, 128, 64, 48, 32, 16) PLATE = 64 # plate side, in the units the composite is drawn in GLYPH = 44 # how much of that side the glyph's longest edge covers RADIUS = 14 # corner rounding of the plate BG = "#2b2b2b" # ui.BAR FG = "#ededed" # ui.FG def glyph_box(svg): """Bounding box of the drawing, in viewBox units. inkscape answers in the file's own pixel units, which are viewBox units only when width and viewBox agree — so convert. """ out = subprocess.run(["inkscape", "--query-all", str(svg)], capture_output=True, text=True, check=True).stdout line = next(l for l in out.splitlines() if l.startswith("svg")) x, y, w, h = (float(v) for v in line.split(",")[1:5]) text = svg.read_text() width = float(re.search(r'\swidth="([\d.]+)"', text).group(1)) vb_w = float(re.search(r'viewBox="[\d.\-]+ [\d.\-]+ ([\d.]+)', text).group(1)) u = vb_w / width return x * u, y * u, w * u, h * u def main(): if not SRC.is_file(): sys.exit(f"{SRC.name} not found") if not shutil.which("inkscape"): sys.exit("inkscape not found — needed to rasterise the SVG") try: from PIL import Image except ImportError: sys.exit("Pillow not found — needed to downscale the render") d = re.search(r'\sd="([^"]+)"', SRC.read_text()).group(1) x, y, w, h = glyph_box(SRC) # Scale the glyph to GLYPH along its longest edge, then centre what that # leaves. The offsets cancel the bbox origin, which is not at 0,0. s = GLYPH / max(w, h) tx = (PLATE - w * s) / 2 - x * s ty = (PLATE - h * s) / 2 - y * s OUT.mkdir(exist_ok=True) composite = OUT / "_composite.svg" composite.write_text( f'\n' f' \n' f' \n' f' \n' f' \n\n') master = OUT / "_master.png" subprocess.run(["inkscape", "--export-type=png", f"--export-filename={master}", "-w", "512", "-h", "512", str(composite)], check=True, capture_output=True) img = Image.open(master).convert("RGBA") for size in SIZES: img.resize((size, size), Image.LANCZOS).save(OUT / f"icon-{size}.png") composite.unlink() master.unlink() print(f"glyph bbox {w:.2f}x{h:.2f} at ({x:.2f}, {y:.2f}) " f"-> scale {s:.3f}, offset ({tx:.2f}, {ty:.2f})") print("wrote " + ", ".join(f"icon-{s}.png" for s in SIZES) + f" in {OUT}") if __name__ == "__main__": main()