#!/usr/bin/env python3 """Generate a macOS .icns app icon for Stampli Bulk Editor.""" import os import subprocess import tempfile from pathlib import Path from PIL import Image, ImageDraw, ImageFont SIZE = 1024 def draw_icon(size=SIZE): img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) s = size / 1024 # scale factor # Rounded-rect background — deep navy blue bg_color = (22, 37, 67) margin = int(80 * s) radius = int(180 * s) draw.rounded_rectangle( [margin, margin, size - margin, size - margin], radius=radius, fill=bg_color, ) # Document shape — white, slightly offset right doc_left = int(280 * s) doc_top = int(180 * s) doc_right = int(750 * s) doc_bottom = int(820 * s) doc_radius = int(30 * s) draw.rounded_rectangle( [doc_left, doc_top, doc_right, doc_bottom], radius=doc_radius, fill=(255, 255, 255), ) # Dog-ear fold on top-right corner of document fold_size = int(90 * s) fold_points = [ (doc_right - fold_size, doc_top), (doc_right, doc_top + fold_size), (doc_right, doc_top), ] draw.polygon(fold_points, fill=bg_color) fold_triangle = [ (doc_right - fold_size, doc_top), (doc_right - fold_size, doc_top + fold_size), (doc_right, doc_top + fold_size), ] draw.polygon(fold_triangle, fill=(200, 210, 225)) # Horizontal lines on document (invoice lines) line_color = (180, 195, 215) line_h = int(12 * s) line_left = int(330 * s) line_right_short = int(580 * s) line_right_long = int(700 * s) y_positions = [340, 400, 460, 520, 580] for i, y in enumerate(y_positions): y_scaled = int(y * s) right = line_right_short if i % 2 else line_right_long draw.rounded_rectangle( [line_left, y_scaled, right, y_scaled + line_h], radius=int(6 * s), fill=line_color, ) # Dollar sign — accent color (Stampli teal/green) accent = (0, 191, 165) dollar_x = int(380 * s) dollar_y = int(660 * s) dollar_size = int(110 * s) draw.ellipse( [ dollar_x - dollar_size, dollar_y - dollar_size, dollar_x + dollar_size, dollar_y + dollar_size, ], fill=accent, ) try: font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", int(130 * s)) except (OSError, IOError): font = ImageFont.load_default() draw.text((dollar_x, dollar_y), "$", fill="white", font=font, anchor="mm") # Checkmark badge — bottom-right badge_cx = int(680 * s) badge_cy = int(750 * s) badge_r = int(85 * s) draw.ellipse( [ badge_cx - badge_r, badge_cy - badge_r, badge_cx + badge_r, badge_cy + badge_r, ], fill=(46, 204, 113), outline=bg_color, width=int(12 * s), ) check_points = [ (int(640 * s), int(750 * s)), (int(670 * s), int(785 * s)), (int(730 * s), int(715 * s)), ] draw.line(check_points, fill="white", width=int(18 * s), joint="curve") # Small pencil icon — top-left area, representing "edit" pencil_color = (255, 193, 7) px, py = int(220 * s), int(280 * s) pencil_body = [ (px, py + int(60 * s)), (px + int(15 * s), py - int(30 * s)), (px + int(45 * s), py - int(20 * s)), (px + int(30 * s), py + int(70 * s)), ] draw.polygon(pencil_body, fill=pencil_color) pencil_tip = [ (px, py + int(60 * s)), (px + int(30 * s), py + int(70 * s)), (px + int(10 * s), py + int(90 * s)), ] draw.polygon(pencil_tip, fill=(60, 60, 60)) return img def make_icns(img, output_path): """Create .icns from a PIL Image using macOS iconutil.""" with tempfile.TemporaryDirectory() as tmpdir: iconset = os.path.join(tmpdir, "icon.iconset") os.makedirs(iconset) sizes = [16, 32, 64, 128, 256, 512, 1024] for sz in sizes: resized = img.resize((sz, sz), Image.LANCZOS) resized.save(os.path.join(iconset, f"icon_{sz}x{sz}.png")) if sz <= 512: double = img.resize((sz * 2, sz * 2), Image.LANCZOS) double.save(os.path.join(iconset, f"icon_{sz}x{sz}@2x.png")) subprocess.run( ["iconutil", "-c", "icns", iconset, "-o", output_path], check=True ) if __name__ == "__main__": project = Path(__file__).parent icon_img = draw_icon() png_path = project / "app_icon.png" icon_img.save(str(png_path)) print(f"Saved PNG: {png_path}") icns_path = project / "app_icon.icns" make_icns(icon_img, str(icns_path)) print(f"Saved ICNS: {icns_path}")