#!/usr/bin/env python3 """Draws the app icon and packs the .icns, with no Xcode tooling involved. macOS rounds the corners of nothing: an app icon has to bring its own squircle. This draws one at 1024px, puts a waveform on it, then downsamples to the sizes macOS asks for. The .icns container is written by hand — it is just a header plus one length-prefixed PNG per size. """ import math import pathlib import struct from PIL import Image, ImageDraw OUT = pathlib.Path(__file__).resolve().parent.parent / "mac-app" / "src-tauri" / "icons" BASE = 1024 SUPERSAMPLE = 2 # draw big, shrink down: cheap anti-aliasing BACKGROUND_TOP = (39, 39, 42) BACKGROUND_BOTTOM = (24, 24, 27) WAVE = (250, 250, 250) ACCENT = (96, 165, 250) def squircle_mask(size, radius_ratio=0.2237): """Apple's icon shape is close enough to a rounded rect at this radius.""" mask = Image.new("L", (size, size), 0) draw = ImageDraw.Draw(mask) inset = int(size * 0.085) box = [inset, inset, size - inset, size - inset] draw.rounded_rectangle(box, radius=int(size * radius_ratio), fill=255) return mask def vertical_gradient(size, top, bottom): gradient = Image.new("RGB", (1, size)) for y in range(size): t = y / max(1, size - 1) gradient.putpixel( (0, y), tuple(int(top[i] + (bottom[i] - top[i]) * t) for i in range(3)), ) return gradient.resize((size, size), Image.NEAREST) def draw_icon(size): canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0)) canvas.paste(vertical_gradient(size, BACKGROUND_TOP, BACKGROUND_BOTTOM), (0, 0)) canvas.putalpha(squircle_mask(size)) draw = ImageDraw.Draw(canvas) # A waveform: a few overlaid sines so it reads as audio rather than a # perfect textbook sine. centre = size / 2 left = size * 0.20 right = size * 0.80 span = right - left bars = 23 bar_width = span / (bars * 1.9) gap = (span - bars * bar_width) / (bars - 1) for index in range(bars): phase = index / (bars - 1) envelope = ( 0.55 * math.sin(phase * math.pi) + 0.30 * math.sin(phase * math.pi * 3.7 + 0.6) + 0.18 * math.sin(phase * math.pi * 7.3 + 1.9) ) height = abs(envelope) * size * 0.30 + size * 0.022 x0 = left + index * (bar_width + gap) colour = ACCENT if index % 5 == 2 else WAVE draw.rounded_rectangle( [x0, centre - height, x0 + bar_width, centre + height], radius=bar_width / 2, fill=colour, ) return canvas def render(size): big = draw_icon(size * SUPERSAMPLE) return big.resize((size, size), Image.LANCZOS) def write_icns(path, images): """ICNS: 'icns' + total length, then (type, length, PNG payload) records.""" # Type codes macOS reads PNG data from, per size. types = { 16: b"icp4", 32: b"icp5", 64: b"icp6", 128: b"ic07", 256: b"ic08", 512: b"ic09", 1024: b"ic10", } chunks = [] for size, png in images.items(): if size not in types: continue chunks.append(types[size] + struct.pack(">I", len(png) + 8) + png) body = b"".join(chunks) path.write_bytes(b"icns" + struct.pack(">I", len(body) + 8) + body) def main(): OUT.mkdir(parents=True, exist_ok=True) sizes = [16, 32, 64, 128, 256, 512, 1024] rendered = {size: render(size) for size in sizes} # What tauri.conf.json points at. rendered[32].save(OUT / "32x32.png") rendered[128].save(OUT / "128x128.png") rendered[256].save(OUT / "128x128@2x.png") rendered[512].save(OUT / "icon.png") pngs = {} for size, image in rendered.items(): temp = OUT / ("_%d.png" % size) image.save(temp) pngs[size] = temp.read_bytes() temp.unlink() write_icns(OUT / "icon.icns", pngs) for name in ("32x32.png", "128x128.png", "128x128@2x.png", "icon.png", "icon.icns"): print("%-16s %6d bytes" % (name, (OUT / name).stat().st_size)) if __name__ == "__main__": main()