#!/usr/bin/env python3 """Generate Bugger app icon (bug beetle) in all required sizes for macOS.""" import math, os, shutil from pathlib import Path from PIL import Image, ImageDraw ROOT = Path(__file__).resolve().parent.parent ICONSET = ROOT / "Resources" / "Assets.xcassets" / "AppIcon.appiconset" # Required macOS icon sizes: (logical_size, scale_factor, actual_pixels) SIZES = [ (16, 1, 16), (16, 2, 32), (32, 1, 32), (32, 2, 64), (128, 1, 128), (128, 2, 256), (256, 1, 256), (256, 2, 512), (512, 1, 512), (512, 2, 1024), ] def draw_bug(canvas_size: int) -> Image.Image: """Draw a bug beetle icon at given canvas size. Returns RGBA image.""" img = Image.new("RGBA", (canvas_size, canvas_size), (0, 0, 0, 0)) d = ImageDraw.Draw(img) s = canvas_size # shorthand # Colors body_fill = (45, 50, 55, 255) # dark charcoal body_highlight = (65, 72, 78, 255) # lighter shell accent_green = (76, 188, 128, 255) # green accent antenna = (45, 50, 55, 255) leg = (45, 50, 55, 255) eye = (255, 255, 255, 255) # Margins (proportional) margin = s * 0.12 # ---- Body (rounded oval) ---- body_rect = ( margin + s * 0.08, # left margin + s * 0.22, # top s - margin - s * 0.08, # right s - margin + s * 0.05, # bottom ) d.rounded_rectangle(body_rect, radius=s * 0.28, fill=body_fill) # ---- Shell highlight (lighter arc on top half) ---- shell_rect = ( margin + s * 0.06, margin + s * 0.22, s - margin - s * 0.06, s * 0.62, ) d.pieslice(shell_rect, start=180, end=360, fill=body_highlight) # ---- Green accent stripe ---- stripe_rect = ( s * 0.32, s * 0.38, s * 0.68, s * 0.62, ) d.rounded_rectangle(stripe_rect, radius=s * 0.06, fill=accent_green) # ---- Head (small semi-circle at top) ---- head_center = (s * 0.50, s * 0.30) head_r = s * 0.095 d.ellipse( (head_center[0] - head_r, head_center[1] - head_r, head_center[0] + head_r, head_center[1] + head_r), fill=body_fill ) # ---- Eyes ---- eye_r = s * 0.025 eye_offset = s * 0.04 for ex in (head_center[0] - eye_offset, head_center[0] + eye_offset): d.ellipse( (ex - eye_r, head_center[1] - eye_r - s * 0.01, ex + eye_r, head_center[1] + eye_r - s * 0.01), fill=eye ) # ---- Antennae (two curved lines) ---- ant_line_w = max(2, int(s * 0.015)) ant_start_y = head_center[1] - head_r + s * 0.01 for side, curve_dir in [(-1, -0.08), (1, 0.08)]: ant_start_x = head_center[0] + side * s * 0.04 points = [ (ant_start_x, ant_start_y), (ant_start_x + side * s * 0.06, ant_start_y - s * 0.08), (ant_start_x + side * s * 0.14, ant_start_y - s * 0.18 + curve_dir * s), (ant_start_x + side * s * 0.18 + curve_dir * s * 0.5, ant_start_y - s * 0.17), ] for i in range(len(points) - 1): d.line([points[i], points[i + 1]], fill=antenna, width=ant_line_w) # dot at tip tip_r = max(1, int(s * 0.012)) tip = points[-1] d.ellipse((tip[0] - tip_r, tip[1] - tip_r, tip[0] + tip_r, tip[1] + tip_r), fill=antenna) # ---- Legs (3 per side) ---- leg_w = max(2, int(s * 0.018)) leg_positions = [0.45, 0.56, 0.67] # y-positions as fraction of body height body_left = margin + s * 0.02 body_right = s - margin - s * 0.02 for frac in leg_positions: leg_y = s * (0.22 + frac * 0.57) # Left legs d.line( [(body_left, leg_y), (body_left - s * 0.16, leg_y - s * 0.08), (body_left - s * 0.22, leg_y + s * 0.04)], fill=leg, width=leg_w ) # Right legs d.line( [(body_right, leg_y), (body_right + s * 0.16, leg_y - s * 0.08), (body_right + s * 0.22, leg_y + s * 0.04)], fill=leg, width=leg_w ) return img def main(): ICONSET.mkdir(parents=True, exist_ok=True) # Generate the largest size and downscale master = draw_bug(1024) images = [] for logical, scale, pixels in SIZES: if pixels == 1024: img = master else: img = master.resize((pixels, pixels), Image.LANCZOS) filename = f"icon_{logical}x{logical}@x{scale}.png" filepath = ICONSET / filename img.save(filepath, "PNG") images.append({ "size": f"{logical}x{logical}", "scale": f"{scale}x", "filename": filename, }) print(f" {filename} ({pixels}×{pixels})") # Update Contents.json contents = { "images": [], "info": {"author": "xcode", "version": 1}, } for img in images: contents["images"].append({ "size": img["size"], "idiom": "mac", "filename": img["filename"], "scale": img["scale"], }) import json contents_path = ICONSET / "Contents.json" with open(contents_path, "w") as f: json.dump(contents, f, indent=2) print(f"\nWrote {contents_path}") print(f"Generated {len(images)} icon sizes in {ICONSET}") if __name__ == "__main__": main()