#!/usr/bin/env python3 # build: assemble the installable GRUB theme into dist/udt/. # Copyright (C) 2026 Danilo M. # Licensed under the GNU General Public License v2 only. """Assemble dist/udt/ from the theme.txt udt-palette rendered. Usage: build [--selftest] Writes the PNG slices, the .pf2 fonts and a GRUB-readable copy of the current wallpaper next to theme.txt. Everything is staged in a temporary directory and swapped in at the end, so a failed run leaves the previous dist/udt/ untouched. Never touches /boot: root copies dist/udt/ there. """ import struct import sys import zlib PNG_SIG = b"\x89PNG\r\n\x1a\n" def chunk(tag, data): return (struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data))) def png(w, h, rgba): """A w x h PNG of one colour: 8-bit RGBA, not interlaced, which GRUB reads.""" row = b"\0" + bytes(rgba) * w # filter byte 0, then the pixels return (PNG_SIG + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(row * h)) + chunk(b"IEND", b"")) def unpng(data): """Decode what png() writes: (width, height, set of RGBA pixels).""" assert data[:8] == PNG_SIG pos, idat = 8, b"" while pos < len(data): n, tag = struct.unpack(">I4s", data[pos:pos + 8]) body = data[pos + 8:pos + 8 + n] assert struct.unpack(">I", data[pos + 8 + n:pos + 12 + n])[0] == zlib.crc32(tag + body) if tag == b"IHDR": w, h = struct.unpack(">II", body[:8]) elif tag == b"IDAT": idat += body pos += 12 + n raw = zlib.decompress(idat) stride = 1 + 4 * w px = set() for y in range(h): row = raw[y * stride:(y + 1) * stride] assert row[0] == 0 px |= {tuple(row[1 + 4 * x:5 + 4 * x]) for x in range(w)} return w, h, px def selftest(): w, h, px = unpng(png(3, 2, (0x12, 0x34, 0x56, 0xd9))) assert (w, h, px) == (3, 2, {(0x12, 0x34, 0x56, 0xd9)}), (w, h, px) print("selftest ok") def main(): if sys.argv[1:] == ["--selftest"]: return selftest() if __name__ == "__main__": main()