mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
Older macOS versions display Buzz’s opaque icon as an oversized square. Bake rounded corners and transparent padding into the macOS `.icns`, preserving the original bee artwork and leaving other platforms unchanged. Generation uses Hermit-pinned uv, Python 3.14.3, and Pillow 12.2.0. `scripts/macos-icon.sh --check` regenerates temporarily and compares all decoded representations; macOS CI runs it before packaging. Pixel reproducibility is verified independently of Apple’s ICNS container encoding. Validated native AppKit preview, all 10 representations, fresh-cache generation, one-pixel/missing-representation drift rejection, workflow lint, and pre-push checks. Full local `just ci` is running. Packaged Dock checks on older/current macOS remain outstanding. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the legacy macOS icon without relying on system corner masking.
|
|
|
|
Run: scripts/macos-icon.sh [--check]
|
|
|
|
The wrapper pins uv, Python and Pillow. Verification compares the complete set
|
|
of decoded RGBA representations, not PNG encoding or ICNS container bytes:
|
|
Apple's system iconutil may encode the same pixels differently across macOS.
|
|
The shared artwork and other platforms' icons are intentionally untouched.
|
|
"""
|
|
|
|
# /// script
|
|
# requires-python = "==3.14.3"
|
|
# dependencies = ["Pillow==12.2.0"]
|
|
# ///
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
|
|
from PIL import Image, ImageDraw
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
ICONS = ROOT / "desktop/src-tauri/icons"
|
|
|
|
|
|
def generate(output):
|
|
# An 824px tile on a 1024px canvas leaves the macOS optical inset.
|
|
# Work at 4x resolution to antialias the rounded edge at every icon size.
|
|
scale = 4
|
|
canvas = Image.new("RGBA", (1024 * scale, 1024 * scale))
|
|
with Image.open(ICONS / "buzz-source.png") as source:
|
|
tile = source.convert("RGBA").resize(
|
|
(824 * scale, 824 * scale), Image.Resampling.LANCZOS
|
|
)
|
|
mask = Image.new("L", tile.size)
|
|
ImageDraw.Draw(mask).rounded_rectangle(
|
|
(0, 0, tile.width - 1, tile.height - 1), radius=185 * scale, fill=255
|
|
)
|
|
canvas.paste(tile, (100 * scale, 100 * scale), mask)
|
|
|
|
with tempfile.TemporaryDirectory(prefix="buzz-macos-icon-") as temporary:
|
|
iconset = Path(temporary) / "Buzz.iconset"
|
|
iconset.mkdir()
|
|
for size in (16, 32, 128, 256, 512):
|
|
for density in (1, 2):
|
|
suffix = "@2x" if density == 2 else ""
|
|
canvas.resize(
|
|
(size * density, size * density), Image.Resampling.LANCZOS
|
|
).save(iconset / f"icon_{size}x{size}{suffix}.png")
|
|
subprocess.run(
|
|
["/usr/bin/iconutil", "-c", "icns", str(iconset), "-o", str(output)],
|
|
check=True,
|
|
)
|
|
|
|
|
|
def verify(candidate, reference):
|
|
"""Reject missing/extra representations and any decoded pixel drift."""
|
|
with tempfile.TemporaryDirectory(prefix="buzz-icon-check-") as temporary:
|
|
sets = []
|
|
for name, icon in (("candidate", candidate), ("reference", reference)):
|
|
iconset = Path(temporary) / f"{name}.iconset"
|
|
subprocess.run(
|
|
["/usr/bin/iconutil", "-c", "iconset", str(icon), "-o", str(iconset)],
|
|
check=True,
|
|
)
|
|
sets.append({p.name: p for p in iconset.glob("*.png")})
|
|
if sets[0].keys() != sets[1].keys():
|
|
raise ValueError("macOS icon representation set drifted")
|
|
for name in sorted(sets[0]):
|
|
with Image.open(sets[0][name]) as a, Image.open(sets[1][name]) as b:
|
|
if a.size != b.size or a.convert("RGBA").tobytes() != b.convert("RGBA").tobytes():
|
|
raise ValueError(f"macOS icon pixel drift: {name}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--check", action="store_true", help="verify without modifying the icon")
|
|
parser.add_argument("--output", type=Path, default=ICONS / "icon.icns")
|
|
args = parser.parse_args()
|
|
if args.check:
|
|
with tempfile.TemporaryDirectory(prefix="buzz-icon-regenerate-") as temporary:
|
|
generated = Path(temporary) / "icon.icns"
|
|
generate(generated)
|
|
verify(args.output, generated)
|
|
print("macOS icon: all decoded representations match")
|
|
else:
|
|
generate(args.output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|