joeylib2/tools/iconBake.py

463 lines
21 KiB
Python
Executable file

#!/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 BeOS and Haiku: 32x32 and
16x16 in the BeOS palette,
raw, for xres (ICON, MICN)
iconBake.py c PICTURE OUT.c SIZE the SDL3 backend's window
icon: SIZE x SIZE RGBA as C
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 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
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,
)
# 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("<BBBB", b, g, r, a) if a else bytes(4) for r, g, b, a in row)
table = b""
else:
# Index 0, black, where transparent (the 16 VGA colours start with
# black too).
indices = toColours(image, colours, bitCount == 8, (0, 0, 0))
indices = [i if isOpaque else 0 for i, isOpaque in zip(indices, mask)]
xor = b""
for row in rows(indices, w, h, True):
if bitCount == 4:
data = bytes((row[x] << 4) | (row[x + 1] if x + 1 < w else 0) for x in range(0, w, 2))
else:
data = bytes(row)
xor += padTo(data, 4)
table = b"".join(struct.pack("<BBBB", b, g, r, 0) for r, g, b in colours)
andBits = andMask(mask, w, h)
header = struct.pack("<IiiHHIIiiII", BITMAPINFOHEADER_BYTES, w, h * 2, 1, bitCount, 0, len(xor) + len(andBits), 0, 0, len(colours) if bitCount <= 8 else 0, 0)
return header + table + xor + andBits
def icoFile(images):
"""images: (width, height, colourCount, bitCount, data) into a .ico."""
out = struct.pack("<HHH", 0, ICO_TYPE, len(images))
offset = ICONDIR_BYTES + ICONDIRENTRY_BYTES * len(images)
for w, h, count, bitCount, data in images:
out += struct.pack("<BBBBHHII", w % 256, h % 256, count % 256, 0, 1, bitCount, len(data), offset)
offset += len(data)
return out + b"".join(image[4] for image in images)
def medianCut(image):
"""256 colours for an 8-bit icon of image: black (where transparent:
the AND mask's partner), then its own colours, cut down."""
flat = Image.new("RGBA", image.size, (0, 0, 0, 255))
flat.alpha_composite(image)
palette = flat.convert("RGB").quantize(colors=255, method=Image.Quantize.MEDIANCUT).getpalette()[:255 * 3]
palette = [0, 0, 0] + palette + [0] * (255 * 3 - len(palette))
return tuple(tuple(palette[i:i + 3]) for i in range(0, 256 * 3, 3))
def bakeIco(picture, out):
images = []
for size in ICO_SIZES:
image = fit(picture, size, size)
images.append((size, size, 0, 8, icoImage(image, 8, medianCut(image))))
images.append((size, size, 0, 32, icoImage(image, 32, None)))
open(out, "wb").write(icoFile(images))
def bakeIco16(picture, out):
image = fit(picture, ICO16_SIZE, ICO16_SIZE)
open(out, "wb").write(icoFile([(ICO16_SIZE, ICO16_SIZE, len(VGA16), 4, icoImage(image, 4, VGA16))]))
def bakeBmp(picture, out):
flat = Image.new("RGBA", (DVX_SIZE, DVX_SIZE), DVX_GREY + (255,))
flat.alpha_composite(fit(picture, DVX_SIZE, DVX_SIZE))
flat.convert("RGB").save(out, "BMP")
def bakePng(picture, out, size):
w, h = (int(v) for v in size.lower().split("x"))
fit(picture, w, h).save(out, "PNG")
def bakeInfo(picture, out, stack):
"""A WBTOOL DiskObject: the header, its gadget, the image (struct Image
and its planes, colour 0 where transparent) and nothing more."""
image = fit(picture, AMIGA_W, AMIGA_H * 2).resize((AMIGA_W, AMIGA_H), Image.LANCZOS)
mask = opaque(image)
# Workbench's grey is the background alone: the picture is drawn in the
# other three.
figure = toColours(image, AMIGA_COLOURS[1:], False, (255, 255, 255))
indices = [i + 1 if isOpaque else 0 for i, isOpaque in zip(figure, mask)]
rowWords = (AMIGA_W + 15) // 16
planes = b""
for plane in range(AMIGA_DEPTH):
for row in rows(indices, AMIGA_W, AMIGA_H, False):
words = [0] * rowWords
for x, index in enumerate(row):
if index & (1 << plane):
words[x // 16] |= 0x8000 >> (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):
colours = tuple(BE_PALETTE[i:i + 3] for i in range(0, len(BE_PALETTE), 3))
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))
open(out, "wb").write(bytes(i if isOpaque else BE_TRANSPARENT for i, isOpaque in zip(indices, opaque(image))))
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 <stdint.h>\n\n"
"const uint16_t gHostedIconSize = %du;\n"
"const uint8_t gHostedIconRgba[] = {\n%s\n};\n") % (size, size, size, "\n".join(lines))
open(out, "w").write(text)
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("<HHHH", IIGS_ICON_TYPE_COLOUR, len(pixels), h, w) + bytes(pixels) + bytes(shape)
def bakeIigs(picture, out, name):
icons = iigsIcon(picture, *IIGS_BIG) + iigsIcon(picture, *IIGS_SMALL)
record = pascal("", IIGS_BOSS_BYTES) + pascal(name, IIGS_NAME_BYTES) + struct.pack("<HH", IIGS_APPLICATION_TYPE, 0) + icons
header = struct.pack("<IHI", 0, IIGS_ICON_FILE_ID, 0) + pascal(name, IIGS_FILE_NAME_BYTES)
# The record's length counts its own length word; a zero length ends the
# list.
open(out, "wb").write(header + struct.pack("<H", 2 + len(record)) + record + struct.pack("<H", 0))
BAKES = {
"ico": (bakeIco, 0),
"ico16": (bakeIco16, 0),
"bmp": (bakeBmp, 0),
"png": (bakePng, 1),
"info": (bakeInfo, 1),
"be": (bakeBe, 1),
"c": (bakeC, 1),
"iigs": (bakeIigs, 1),
}
def main(argv):
if len(argv) < 4 or argv[1] not in BAKES or len(argv) != 4 + BAKES[argv[1]][1]:
sys.stderr.write(__doc__)
return 2
picture = Image.open(argv[2]).convert("RGBA")
BAKES[argv[1]][0](picture, *argv[3:])
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))