#!/usr/bin/env python3 # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT """iconBake.py - a program's icon, from one picture, for each system that shows programs by an icon. The picture is a PNG (or anything PIL reads), best square, with transparency; it is fitted into each icon's size keeping its shape, and centred. A picture of 64 pixels or less is taken for pixel art and enlarged by whole pixels; anything else is resampled smoothly. Where an icon has few colours it takes the nearest (the 8-bit Windows icon dithers to its own 256), and a pixel at least half transparent is transparent. iconBake.py ico PICTURE OUT.ico Windows 95 on (Win32): 16, 32 and 48 pixels, each in 256 colours and in 32-bit iconBake.py ico16 PICTURE OUT.ico Windows 3.x (Win16): 32x32 in the 16 VGA colours iconBake.py bmp PICTURE OUT.bmp DVX: 32x32, 24-bit, on DVX's grey iconBake.py png PICTURE OUT.png WxH a picture WxH (the PSP's 144x80 ICON0, a web page's icon) iconBake.py info PICTURE OUT.info STACK the Amiga's Workbench: a tool's icon in its four colours, STACK bytes of stack when started from it iconBake.py be PICTURE OUT32 OUT16 OUT.hvif BeOS and Haiku: 32x32 and 16x16 in the BeOS palette, raw, for xres (ICON, MICN), and the 32x32 one as Haiku's vector icon (VICN) iconBake.py c PICTURE OUT.c SIZE the SDL3 backend's window icon: SIZE x SIZE RGBA as C iconBake.py icns PICTURE OUT.icns macOS: an app's icon, 16 pixels up to 512 (or the picture's size), and twice that for Retina screens iconBake.py iigs PICTURE OUT NAME the Apple IIgs Finder: an icon file ($CA) giving the program NAME its icons, in the Finder's 16 colours """ import io import math import struct import sys from PIL import Image # A picture this small or smaller is pixel art: enlarged by whole pixels. PIXEL_ART_MAX = 64 ALPHA_OPAQUE_MIN = 128 # Windows: the sizes Win32 icons come in, and the 16 colours of a Windows # 3.x icon (the VGA's, in Windows' order). ICO_SIZES = (16, 32, 48) ICO16_SIZE = 32 VGA16 = ( (0, 0, 0), (128, 0, 0), (0, 128, 0), (128, 128, 0), (0, 0, 128), (128, 0, 128), (0, 128, 128), (192, 192, 192), (128, 128, 128), (255, 0, 0), (0, 255, 0), (255, 255, 0), (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255), ) ICO_TYPE = 1 BITMAPINFOHEADER_BYTES = 40 ICONDIR_BYTES = 6 ICONDIRENTRY_BYTES = 16 # DVX: its icons' size and the grey they sit on (its own apps'). DVX_SIZE = 32 DVX_GREY = (192, 192, 192) # The Amiga: a Workbench tool icon (struct DiskObject, workbench.h) of # AMIGA_W x AMIGA_H Workbench pixels, which are twice as tall as wide, in # the four colours Workbench 2.0 on has (grey, black, white, blue). AMIGA_W = 48 AMIGA_H = 24 AMIGA_DEPTH = 2 AMIGA_COLOURS = ((170, 170, 170), (0, 0, 0), (255, 255, 255), (102, 136, 187)) WB_DISKMAGIC = 0xE310 WB_DISKVERSION = 1 WB_DISKREVISION = 1 WBTOOL = 3 NO_ICON_POSITION = 0x80000000 GFLG_GADGIMAGE = 0x0004 GACT_RELVERIFY_IMMEDIATE = 0x0003 GTYP_BOOLGADGET = 0x0001 # Any non-zero pointer: on disk it says the structure follows. PRESENT = 1 # BeOS and Haiku: 32x32 and 16x16 icons in the system palette (Haiku's # headers/private/interface/Palette.h, MIT; index 255 is transparent, # B_TRANSPARENT_MAGIC_CMAP8). BE_LARGE = 32 BE_SMALL = 16 BE_TRANSPARENT = 255 # Haiku's vector icon (HVIF, src/libs/icon/flat_icon in Haiku): the 16x16 # and 32x32 icons' pixels as outlines on its 64-unit grid, a shape a colour, # hinted so every edge falls on a pixel. The 16x16 shapes show below 32 # pixels and the 32x32 ones from there up (scale 0.5 is 32 pixels; a level of # detail byte is HVIF_LOD_UNITS to 1), so each size draws exactly its own # pixels. A section holds at most 255 of anything; an icon needing more gets # no vector icon. HVIF_MAGIC = b"ncif" HVIF_GRID = 64 HVIF_STYLE_SOLID_COLOR = 1 HVIF_PATH_CLOSED = 0x02 HVIF_PATH_NO_CURVES = 0x08 HVIF_SHAPE_PATH_SOURCE = 10 HVIF_SHAPE_HINTING = 0x04 HVIF_SHAPE_LOD_SCALE = 0x08 HVIF_LOD_UNITS = 63.75 HVIF_LOD_SMALL_MAX = 31 # just under scale 0.5, 32 pixels HVIF_LOD_LARGE_MIN = 31 HVIF_LOD_LARGE_MAX = 255 # scale 4 HVIF_COORD_BIAS = 32 HVIF_MAX_ITEMS = 255 HVIF_OPAQUE = 255 BE_PALETTE = ( 0, 0, 0, 8, 8, 8, 16, 16, 16, 24, 24, 24, 32, 32, 32, 40, 40, 40, 48, 48, 48, 56, 56, 56, 64, 64, 64, 72, 72, 72, 80, 80, 80, 88, 88, 88, 96, 96, 96, 104, 104, 104, 112, 112, 112, 120, 120, 120, 128, 128, 128, 136, 136, 136, 144, 144, 144, 152, 152, 152, 160, 160, 160, 168, 168, 168, 176, 176, 176, 184, 184, 184, 192, 192, 192, 200, 200, 200, 208, 208, 208, 216, 216, 216, 224, 224, 224, 232, 232, 232, 240, 240, 240, 248, 248, 248, 0, 0, 255, 0, 0, 229, 0, 0, 204, 0, 0, 179, 0, 0, 154, 0, 0, 129, 0, 0, 105, 0, 0, 80, 0, 0, 55, 0, 0, 30, 255, 0, 0, 228, 0, 0, 203, 0, 0, 178, 0, 0, 153, 0, 0, 128, 0, 0, 105, 0, 0, 80, 0, 0, 55, 0, 0, 30, 0, 0, 0, 255, 0, 0, 228, 0, 0, 203, 0, 0, 178, 0, 0, 153, 0, 0, 128, 0, 0, 105, 0, 0, 80, 0, 0, 55, 0, 0, 30, 0, 0, 152, 51, 255, 255, 255, 203, 255, 255, 203, 255, 203, 203, 255, 152, 203, 255, 102, 203, 255, 51, 203, 255, 0, 152, 255, 255, 152, 255, 203, 152, 255, 152, 152, 255, 102, 152, 255, 51, 152, 255, 0, 102, 255, 255, 102, 255, 203, 102, 255, 152, 102, 255, 102, 102, 255, 51, 102, 255, 0, 51, 255, 255, 51, 255, 203, 51, 255, 152, 51, 255, 102, 51, 255, 51, 51, 255, 0, 255, 152, 255, 255, 152, 203, 255, 152, 152, 255, 152, 102, 255, 152, 51, 255, 152, 0, 0, 102, 255, 0, 102, 203, 203, 203, 255, 203, 203, 203, 203, 203, 152, 203, 203, 102, 203, 203, 51, 203, 203, 0, 152, 203, 255, 152, 203, 203, 152, 203, 152, 152, 203, 102, 152, 203, 51, 152, 203, 0, 102, 203, 255, 102, 203, 203, 102, 203, 152, 102, 203, 102, 102, 203, 51, 102, 203, 0, 51, 203, 255, 51, 203, 203, 51, 203, 152, 51, 203, 102, 51, 203, 51, 51, 203, 0, 255, 102, 255, 255, 102, 203, 255, 102, 152, 255, 102, 102, 255, 102, 51, 255, 102, 0, 0, 102, 152, 0, 102, 102, 203, 152, 255, 203, 152, 203, 203, 152, 152, 203, 152, 102, 203, 152, 51, 203, 152, 0, 152, 152, 255, 152, 152, 203, 152, 152, 152, 152, 152, 102, 152, 152, 51, 152, 152, 0, 102, 152, 255, 102, 152, 203, 102, 152, 152, 102, 152, 102, 102, 152, 51, 102, 152, 0, 51, 152, 255, 51, 152, 203, 51, 152, 152, 51, 152, 102, 51, 152, 51, 51, 152, 0, 230, 134, 0, 255, 51, 203, 255, 51, 152, 255, 51, 102, 255, 51, 51, 255, 51, 0, 0, 102, 51, 0, 102, 0, 203, 102, 255, 203, 102, 203, 203, 102, 152, 203, 102, 102, 203, 102, 51, 203, 102, 0, 152, 102, 255, 152, 102, 203, 152, 102, 152, 152, 102, 102, 152, 102, 51, 152, 102, 0, 102, 102, 255, 102, 102, 203, 102, 102, 152, 102, 102, 102, 102, 102, 51, 102, 102, 0, 51, 102, 255, 51, 102, 203, 51, 102, 152, 51, 102, 102, 51, 102, 51, 51, 102, 0, 255, 0, 255, 255, 0, 203, 255, 0, 152, 255, 0, 102, 255, 0, 51, 255, 175, 19, 0, 51, 255, 0, 51, 203, 203, 51, 255, 203, 51, 203, 203, 51, 152, 203, 51, 102, 203, 51, 51, 203, 51, 0, 152, 51, 255, 152, 51, 203, 152, 51, 152, 152, 51, 102, 152, 51, 51, 152, 51, 0, 102, 51, 255, 102, 51, 203, 102, 51, 152, 102, 51, 102, 102, 51, 51, 102, 51, 0, 51, 51, 255, 51, 51, 203, 51, 51, 152, 51, 51, 102, 51, 51, 51, 51, 51, 0, 255, 203, 102, 255, 203, 152, 255, 203, 203, 255, 203, 255, 0, 51, 152, 0, 51, 102, 0, 51, 51, 0, 51, 0, 203, 0, 255, 203, 0, 203, 203, 0, 152, 203, 0, 102, 203, 0, 51, 255, 227, 70, 152, 0, 255, 152, 0, 203, 152, 0, 152, 152, 0, 102, 152, 0, 51, 152, 0, 0, 102, 0, 255, 102, 0, 203, 102, 0, 152, 102, 0, 102, 102, 0, 51, 102, 0, 0, 51, 0, 255, 51, 0, 203, 51, 0, 152, 51, 0, 102, 51, 0, 51, 51, 0, 0, 255, 203, 51, 255, 203, 0, 255, 255, 0, 255, 255, 51, 255, 255, 102, 255, 255, 152, 255, 255, 203, 255, 255, 255, ) # macOS: an app's icon file (.icns) is "icns" and the file's length, then an # element for each size: its type, its length (these 8 bytes included) and # the picture as a PNG. The @2x types are for Retina screens, twice the # pixels of the size they stand for. Sizes past the first at least as big as # the picture add nothing macOS's own enlarging would not, so are left out, # unless it is pixel art, which fit() enlarges sharply. ICNS_MAGIC = b"icns" ICNS_HEADER = ">4sI" ICNS_ELEMENTS = ( (b"icp4", 16), (b"icp5", 32), (b"ic11", 32), (b"ic12", 64), (b"ic07", 128), (b"ic13", 256), (b"ic08", 256), (b"ic14", 512), (b"ic09", 512), ) # The Apple IIgs Finder: an icon file (file type $CA; Apple's File Type Note # $CA, "Finder Icons File") in a volume's Icons folder, holding one icon data # record that gives a GS/OS application (file type $B3) of the program's name # its icons, normal and small. Each is a QuickDraw II icon (Toolbox Reference # volume 2, QuickDraw II Auxiliary): iconType, iconSize (the image's bytes), # iconHeight, iconWidth, the image at 4 bits a pixel (rows of whole bytes, # the even pixel in the high nibble), then a mask of the same shape, non-zero # where the icon is opaque. They are colour icons (iconType bit 15), whose # colours the Finder leaves alone. The Finder runs in 640 mode, where # QuickDraw draws each 4-bit icon pixel as two screen pixels, its high two # bits then its low two, and in the Finder's palette the first of the pair # is black, red, green, or white and the second black, blue, yellow, or white # (IIGS_FIRST, IIGS_SECOND: measured in MAME, an icon at an even screen x), # so an icon pixel is one of 16 colours a pair makes, seen as their mean. An # icon pixel is some 1.2 times as tall as wide; the big icon is the size of # the Finder's own (its folder is 24 pixels wide). IIGS_BIG = (24, 20) IIGS_SMALL = (12, 10) IIGS_PIXEL_ASPECT = 1.2 IIGS_FIRST = ((0, 0, 0), (255, 0, 0), (0, 255, 0), (255, 255, 255)) IIGS_SECOND = ((0, 0, 0), (0, 0, 255), (255, 255, 0), (255, 255, 255)) IIGS_HALF_BITS = 2 IIGS_HALF_MASK = 3 IIGS_BLACK = 0x0 IIGS_WHITE = 0xF IIGS_OPAQUE = 0xF IIGS_ICON_TYPE_COLOUR = 0x8000 IIGS_ICON_FILE_ID = 0x0001 IIGS_APPLICATION_TYPE = 0x00B3 IIGS_FILE_NAME_BYTES = 16 IIGS_BOSS_BYTES = 64 IIGS_NAME_BYTES = 16 # A pixel with less colour than this (CIELAB chroma) may be shown in a grey; # a more colourful one keeps a colour, or black or white: brown fur nearest # a grey by lightness reads better in the nearest colour. IIGS_GREY_CHROMA = 12 # CIE94 (graphic arts): how much a difference in chroma and in hue counts, # against the colour's own chroma. CIE94_CHROMA = 0.045 CIE94_HUE = 0.015 # sRGB to CIE XYZ (D65), and CIELAB's white point and curve. XYZ_FROM_RGB = ((0.4124, 0.3576, 0.1805), (0.2126, 0.7152, 0.0722), (0.0193, 0.1192, 0.9505)) LAB_WHITE = (0.95047, 1.0, 1.08883) LAB_EPSILON = 0.008856 LAB_KAPPA = 7.787 def fit(picture, w, h): """picture fitted into w x h, its shape kept, centred on transparency.""" pw, ph = picture.size scale = min(w / pw, h / ph) if max(pw, ph) <= PIXEL_ART_MAX and scale >= 2: scale = int(scale) sized = picture.resize((pw * scale, ph * scale), Image.NEAREST) else: sized = picture.resize((max(1, round(pw * scale)), max(1, round(ph * scale))), Image.LANCZOS) out = Image.new("RGBA", (w, h), (0, 0, 0, 0)) out.paste(sized, ((w - sized.size[0]) // 2, (h - sized.size[1]) // 2)) return out def opaque(image): """Each pixel's opacity: True where it is at least half opaque.""" return [a >= ALPHA_OPAQUE_MIN for a in image.getchannel("A").getdata()] def toColours(image, colours, dither, background): """The image's pixels as indices into colours: over background where transparent, dithered (Floyd-Steinberg) or the nearest.""" flat = Image.new("RGBA", image.size, background + (255,)) flat.alpha_composite(image) palette = Image.new("P", (1, 1)) entries = [c for rgb in colours for c in rgb] palette.putpalette(entries + entries[:3] * (256 - len(colours))) mode = Image.Dither.FLOYDSTEINBERG if dither else Image.Dither.NONE return list(flat.convert("RGB").quantize(palette=palette, dither=mode).getdata()) def rows(values, w, h, bottomUp): """values (row after row) as a list of rows, last first if bottomUp.""" out = [values[y * w:(y + 1) * w] for y in range(h)] return out[::-1] if bottomUp else out def padTo(data, align): return data + bytes((-len(data)) % align) def andMask(mask, w, h): """A Windows icon's AND mask: 1 where transparent, bottom-up rows of 1 bit a pixel padded to DWORDs.""" out = b"" for row in rows(mask, w, h, True): bits = 0 data = bytearray() for x, isOpaque in enumerate(row): bits = (bits << 1) | (0 if isOpaque else 1) if x % 8 == 7: data.append(bits) bits = 0 if w % 8: data.append(bits << (8 - w % 8)) out += padTo(bytes(data), 4) return out def icoImage(image, bitCount, colours): """One icon image: its BITMAPINFOHEADER, colour table, XOR bits and AND mask. bitCount 32: BGRA; 4 or 8: indices into colours.""" w, h = image.size mask = opaque(image) if bitCount == 32: # Black where wholly transparent: a system that draws through the # AND mask XORs this in. xor = b"" for row in rows(list(image.getdata()), w, h, True): xor += b"".join(struct.pack("> (x % 16) planes += b"".join(struct.pack(">H", word) for word in words) gadget = struct.pack(">IhhhhHHHIIIiIHI", 0, 0, 0, AMIGA_W, AMIGA_H + 1, GFLG_GADGIMAGE, GACT_RELVERIFY_IMMEDIATE, GTYP_BOOLGADGET, PRESENT, 0, 0, 0, 0, 0, WB_DISKREVISION) header = struct.pack(">HH", WB_DISKMAGIC, WB_DISKVERSION) + gadget + struct.pack(">BBIIIIIII", WBTOOL, 0, 0, 0, NO_ICON_POSITION, NO_ICON_POSITION, 0, 0, int(stack)) imageStruct = struct.pack(">hhhhhIBBI", 0, 0, AMIGA_W, AMIGA_H, AMIGA_DEPTH, PRESENT, (1 << AMIGA_DEPTH) - 1, 0, 0) open(out, "wb").write(header + imageStruct + planes) def bakeBe(picture, out32, out16, outVector): colours = tuple(BE_PALETTE[i:i + 3] for i in range(0, len(BE_PALETTE), 3)) lods = {BE_SMALL: (0, HVIF_LOD_SMALL_MAX), BE_LARGE: (HVIF_LOD_LARGE_MIN, HVIF_LOD_LARGE_MAX)} sizes = [] for size, out in ((BE_LARGE, out32), (BE_SMALL, out16)): image = fit(picture, size, size) indices = toColours(image, colours[:BE_TRANSPARENT], False, (0, 0, 0)) pixels = bytes(i if isOpaque else BE_TRANSPARENT for i, isOpaque in zip(indices, opaque(image))) open(out, "wb").write(pixels) sizes.append((size, pixels, colours) + lods[size]) vector = hvif(sizes) if vector is None: sys.stderr.write("iconBake.py: the icon needs more than %d paths or shapes; no vector icon\n" % HVIF_MAX_ITEMS) vector = b"" open(outVector, "wb").write(vector) def hvifContours(pixels, size, colour): """The outlines of colour's pixels in the size x size palette pixels, as closed loops of corner points: each pixel side with another colour (or the edge) beyond it, directed so the colour is on its right, chained head to tail. Outer edges come out clockwise and holes the other way, so a fill under either rule leaves the holes empty.""" def at(x, y): return 0 <= x < size and 0 <= y < size and pixels[y * size + x] == colour edges = {} for y in range(size): for x in range(size): if not at(x, y): continue if not at(x, y - 1): edges.setdefault((x, y), []).append((x + 1, y)) if not at(x + 1, y): edges.setdefault((x + 1, y), []).append((x + 1, y + 1)) if not at(x, y + 1): edges.setdefault((x + 1, y + 1), []).append((x, y + 1)) if not at(x - 1, y): edges.setdefault((x, y + 1), []).append((x, y)) loops = [] while edges: first = next(iter(edges)) loop = [first] point = first while True: ahead = edges[point].pop() if not edges[point]: del edges[point] if ahead == first: break loop.append(ahead) point = ahead # The corners alone: drop each point on a straight run. corners = [loop[i] for i in range(len(loop)) if (loop[i - 1][0] == loop[i][0]) != (loop[i][0] == loop[(i + 1) % len(loop)][0])] loops.append(corners) return loops def hvif(sizes): """An HVIF of the palette pictures in sizes ((size, pixels, colours, lodMin, lodMax), BE_TRANSPARENT clear): each colour's outlines a closed path each, a shape a colour and size, hinted, and shown only at the scales [lodMin, lodMax] (HVIF_LOD_UNITS a unit of icon scale, 64 pixels being 1). None when that is more of anything than HVIF holds.""" styles = [] paths = [] shapes = [] for size, pixels, colours, lodMin, lodMax in sizes: units = HVIF_GRID // size for colour in sorted(set(pixels) - {BE_TRANSPARENT}): if colours[colour] not in styles: styles.append(colours[colour]) first = len(paths) for loop in hvifContours(pixels, size, colour): if len(loop) > HVIF_MAX_ITEMS: return None paths.append(bytes((HVIF_PATH_CLOSED | HVIF_PATH_NO_CURVES, len(loop))) + bytes(HVIF_COORD_BIAS + v * units for point in loop for v in point)) shapes.append(bytes((HVIF_SHAPE_PATH_SOURCE, styles.index(colours[colour]), len(paths) - first)) + bytes(range(first, len(paths))) + bytes((HVIF_SHAPE_HINTING | HVIF_SHAPE_LOD_SCALE, lodMin, lodMax))) if max(len(styles), len(paths), len(shapes)) > HVIF_MAX_ITEMS: return None out = bytearray(HVIF_MAGIC) out.append(len(styles)) for colour in styles: out += bytes((HVIF_STYLE_SOLID_COLOR,) + tuple(colour) + (HVIF_OPAQUE,)) out.append(len(paths)) for path in paths: out += path out.append(len(shapes)) for shape in shapes: out += shape return bytes(out) def bakeC(picture, out, size): size = int(size) image = fit(picture, size, size) data = bytes(c for pixel in image.getdata() for c in pixel) lines = [] for i in range(0, len(data), 16): lines.append(" " + ", ".join("0x%02X" % b for b in data[i:i + 16]) + ",") text = ("// Generated by tools/iconBake.py: the program's window icon, %d x %d\n" "// RGBA pixels, row after row (src/hosted/sdl3/sdl3Backend.c).\n\n" "#include \n\n" "const uint16_t jlgHostedIconSize = %du;\n" "const uint8_t jlgHostedIconRgba[] = {\n%s\n};\n") % (size, size, size, "\n".join(lines)) open(out, "w").write(text) def bakeIcns(picture, out): sizes = sorted({size for _, size in ICNS_ELEMENTS}) largest = sizes[-1] if max(picture.size) > PIXEL_ART_MAX: largest = next((size for size in sizes if size >= max(picture.size)), largest) pngs = {} elements = b"" for kind, size in ICNS_ELEMENTS: if size > largest: continue if size not in pngs: data = io.BytesIO() fit(picture, size, size).save(data, "PNG") pngs[size] = data.getvalue() elements += struct.pack(ICNS_HEADER, kind, struct.calcsize(ICNS_HEADER) + len(pngs[size])) + pngs[size] open(out, "wb").write(struct.pack(ICNS_HEADER, ICNS_MAGIC, struct.calcsize(ICNS_HEADER) + len(elements)) + elements) def pascal(text, size): """text as a Pascal string (a length byte first) in size bytes.""" data = text.encode("ascii")[:size - 1] return bytes((len(data),)) + data + bytes(size - 1 - len(data)) def linear(value): """An sRGB channel (0-255) as linear light, 0-1.""" value /= 255 return value / 12.92 if value <= 0.04045 else ((value + 0.055) / 1.055) ** 2.4 def lab(rgb): """An sRGB colour in CIELAB.""" channels = [linear(v) for v in rgb] xyz = [sum(m * c for m, c in zip(row, channels)) / white for row, white in zip(XYZ_FROM_RGB, LAB_WHITE)] f = [t ** (1 / 3) if t > LAB_EPSILON else LAB_KAPPA * t + 16 / 116 for t in xyz] return (116 * f[1] - 16, 500 * (f[0] - f[1]), 200 * (f[1] - f[2])) def chroma(colour): return math.hypot(colour[1], colour[2]) def cie94(reference, sample): """CIE94's colour difference of sample from reference (both CIELAB).""" c1 = chroma(reference) dc = c1 - chroma(sample) dh2 = max(0.0, (reference[1] - sample[1]) ** 2 + (reference[2] - sample[2]) ** 2 - dc ** 2) return math.sqrt((reference[0] - sample[0]) ** 2 + (dc / (1 + CIE94_CHROMA * c1)) ** 2 + dh2 / (1 + CIE94_HUE * c1) ** 2) def iigsColours(): """The 16 colours of a Finder icon pixel, each the mean of its pair, in CIELAB; and which of them a colourful pixel may take.""" colours = [] for value in range(16): first = IIGS_FIRST[value >> IIGS_HALF_BITS] second = IIGS_SECOND[value & IIGS_HALF_MASK] colours.append(lab(tuple((a + b) / 2 for a, b in zip(first, second)))) keeps = [v for v in range(16) if v in (IIGS_BLACK, IIGS_WHITE) or chroma(colours[v]) >= IIGS_GREY_CHROMA] return colours, keeps def iigsIcon(picture, w, h): """A QuickDraw II colour icon of the picture at w x h icon pixels: black on its edge, within it the nearest of the Finder's 16 colours.""" image = fit(picture, w, round(h * IIGS_PIXEL_ASPECT)).resize((w, h), Image.LANCZOS) mask = opaque(image) rgb = list(image.convert("RGB").getdata()) colours, keeps = iigsColours() def inside(x, y): return 0 <= x < w and 0 <= y < h and mask[y * w + x] rowBytes = (w + 1) // 2 pixels = bytearray(rowBytes * h) shape = bytearray(rowBytes * h) for y in range(h): for x in range(w): if not mask[y * w + x]: continue edge = not all(inside(x + dx, y + dy) for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1))) if edge: value = IIGS_BLACK else: colour = lab(rgb[y * w + x]) pool = range(16) if chroma(colour) < IIGS_GREY_CHROMA else keeps value = min(pool, key=lambda v: cie94(colour, colours[v])) shift = 4 if x % 2 == 0 else 0 pixels[y * rowBytes + x // 2] |= value << shift shape[y * rowBytes + x // 2] |= IIGS_OPAQUE << shift return struct.pack("