#!/usr/bin/env python3 """Turns the Launcher's icons (assets/icons/.png) into C arrays: src/ui/icons.h and icons.cpp. An icon is 32 x 32 and one colour: a pixel is drawn where the picture is light and opaque, and left alone where it is dark or transparent. Any image editor's PNG will do (grey, RGB, palette, with or without alpha, 1 or 8 bits a channel, not interlaced). Run this after changing a picture: python3 scripts/make_icons.py CI runs it with --check, which fails when the arrays are not what the pictures give. """ import os import struct import sys import zlib ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") SOURCE = os.path.join(ROOT, "assets", "icons") SIZE = 32 def read_png(path): """The picture as rows of booleans: True where a pixel is drawn.""" data = open(path, "rb").read() if data[:8] != b"\x89PNG\r\n\x1a\n": raise ValueError("not a PNG") at, idat, palette, alpha = 8, b"", None, None while at < len(data): (length,), kind = struct.unpack(">I", data[at:at + 4]), data[at + 4:at + 8] body = data[at + 8:at + 8 + length] at += 12 + length if kind == b"IHDR": width, height, depth, colour, _, _, interlace = struct.unpack(">IIBBBBB", body) elif kind == b"PLTE": palette = [body[i:i + 3] for i in range(0, len(body), 3)] elif kind == b"tRNS": alpha = body elif kind == b"IDAT": idat += body if interlace or depth not in (1, 8) or (depth == 1 and colour not in (0, 3)): raise ValueError("save it as a plain 8-bit or 1-bit PNG, not interlaced") channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[colour] step = channels if depth == 8 else 1 # bytes a pixel, for the filters stride = (width * channels * depth + 7) // 8 raw, rows, before = zlib.decompress(idat), [], bytes(stride) for y in range(height): kind, line = raw[y * (stride + 1)], bytearray(raw[y * (stride + 1) + 1:(y + 1) * (stride + 1)]) for i in range(stride): a = line[i - step] if i >= step else 0 b, c = before[i], before[i - step] if i >= step else 0 if kind == 1: line[i] = (line[i] + a) & 255 elif kind == 2: line[i] = (line[i] + b) & 255 elif kind == 3: line[i] = (line[i] + (a + b) // 2) & 255 elif kind == 4: p = a + b - c pa, pb, pc = abs(p - a), abs(p - b), abs(p - c) line[i] = (line[i] + (a if pa <= pb and pa <= pc else b if pb <= pc else c)) & 255 before = bytes(line) row = [] for x in range(width): if depth == 1: v = (line[x // 8] >> (7 - x % 8)) & 1 px = [v * 255] if colour == 0 else [v] else: px = line[x * channels:(x + 1) * channels] if colour == 3: opaque = alpha is None or px[0] >= len(alpha) or alpha[px[0]] > 127 light = sum(palette[px[0]]) > 381 elif colour in (0, 4): opaque, light = colour == 0 or px[1] > 127, px[0] > 127 else: opaque, light = colour == 2 or px[3] > 127, sum(px[:3]) > 381 row.append(opaque and light) rows.append(row) return rows def main(): names = sorted(f[:-4] for f in os.listdir(SOURCE) if f.endswith(".png")) header = ["// Made by scripts/make_icons.py from assets/icons/*.png: change the pictures, not this file.", "#pragma once", "", "#include ", "", "namespace roro::icons {", "", "// One bit a pixel, the leftmost first, a row after another: what Canvas::drawBitmap takes.", f"constexpr int kSize = {SIZE};", ""] body = ["// Made by scripts/make_icons.py from assets/icons/*.png: change the pictures, not this file.", '#include "icons.h"', "", "namespace roro::icons {", ""] for name in names: rows = read_png(os.path.join(SOURCE, name + ".png")) if len(rows) != SIZE or len(rows[0]) != SIZE: sys.exit(f"{name}.png is {len(rows[0])} x {len(rows)}: an icon is {SIZE} x {SIZE}") ident = name.replace("-", "_") header.append(f"extern const uint8_t {ident}[{SIZE * SIZE // 8}]; // {name}.png") body.append(f"const uint8_t {ident}[{SIZE * SIZE // 8}] = {{") for row in rows: packed = [sum(0x80 >> i for i in range(8) if row[b * 8 + i]) for b in range(SIZE // 8)] body.append(" " + ", ".join(f"0x{v:02x}" for v in packed) + ", // " + "".join("#" if v else "." for v in row)) body += ["};", ""] header += ["", "} // namespace roro::icons", ""] body += ["} // namespace roro::icons", ""] out = {os.path.join(ROOT, "src", "ui", "icons.h"): "\n".join(header), os.path.join(ROOT, "src", "ui", "icons.cpp"): "\n".join(body)} if "--check" in sys.argv: stale = [os.path.relpath(p, ROOT) for p, text in out.items() if not os.path.exists(p) or open(p).read() != text] if stale: sys.exit("out of date: " + ", ".join(stale) + " (run scripts/make_icons.py)") print(f"{len(names)} icons, up to date") return for path, text in out.items(): open(path, "w").write(text) print(f"{len(names)} icons written") if __name__ == "__main__": main()