Files
better-claude-config/scripts/build_icons.py
T

153 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Generate icons/app.ico (Windows) and icons/app.icns (macOS) from the
rounded PNG source files in icons/twin-gears/rounded/.
Run from the repo root:
python scripts/build_icons.py
Requirements:
pip install pillow # for .ico
iconutil # built into macOS; for .icns
"""
from __future__ import annotations
import shutil
import subprocess
import tempfile
from pathlib import Path
ROOT = Path(__file__).parent.parent
SRC = ROOT / "icons" / "twin-gears" / "rounded"
OUT = ROOT / "icons"
def build_ico():
try:
from PIL import Image
except ImportError:
print("Pillow not installed — skipping .ico generation (pip install pillow)")
return
import io
import struct
sizes = [16, 24, 32, 48, 64, 128, 256]
images: list[tuple[int, object]] = []
for s in sizes:
p = SRC / f"icon-{s}.png"
if not p.exists():
print(f" Missing {p.name}, skipping")
continue
images.append((s, Image.open(p).convert("RGBA")))
if not images:
print(" No source PNGs found — cannot build .ico")
return
# Write the ICO manually so that entries ≤ 128 px use uncompressed BMP DIB
# and the 256 px entry uses PNG. Pillow's ICO saver stores all sizes as
# PNG-compressed ("Vista icon" format), which PyInstaller's Windows
# resource-updater cannot embed — it silently falls back to its default icon.
def bmp_dib(img: object) -> bytes:
"""Return a BMP DIB (BITMAPINFOHEADER + BGRA rows + AND mask)."""
w, h = img.size
# biHeight is doubled: top half = XOR mask (color), bottom = AND mask
hdr = struct.pack(
"<IiiHHIIiiII",
40, # biSize
w,
h * 2, # biHeight (doubled per ICO convention)
1, # biPlanes
32, # biBitCount
0, # biCompression (BI_RGB)
0, # biSizeImage (0 ok for BI_RGB)
0,
0,
0,
0,
)
pix = img.load()
rows = bytearray()
for y in range(h - 1, -1, -1): # bottom-up
for x in range(w):
r, g, b, a = pix[x, y]
rows += bytes([b, g, r, a])
# AND mask: 1 bit/pixel, rows padded to 32-bit boundary, all 0 (use alpha)
mask_row = ((w + 31) // 32) * 4
and_mask = bytes(mask_row * h)
return hdr + bytes(rows) + and_mask
def png_bytes(img: object) -> bytes:
buf = io.BytesIO()
img.save(buf, format="PNG")
return buf.getvalue()
blobs: list[tuple[int, bytes]] = []
for s, img in images:
blobs.append((s, bmp_dib(img) if s < 256 else png_bytes(img)))
n = len(blobs)
dir_offset = 6 + n * 16 # ICONDIR (6) + n × ICONDIRENTRY (16)
out = bytearray()
out += struct.pack("<HHH", 0, 1, n) # ICONDIR
cur = dir_offset
for s, blob in blobs:
w = h = s % 256 # 256 is stored as 0 in the byte field
out += struct.pack("<BBBBHHII", w, h, 0, 0, 1, 32, len(blob), cur)
cur += len(blob)
for _, blob in blobs:
out += blob
dest = OUT / "app.ico"
dest.write_bytes(out)
print(f" Generated {dest.relative_to(ROOT)} ({len(out) // 1024 + 1} KB)")
def build_icns():
if shutil.which("iconutil") is None:
print(" iconutil not found (macOS-only) — skipping .icns generation")
return
iconset = Path(tempfile.mkdtemp()) / "app.iconset"
iconset.mkdir()
mapping = {
"icon_16x16.png": "icon-16.png",
"icon_16x16@2x.png": "icon-32.png",
"icon_32x32.png": "icon-32.png",
"icon_32x32@2x.png": "icon-64.png",
"icon_128x128.png": "icon-128.png",
"icon_128x128@2x.png": "icon-256.png",
"icon_256x256.png": "icon-256.png",
"icon_256x256@2x.png": "icon-512.png",
"icon_512x512.png": "icon-512.png",
"icon_512x512@2x.png": "icon-1024.png",
}
for dst_name, src_name in mapping.items():
src = SRC / src_name
if src.exists():
shutil.copy2(src, iconset / dst_name)
else:
print(f" Missing {src_name}, skipping {dst_name}")
dest = OUT / "app.icns"
subprocess.run(
["iconutil", "-c", "icns", str(iconset), "-o", str(dest)],
check=True,
)
shutil.rmtree(iconset.parent)
print(f" Generated {dest.relative_to(ROOT)} ({dest.stat().st_size // 1024} KB)")
if __name__ == "__main__":
print("Building icons/app.ico ...")
build_ico()
print("Building icons/app.icns ...")
build_icns()
print("Done.")