#!/usr/bin/env python3 """Generate the Vestibule icon set (SVG + PNG sizes + Windows .ico). Design: a doorway arch (a vestibule is an entrance hall) in cyan on a deep-slate rounded square, with an amber visitor dot — the kiosk is the doorway, the public is the visitor. Outputs (into /packaging/icons/): vestibule.svg scalable source (Flatpak/scalable) vestibule-.png 16..256 px hicolor sizes (Flatpak) vestibule.ico multi-size Windows icon (Inno Setup) Usage: python3 scripts/make-icons.py """ import os from PIL import Image, ImageDraw REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OUT = os.path.join(REPO, "packaging", "icons") BG = (15, 23, 42, 255) # slate-900 BG_EDGE = (30, 41, 59, 255) # slate-800 rim CYAN = (56, 189, 248, 255) # sky-400 arch AMBER = (251, 191, 36, 255) # amber-400 visitor SIZES = [16, 32, 48, 64, 128, 256] SIMPLE_SIZE_MAX = 32 # at or below: legibility beats detail def geometry(size: int) -> dict: """Scale table: every dimension derives from the canvas size.""" s = size inset_x = int(s * 0.30) arch_w = s - 2 * inset_x return { "s": s, "radius": max(2, int(s * 0.18)), "rim_w": max(1, int(s * 0.023)), "stroke": max(2, int(s * 0.075)), "inset_x": inset_x, "arch_top": int(s * 0.18), "leg_bottom": int(s * 0.82), "arch_w": arch_w, "arch_cx": s // 2, "arch_r": arch_w // 2, "dot_r": max(2, int(s * 0.085)), } def draw_arch(d, g, stroke, with_threshold=True): """Doorway arch (dome + legs) + visitor dot; threshold optional (small canvases drop it for legibility).""" s, x, w = g["s"], g["inset_x"], g["arch_w"] dome_bottom = g["arch_top"] + g["arch_r"] # Dome (top half circle outline). d.arc([x, g["arch_top"], x + w, g["arch_top"] + w], start=180, end=360, fill=CYAN, width=stroke) # Legs. d.line([x, dome_bottom, x, g["leg_bottom"]], fill=CYAN, width=stroke) d.line([s - x, dome_bottom, s - x, g["leg_bottom"]], fill=CYAN, width=stroke) if with_threshold: d.line([x - int(s * 0.04), g["leg_bottom"], s - x + int(s * 0.04), g["leg_bottom"]], fill=CYAN, width=stroke) # Visitor: amber dot on the threshold, centered. dot_cy = g["leg_bottom"] - int(s * 0.02) - g["dot_r"] d.ellipse([g["arch_cx"] - g["dot_r"], dot_cy - g["dot_r"], g["arch_cx"] + g["dot_r"], dot_cy + g["dot_r"]], fill=AMBER) def draw_full_icon(size: int) -> Image.Image: """Full-detail icon: rimmed background + arch.""" g = geometry(size) s = g["s"] img = Image.new("RGBA", (s, s), (0, 0, 0, 0)) d = ImageDraw.Draw(img) # Background: rounded square with a subtle rim. d.rounded_rectangle([0, 0, s - 1, s - 1], radius=g["radius"], fill=BG) d.rounded_rectangle([g["rim_w"], g["rim_w"], s - 1 - g["rim_w"], s - 1 - g["rim_w"]], radius=max(1, g["radius"] - g["rim_w"]), outline=BG_EDGE, width=g["rim_w"]) draw_arch(d, g, g["stroke"]) return img def draw_simple_icon(size: int) -> Image.Image: """Legibility variant for small canvases: rim and threshold dropped, minimum stroke widths.""" g = geometry(size) s = g["s"] img = Image.new("RGBA", (s, s), (0, 0, 0, 0)) d = ImageDraw.Draw(img) d.rounded_rectangle([0, 0, s - 1, s - 1], radius=g["radius"], fill=BG) draw_arch(d, g, max(2, g["stroke"]), with_threshold=False) return img def draw_icon(size: int) -> Image.Image: """Step-down: small canvases use the simple variant, all others full.""" if size <= SIMPLE_SIZE_MAX: return draw_simple_icon(size) return draw_full_icon(size) SVG = """ """ def main(): os.makedirs(OUT, exist_ok=True) with open(os.path.join(OUT, "vestibule.svg"), "w") as f: f.write(SVG) rendered = {size: render_and_save(size) for size in SIZES} # Windows .ico: multi-size, PNG-compressed entries for large sizes. rendered[256].save( os.path.join(OUT, "vestibule.ico"), format="ICO", sizes=[(s, s) for s in (16, 32, 48, 64, 128, 256)], append_images=[rendered[s] for s in (128, 64, 48, 32, 16)], ) print(f"wrote icon set to {OUT}") print("\n".join( f" {name} ({os.path.getsize(os.path.join(OUT, name))} bytes)" for name in sorted(os.listdir(OUT)) )) def render_and_save(size: int) -> Image.Image: img = draw_icon(size) img.save(os.path.join(OUT, f"vestibule-{size}.png")) return img if __name__ == "__main__": main()