#!/usr/bin/env python3 """make_pixels.py -- small pixel art for morel.pleroma.nexus Python standard library only. Every sprite is an explicit grid of characters (see the big string blocks below); edit a character, rerun, and the picture changes. The script writes its outputs next to itself: button.gif 88x31 animated web button (GIF89a, own LZW) button.png 88x31 still frame of the same button favicon.png 32x32 apple-touch-icon.png 180x180 (a 36x36 drawing shown at 5x) favicon.ico 16x16 + 32x32 (PNG-in-ICO) stamp-bee.png, stamp-comb.png, stamp-morel.png 32x32, transparent contact-sheet.png everything at 4x for review verify-report.txt pass/fail table, sizes, and an ASCII dump of a frame Then it re-reads every file it wrote with independent decoders and prints a pass/fail table. Exit status is 1 if any check fails. Run: python3 make_pixels.py """ import math import os import struct import sys import zlib OUT = os.path.dirname(os.path.abspath(__file__)) # -------------------------------------------------------------------------- # palette: one character per colour # -------------------------------------------------------------------------- PAL = { "n": (0x0B, 0x12, 0x24), # night "d": (0x18, 0x2C, 0x3E), # dusk "t": (0x7F, 0xB8, 0xC4), # teal "h": (0xE6, 0xC2, 0x7A), # honey "c": (0xE9, 0xE2, 0xCF), # cream "b": (0x4A, 0x2F, 0x1A), # dark brown: pits, bee stripes "m": (0xB9, 0x8A, 0x45), # honey-brown: shaded side of the cap, honey in cells "s": (0x4A, 0x6A, 0x80), # dim star "u": (0x8A, 0x5A, 0x2B), # bark brown: bee head "e": (0xBF, 0xAE, 0x8C), # shaded side of the stem } GIF_ORDER = "ndthcbmsue" # palette index = position in this string # '.' in a grid means "nothing here": transparent in stamps, background in # opaque pictures. # -------------------------------------------------------------------------- # sprites # -------------------------------------------------------------------------- # the button's morel: 13 wide, 22 tall. Pointed top, widest low down. BUTTON_MOREL = [ "......h......", ".....hhh.....", ".....hbh.....", "....hhhhm....", "....hbhhm....", "...hhhhbhm...", "...hbhhhhm...", "..hhhhbhhmm..", "..hbhhhhbhm..", "..hhhbhhhhm..", ".hhhhhhbhhmm.", ".hbhhbhhhbhm.", ".hhhhhhhhhmm.", ".hhbhhhbhhmm.", "..hhhhbhhmm..", "...mmmmmmm...", "....cccce....", "....cccce....", "....cccce....", "....cccce....", "...cccccee...", "...cccccee...", ] # the button's lettering: 2px strokes, x-height 10, the l is 14 tall. # '#' is ink. All glyphs sit on the same baseline (bottom row). FONT = { "m": [ "##.###..###.", "############", "###..###..##", "##...##...##", "##...##...##", "##...##...##", "##...##...##", "##...##...##", "##...##...##", "##...##...##", ], "o": [ "..####..", ".######.", "###..###", "##....##", "##....##", "##....##", "##....##", "###..###", ".######.", "..####..", ], "r": [ "##.###.", "#######", "###..##", "##.....", "##.....", "##.....", "##.....", "##.....", "##.....", "##.....", ], "e": [ "..####..", ".######.", "###..###", "##....##", "########", "########", "##......", "###...##", ".######.", "..####..", ], "l": [ "##.", "##.", "##.", "##.", "##.", "##.", "##.", "##.", "##.", "##.", "##.", "##.", "###", ".##", ], } # gap (in pixels) after each letter of "morel"; r has air on its right already WORD = [("m", 2), ("o", 2), ("r", 1), ("e", 2), ("l", 0)] # the button's bee, facing right, two wing positions. 7 wide, 5 tall. BEE_UP = [ "..cc...", ".ccc...", ".hbhbh.", "hhbhbhu", ".hbhbh.", ] BEE_FLAT = [ ".......", ".cccc..", ".hbhbh.", "hhbhbhu", ".hbhbh.", ] # favicon, 16x16. '.' is night here. FAV16 = [ "................", ".......hh.......", "..c...hhhm......", "......hbhm......", ".....hhhhhm.....", ".....hbhbhm..t..", "....hhhhhhhm....", "....hbhhbhhm....", "....hhhbhhbm....", "....hhbhhhmm....", ".....hhhhmm.....", "......ccce......", "......ccce......", "......ccce......", ".....ccccee.....", "dddddddddddddddd", ] # a morel for 32x32: rows are written without their side margins and are # centred by the script. 18 wide at the widest, 28 tall. MOREL_28 = [ "hh", "hhhm", "hhbm", "hhhhhm", "hbbhhm", "hhbhbhhm", "hhhhbbhm", "hbhhhhhhmm", "hbbhhbbhhm", "hhhhhhbhhhmm", "hhhbhhhhbhhm", "hhhbbhhhbbhhmm", "hbhhhhbhhhhhhm", "hbbhhbbhhhbbhhmm", "hhhhhhhhbhhbhhhm", "hhhbhhhhbbhhhhmm", "hhhhbbhhhhhhhbbhmm", "hhbhhbhhhbbhhhbhmm", "hhhhhhhbhhbhhhmm", "hhhbbhhhhhhhmm", "mmmmmmmmmm", "ccccccee", "ccccccee", "ccccccee", "ccccccee", "ccccccee", "ccccccceee", "ccccccceee", ] FAV32_STARS = [(4, 4, "c"), (26, 3, "t"), (28, 12, "s"), (3, 15, "s"), (6, 22, "t"), (25, 20, "c"), (9, 9, "s"), (23, 8, "s")] # a morel for the 180px icon, drawn on a 36x36 grid (each cell shown as 5x5). # 20 wide at the widest, 31 tall; more pits than the 32px one. MOREL_31 = [ "hh", "hhhm", "hbbm", "hhbhhm", "hhhhbm", "hbbhhbhm", "hhbhhhhm", "hhhhbbhhhm", "hbhhhbhhbm", "hbbhhhhhbbhm", "hhbhhbbhhbhm", "hhhhhhbbhhhhmm", "hhbbhhhhhhbbhm", "hhhbbhhhbbhhbhmm", "hbhhhhhhhbbhhhhm", "hbbhhhbbhhhhhbbhmm", "hhbbhhhbbhhbhhbbhm", "hhhhhhbhhhhbbhhhhhmm", "hhbbhhhhhbhhbbhhbbhm", "hhhbbhhbbhhhhbhhhbmm", "hhhhhhhhbbhhbbhhhhmm", "hhhbbhhhhhhhhbbhmm", "hhhbhhhbbhhhhhmm", "mmmmmmmmmmmm", "ccccccee", "ccccccee", "ccccccee", "ccccccee", "ccccccee", "ccccccceee", "ccccccceee", ] APPLE_STARS = [(5, 5, "c"), (30, 4, "t"), (27, 13, "s"), (4, 13, "s"), (7, 22, "t"), (31, 21, "c"), (10, 9, "s"), (24, 3, "s"), (3, 28, "s"), (32, 28, "s"), (29, 17, "s"), (6, 17, "c")] APPLE_BEE_AT = (26, 7) # the little button bee, top right # stamp: bee, facing right. '.' is transparent. STAMP_BEE = [ "......ccc....cc.....", ".....ccccc..cccc....", ".....cccccc.ccccc...", "......ccccctcccc...t", ".......cccctccc...t.", "....hhbbhhbbhhuu.t..", "...hhhbbhhbbhhuuuu..", ".mhhhhbbhhbbhhuubu..", "...hhhbbhhbbhhuuuu..", "....hhbbhhbbhhuu....", ".....m..m..m........", ] # stamp: seven honeycomb cells, three of them holding honey. 25 x 24. STAMP_COMB = [ "........h.......h........", "......hhmhh...hh.hh......", "....hhmmmmmhhh.....hh....", "....hmmcmmmmh.......h....", "....hmcmmmmmh.......h....", "....hmmmmmmmh.......h....", "....hmmmmmmmh.......h....", "....hhmmmmmhhh.....hh....", "..hh..hhmhhmmmhh.hh..hh..", "hh......hmmmmmmmh......hh", "h.......hmmcmmmmh.......h", "h.......hmcmmmmmh.......h", "h.......hmmmmmmmh.......h", "h.......hmmmmmmmh.......h", "hh.....hhhmmmmmhhh.....hh", "..hh.hh...hhmhh...hh.hh..", "....h.......h.......h....", "....h.......h.......h....", "....h.......h.......h....", "....h.......hmmmmmmmh....", "....h.......hmmmmmmmh....", "....hh.....hhhmmmmmhh....", "......hh.hh...hhmhh......", "........h.......h........", ] # 3x5 label font for the contact sheet only TINY = { "a": "010101111101101", "b": "110101110101110", "c": "011100100100011", "d": "110101101101110", "e": "111100110100111", "f": "111100110100100", "g": "011100101101011", "h": "101101111101101", "i": "111010010010111", "j": "001001001101010", "k": "101101110101101", "l": "100100100100111", "m": "101111111101101", "n": "110101101101101", "o": "010101101101010", "p": "110101110100100", "q": "010101101110011", "r": "110101110101101", "s": "011100010001110", "t": "111010010010010", "u": "101101101101111", "v": "101101101101010", "w": "101101111111101", "x": "101101010101101", "y": "101101010010010", "z": "111001010100111", "0": "111101101101111", "1": "010110010010111", "2": "110001010100111", "3": "110001010001110", "4": "101101111001001", "5": "111100110001110", "6": "011100111101111", "7": "111001010010010", "8": "111101111101111", "9": "111101111001110", ".": "000000000000010", "-": "000000111000000", ":": "000010000010000", "/": "001001010100100", " ": "000000000000000", } # -------------------------------------------------------------------------- # grid helpers. A "grid" is a list of lists of palette characters ('.' = empty) # -------------------------------------------------------------------------- def strict(rows, name): """Explicit grid: every row must be the same width.""" w = len(rows[0]) for i, r in enumerate(rows): if len(r) != w: raise SystemExit(f"{name}: row {i} is {len(r)} wide, expected {w}") for ch in r: if ch != "." and ch != "#" and ch not in PAL: raise SystemExit(f"{name}: row {i} has unknown colour {ch!r}") return [list(r) for r in rows] def centred(rows, name): """Rows written without margins: centre each one on the widest row.""" w = max(len(r) for r in rows) out = [] for i, r in enumerate(rows): if (w - len(r)) % 2: raise SystemExit(f"{name}: row {i} width {len(r)} has the wrong parity to centre on {w}") pad = (w - len(r)) // 2 out.append("." * pad + r + "." * pad) return strict(out, name) def blank(w, h, ch="."): return [[ch] * w for _ in range(h)] def paste(dst, src, x0, y0, clip=None): """Draw src onto dst; '.' cells in src are skipped. clip=(x1,y1,x2,y2) inclusive.""" H, W = len(dst), len(dst[0]) cx1, cy1, cx2, cy2 = clip if clip else (0, 0, W - 1, H - 1) for j, row in enumerate(src): y = y0 + j if y < cy1 or y > cy2: continue for i, ch in enumerate(row): x = x0 + i if ch == "." or x < cx1 or x > cx2: continue dst[y][x] = ch def centre_on(src, w, h, bg="."): g = blank(w, h, bg) paste(g, src, (w - len(src[0])) // 2, (h - len(src)) // 2) return g def scale_grid(g, k): out = [] for row in g: big = [ch for ch in row for _ in range(k)] out.extend([list(big) for _ in range(k)]) return out def to_rgba(g): """grid -> list of rows of (r,g,b,a)""" return [[(0, 0, 0, 0) if ch == "." else PAL[ch] + (255,) for ch in row] for row in g] # -------------------------------------------------------------------------- # the button # -------------------------------------------------------------------------- BW, BH = 88, 31 NFRAMES = 40 DELAY_CS = 12 # hundredths of a second per frame -> 4.8 s loop STILL_FRAME = 34 # which frame becomes button.png KEY_FRAMES = [3, 10, 17, 24, 31, 36] MOREL_AT = (4, 4) TEXT_X, TEXT_BASELINE = 26, 20 # baseline = y of the bottom row of the letters STATIC_STARS = [(22, 4, "c"), (34, 3, "s"), (45, 6, "t"), (59, 3, "c"), (66, 8, "s"), (73, 4, "s"), (82, 16, "t"), (75, 20, "s"), (20, 15, "s"), (3, 9, "s"), (18, 9, "t"), (84, 23, "s"), (22, 22, "s"), (73, 13, "s")] TWINKLE_A = (79, 5) # a plus-shaped star that flares once per loop TWINKLE_B = (52, 3) # a dot that dims twice per loop def word_grid(): h = max(len(FONT[c]) for c, _ in WORD) w = sum(len(FONT[c][0]) + gap for c, gap in WORD) g = blank(w, h) x = 0 for c, gap in WORD: glyph = strict(FONT[c], "font " + c) top = h - len(glyph) for j, row in enumerate(glyph): for i, ch in enumerate(row): if ch == "#": g[top + j][x + i] = "h" x += len(glyph[0]) + gap return g def bee_pos(i): x = -8 + (i * 100) // NFRAMES # 2.5 px a frame, left to right y = 10 + round(2 * math.sin(2 * math.pi * 2 * i / NFRAMES)) # two gentle bobs return x, y def button_frame(i): g = blank(BW, BH, "n") # ground: a dusk strip with one dithered row above it for y in range(26, BH): g[y] = ["d"] * BW for x in range(0, BW, 2): g[25][x] = "d" # stars for x, y, ch in STATIC_STARS: g[y][x] = ch ax, ay = TWINKLE_A g[ay][ax] = "c" arm = {16: "s", 17: "s", 18: "t", 19: "t", 20: "t", 21: "t", 22: "s", 23: "s"}.get(i) if arm: for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)): g[ay + dy][ax + dx] = arm bx, by = TWINKLE_B g[by][bx] = {0: "t", 1: "s", 2: "s", 3: "t"}.get(i % 20, "c") # the bee, in front of the stars, behind everything else x, y = bee_pos(i) bee = strict(BEE_UP if i % 2 == 0 else BEE_FLAT, "bee") paste(g, bee, x, y, clip=(1, 1, BW - 2, BH - 2)) # mushroom and lettering paste(g, strict(BUTTON_MOREL, "button morel"), *MOREL_AT) wg = word_grid() paste(g, wg, TEXT_X, TEXT_BASELINE - len(wg) + 1) # one-pixel teal border for x in range(BW): g[0][x] = g[BH - 1][x] = "t" for y in range(BH): g[y][0] = g[y][BW - 1] = "t" return g # -------------------------------------------------------------------------- # icons and stamps # -------------------------------------------------------------------------- def favicon16(): g = strict(FAV16, "fav16") return [[("n" if ch == "." else ch) for ch in row] for row in g] def favicon32(): g = blank(32, 32, "n") for y in range(29, 32): g[y] = ["d"] * 32 for x in range(0, 32, 2): g[28][x] = "d" for x, y, ch in FAV32_STARS: g[y][x] = ch m = centred(MOREL_28, "morel 28") paste(g, m, (32 - len(m[0])) // 2, 2) return g def apple_icon(): g = blank(36, 36, "n") for y in range(32, 36): g[y] = ["d"] * 36 for x in range(0, 36, 2): g[31][x] = "d" for x, y, ch in APPLE_STARS: g[y][x] = ch paste(g, strict(BEE_UP, "bee"), *APPLE_BEE_AT) m = centred(MOREL_31, "morel 31") paste(g, m, (36 - len(m[0])) // 2, 2) return scale_grid(g, 5) def stamp_bee(): return centre_on(strict(STAMP_BEE, "stamp bee"), 32, 32) def stamp_comb(): return centre_on(strict(STAMP_COMB, "stamp comb"), 32, 32) def stamp_morel(): g = centre_on(centred(MOREL_28, "morel 28"), 32, 32) # two blades of grass either side of the foot for x, y in ((9, 29), (8, 28), (10, 28), (22, 29), (23, 28), (21, 28)): g[y][x] = "t" return g # -------------------------------------------------------------------------- # encoders # -------------------------------------------------------------------------- def png_bytes(rows, alpha=True): """rows: list of rows of (r,g,b,a). 8-bit RGBA (or RGB), filter 0.""" h, w = len(rows), len(rows[0]) raw = bytearray() for row in rows: raw.append(0) for px in row: raw.extend(px if alpha else px[:3]) def chunk(tag, data): return (struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)) return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 6 if alpha else 2, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + chunk(b"IEND", b"")) def lzw_encode(indices, min_code_size): """GIF-flavoured LZW: variable-width codes packed least significant bit first.""" clear = 1 << min_code_size eoi = clear + 1 out = bytearray() acc = 0 nbits = 0 size = min_code_size + 1 nxt = eoi + 1 table = {} def emit(code): nonlocal acc, nbits acc |= code << nbits nbits += size while nbits >= 8: out.append(acc & 0xFF) acc >>= 8 nbits -= 8 emit(clear) prefix = indices[0] for k in indices[1:]: key = (prefix, k) if key in table: prefix = table[key] continue emit(prefix) if nxt < 4096: table[key] = nxt nxt += 1 if nxt > (1 << size) and size < 12: size += 1 else: emit(clear) table = {} nxt = eoi + 1 size = min_code_size + 1 prefix = k emit(prefix) emit(eoi) if nbits: out.append(acc & 0xFF) return bytes(out) def sub_blocks(data): out = bytearray() for i in range(0, len(data), 255): part = data[i:i + 255] out.append(len(part)) out.extend(part) out.append(0) return bytes(out) def gif_bytes(frames, w, h, palette, delay_cs): """frames: list of flat index lists. palette: list of (r,g,b), length a power of two.""" n = len(palette) bits = n.bit_length() - 1 assert 1 << bits == n and 2 <= bits <= 8 out = bytearray(b"GIF89a") out += struct.pack("I", data[pos:pos + 4]) tag = data[pos + 4:pos + 8] body = data[pos + 8:pos + 8 + ln] (crc,) = struct.unpack(">I", data[pos + 8 + ln:pos + 12 + ln]) if zlib.crc32(tag + body) & 0xFFFFFFFF != crc: raise ValueError("CRC mismatch in " + tag.decode("latin1")) if tag == b"IHDR": ihdr = struct.unpack(">IIBBBBB", body) elif tag == b"IDAT": idat += body elif tag == b"IEND": ended = True pos += 12 + ln if not ihdr or not ended: raise ValueError("missing IHDR or IEND") w, h, depth, ctype, comp, filt, lace = ihdr if depth != 8 or ctype not in (2, 6) or comp or filt or lace: raise ValueError("unexpected IHDR fields") ch = 4 if ctype == 6 else 3 raw = zlib.decompress(idat) stride = 1 + w * ch if len(raw) != stride * h: raise ValueError(f"inflated to {len(raw)} bytes, expected {stride * h}") rows = [] for y in range(h): line = raw[y * stride:(y + 1) * stride] if line[0] != 0: raise ValueError("unexpected filter type") rows.append([tuple(line[1 + x * ch:1 + (x + 1) * ch]) for x in range(w)]) return w, h, ch, rows def lzw_decode(data, min_code_size): """Returns (indices, saw_end_code).""" clear = 1 << min_code_size eoi = clear + 1 size = min_code_size + 1 table = {i: (i,) for i in range(clear)} nxt = eoi + 1 out = [] prev = None acc = 0 nbits = 0 pos = 0 while True: while nbits < size: if pos >= len(data): return out, False acc |= data[pos] << nbits pos += 1 nbits += 8 code = acc & ((1 << size) - 1) acc >>= size nbits -= size if code == clear: table = {i: (i,) for i in range(clear)} nxt = eoi + 1 size = min_code_size + 1 prev = None continue if code == eoi: return out, True if prev is None: if code >= clear: raise ValueError("first code is not a literal") entry = table[code] elif code < nxt and code in table: entry = table[code] elif code == nxt: entry = table[prev] + (table[prev][0],) else: raise ValueError("code out of range") out.extend(entry) if prev is not None and nxt < 4096: table[nxt] = table[prev] + (entry[0],) nxt += 1 if nxt == (1 << size) and size < 12: size += 1 prev = code def gif_parse(data): """Walk the whole file. Returns a dict; raises ValueError on a fault.""" if data[:6] != b"GIF89a": raise ValueError("bad header") w, h, packed, bg, aspect = struct.unpack(" len(data): raise ValueError("entry runs past end of file") entries.append((w, h, bpp, data[off:off + size])) return entries # -------------------------------------------------------------------------- # contact sheet # -------------------------------------------------------------------------- SHEET_BG = (5, 8, 16) class Canvas: def __init__(self, w, h, bg): self.w, self.h = w, h self.rows = [bytearray(bytes(bg) * w) for _ in range(h)] def rect(self, x, y, w, h, rgb): line = bytes(rgb) * w for j in range(y, y + h): self.rows[j][x * 3:(x + w) * 3] = line def blit(self, rgba_rows, x, y, k, under): """Nearest-neighbour at k times; transparent pixels show `under`.""" for j, row in enumerate(rgba_rows): line = b"".join(bytes(px[:3] if px[3] else under) * k for px in row) for jj in range(k): self.rows[y + j * k + jj][x * 3:x * 3 + len(line)] = line def text(self, s, x, y, rgb, k=2): for ch in s.lower(): bits = TINY.get(ch, TINY["-"]) for n, bit in enumerate(bits): if bit == "1": self.rect(x + (n % 3) * k, y + (n // 3) * k, k, k, rgb) x += 4 * k def to_rows(self): return [[tuple(r[i:i + 3]) + (255,) for i in range(0, len(r), 3)] for r in self.rows] def contact_sheet(items, width=1136, k=4): """items: list of (label, rgba_rows). Simple left-to-right flow layout.""" gap, label_h, margin = 16, 16, 16 # first pass: positions placed = [] x, y, row_h = margin, margin, 0 for label, rows in items: w, h = len(rows[0]) * k, len(rows) * k if x + w + margin > width and x > margin: x = margin y += row_h + gap row_h = 0 placed.append((label, rows, x, y)) x += max(w, len(label) * 8) + gap row_h = max(row_h, h + label_h + 2) height = y + row_h + margin c = Canvas(width, height, SHEET_BG) for label, rows, x, y in placed: w, h = len(rows[0]) * k, len(rows) * k c.text(label, x, y, PAL["t"]) c.rect(x - 1, y + label_h - 1, w + 2, h + 2, PAL["d"]) # frame shows the true edge c.blit(rows, x, y + label_h, k, PAL["n"]) return c # -------------------------------------------------------------------------- # main: build, write, verify # -------------------------------------------------------------------------- def write(name, data): with open(os.path.join(OUT, name), "wb") as f: f.write(data) def read(name): with open(os.path.join(OUT, name), "rb") as f: return f.read() def main(): results = [] # (file, check, ok, detail) def check(name, what, ok, detail=""): results.append((name, what, bool(ok), detail)) # ---- build ---- frames = [button_frame(i) for i in range(NFRAMES)] gif_pal = [PAL[c] for c in GIF_ORDER] while len(gif_pal) < 16: gif_pal.append(PAL["n"]) index_of = {c: i for i, c in enumerate(GIF_ORDER)} frame_idx = [[index_of[ch] for row in g for ch in row] for g in frames] pngs = { "button.png": to_rgba(frames[STILL_FRAME]), "favicon.png": to_rgba(favicon32()), "apple-touch-icon.png": to_rgba(apple_icon()), "stamp-bee.png": to_rgba(stamp_bee()), "stamp-comb.png": to_rgba(stamp_comb()), "stamp-morel.png": to_rgba(stamp_morel()), } expect = {"button.png": (88, 31), "favicon.png": (32, 32), "apple-touch-icon.png": (180, 180), "stamp-bee.png": (32, 32), "stamp-comb.png": (32, 32), "stamp-morel.png": (32, 32)} fav16 = to_rgba(favicon16()) items = [(f"gif frame {i}", to_rgba(frames[i])) for i in KEY_FRAMES] items += [("button.png", pngs["button.png"]), ("ico 16", fav16), ("favicon 32", pngs["favicon.png"]), ("stamp-bee", pngs["stamp-bee.png"]), ("stamp-comb", pngs["stamp-comb.png"]), ("stamp-morel", pngs["stamp-morel.png"]), ("apple-touch-icon 180", pngs["apple-touch-icon.png"])] sheet = contact_sheet(items) sheet_rows = sheet.to_rows() # ---- write ---- write("button.gif", gif_bytes(frame_idx, BW, BH, gif_pal, DELAY_CS)) for name, rows in pngs.items(): write(name, png_bytes(rows)) write("favicon.ico", ico_bytes([(16, 16, png_bytes(fav16)), (32, 32, png_bytes(pngs["favicon.png"]))])) write("contact-sheet.png", png_bytes(sheet_rows, alpha=False)) # ---- verify: LZW self-test on awkward data (forces table resets) ---- import random rnd = random.Random(5) for label, data in (("random 30000", [rnd.randrange(16) for _ in range(30000)]), ("all one value", [7] * 9000), ("single pixel", [3])): try: back, ended = lzw_decode(lzw_encode(data, 4), 4) check("(self-test)", "LZW round trip, " + label, back == data and ended) except Exception as e: # noqa check("(self-test)", "LZW round trip, " + label, False, repr(e)) # ---- verify: PNGs ---- sources = dict(pngs) expect["contact-sheet.png"] = (sheet.w, sheet.h) sources["contact-sheet.png"] = sheet_rows for name, (ew, eh) in expect.items(): try: w, h, ch, rows = png_decode(read(name)) check(name, "signature, chunk CRCs, inflate length", True) check(name, f"dimensions {ew}x{eh}", (w, h) == (ew, eh), f"got {w}x{h}") src = sources[name] same = all(tuple(p[:ch]) == q for rs, rd in zip(src, rows) for p, q in zip(rs, rd)) check(name, "pixels match the source grid", same and len(rows) == len(src)) if name.startswith("stamp-"): clear = sum(1 for r in rows for p in r if p[3] == 0) solid = sum(1 for r in rows for p in r if p[3] == 255) check(name, "has transparent and solid pixels only", clear > 0 and solid > 0 and clear + solid == w * h, f"{solid} solid, {clear} clear") except Exception as e: # noqa check(name, "decodes", False, repr(e)) # ---- verify: GIF ---- name = "button.gif" gdata = read(name) try: info = gif_parse(gdata) check(name, "header GIF89a", True) check(name, "logical screen 88x31", (info["w"], info["h"]) == (BW, BH)) check(name, "global palette, 16 entries, ours", info["palette"] == gif_pal) check(name, "loops forever (NETSCAPE2.0, count 0)", info["loop"] == 0) check(name, f"{NFRAMES} frames", len(info["frames"]) == NFRAMES, f"got {len(info['frames'])}") check(name, f"every delay is {DELAY_CS} cs", all(d == DELAY_CS for d in info["delays"])) check(name, "every frame is full size at 0,0", all((f["x"], f["y"], f["w"], f["h"]) == (0, 0, BW, BH) for f in info["frames"])) check(name, f"every LZW stream gives exactly {BW * BH} indices", all(len(f["indices"]) == BW * BH for f in info["frames"])) check(name, "every LZW stream ends with the end code", all(f["ended"] for f in info["frames"])) check(name, "every index is inside the palette", all(max(f["indices"]) < 16 for f in info["frames"])) check(name, "decoded frames match the source grids", [f["indices"] for f in info["frames"]] == frame_idx) check(name, "trailer present, nothing after it", info["trailer"] and info["trailing"] == 0) used = sorted({i for f in info["frames"] for i in f["indices"]}) check(name, "colours actually used", True, f"{len(used)} of 16") except Exception as e: # noqa check(name, "parses", False, repr(e)) check(name, "under 30 KB", len(gdata) < 30000, f"{len(gdata)} bytes") # ---- verify: ICO ---- name = "favicon.ico" try: entries = ico_parse(read(name)) check(name, "ICO header, 2 entries", len(entries) == 2) for (w, h, bpp, blob), want in zip(entries, (16, 32)): pw, ph, ch, rows = png_decode(blob) check(name, f"entry {want}x{want}: directory and embedded PNG agree", (w, h) == (want, want) == (pw, ph) and bpp == 32 and ch == 4) check(name, "32x32 entry is byte-identical to favicon.png", entries[1][3] == read("favicon.png")) except Exception as e: # noqa check(name, "parses", False, repr(e)) # ---- sizes ---- deliver = ["button.gif", "button.png", "favicon.png", "apple-touch-icon.png", "favicon.ico", "stamp-bee.png", "stamp-comb.png", "stamp-morel.png", "contact-sheet.png"] sizes = [(n, os.path.getsize(os.path.join(OUT, n))) for n in deliver] total = sum(s for _, s in sizes) check("(all files)", "total under 300 KB", total < 300000, f"{total} bytes") # ---- report ---- lines = [] lines.append("VERIFICATION") wf = max(len(r[0]) for r in results) wc = max(len(r[1]) for r in results) for f, what, ok, detail in results: lines.append(f"{'PASS' if ok else 'FAIL'} {f:<{wf}} {what:<{wc}} {detail}".rstrip()) npass = sum(1 for r in results if r[2]) lines.append(f"{npass} of {len(results)} checks passed") lines.append("") lines.append("FILE SIZES") for n, s in sizes: lines.append(f"{s:>7} bytes {n}") lines.append(f"{total:>7} bytes total") lines.append("") lines.append(f"BUTTON, FRAME {STILL_FRAME} (this is also button.png)") lines.append("key: space=night ,=dusk T=teal H=honey C=cream b=dark brown") lines.append(" m=honey-brown :=dim star u=bee head e=stem shade") glyph = {"n": " ", "d": ",", "t": "T", "h": "H", "c": "C", "b": "b", "m": "m", "s": ":", "u": "u", "e": "e"} for row in frames[STILL_FRAME]: lines.append("".join(glyph[ch] for ch in row)) lines.append("") for title, g in (("STAMP-BEE", stamp_bee()), ("STAMP-COMB", stamp_comb()), ("STAMP-MOREL / the 32px morel", stamp_morel()), ("ICO 16x16", favicon16()), ("FAVICON 32x32", favicon32())): lines.append(title + " (. = transparent)") for row in g: lines.append("".join(glyph.get(ch, ".") for ch in row)) lines.append("") report = "\n".join(lines) + "\n" write("verify-report.txt", report.encode("ascii")) print(report) return 0 if npass == len(results) else 1 if __name__ == "__main__": sys.exit(main())