Reads and edits BWF metadata for production sound. One source tree builds a single self-contained page and a native Tauri app with a Rust audio engine and WAV writer. Around 370 checks across seven test suites. First commit of the existing state, so that from here every change can be seen and undone.
139 lines
4.0 KiB
Python
139 lines
4.0 KiB
Python
#!/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()
|