#!/usr/bin/env python3 """Pack icon.png into a multi-size icon.ico used by the Windows resource compiler. ICO format note: Vista+ allows PNG-encoded frames inside ICO files, BUT the Microsoft resource compiler (rc.exe) only reliably accepts PNG payloads for the 256x256 frame. PNG payloads for 16/32/48/64/128 are silently dropped (or worse — rc.exe writes a resource section Explorer can't decode and the EXE shows the generic icon). So we encode small sizes as DIB (BITMAPINFOHEADER + BGRA pixels + AND-mask) and keep PNG only for 256. Requires Pillow. We use it to decode the source PNG and resample. python3 -m venv .venv && .venv/bin/pip install Pillow .venv/bin/python3 resources/build_ico.py """ import io import struct import sys from pathlib import Path try: from PIL import Image except ImportError: sys.exit("Pillow required: pip install Pillow") DIB_SIZES = [16, 32, 48, 64, 128] # BITMAPINFOHEADER + BGRA PNG_SIZES = [256] # PNG payload (preserves alpha cleanly at large size) HERE = Path(__file__).parent SRC = HERE / "icon.png" def encode_dib(img: Image.Image, size: int) -> bytes: """Encode an RGBA image as a DIB-format ICO frame. Layout: BITMAPINFOHEADER (40 bytes), then BGRA pixels bottom-up, then a 1-bit AND-mask (also bottom-up, row-padded to 4 bytes). The header declares double the actual height so Windows knows the AND-mask is appended — this is the (counter-intuitive but mandatory) ICO convention; the file is otherwise an ordinary 32bpp BMP minus the 14-byte BITMAPFILEHEADER. """ img = img.convert("RGBA").resize((size, size), Image.LANCZOS) pixels = img.load() # 32bpp + alpha — AND mask can be all zero (fully opaque-from-mask; # the per-pixel alpha is what Windows actually composites). Still # required structurally. bgra_rows = [] for y in range(size - 1, -1, -1): # bottom-up row = bytearray() for x in range(size): r, g, b, a = pixels[x, y] row += bytes((b, g, r, a)) bgra_rows.append(bytes(row)) bgra = b"".join(bgra_rows) mask_row_bytes = ((size + 31) // 32) * 4 # 4-byte aligned mask = b"\x00" * (mask_row_bytes * size) header = struct.pack( " bytes: img = img.convert("RGBA").resize((size, size), Image.LANCZOS) buf = io.BytesIO() img.save(buf, format="PNG", optimize=True) return buf.getvalue() def main() -> None: if not SRC.exists(): sys.exit(f"missing {SRC}") src = Image.open(SRC) frames = [] for sz in DIB_SIZES: frames.append((sz, "dib", encode_dib(src, sz))) for sz in PNG_SIZES: frames.append((sz, "png", encode_png(src, sz))) out = bytearray() # ICONDIR: reserved(2) + type=1(2) + count(2) out += struct.pack("