#!/usr/bin/env python3 """Generate lossless RLE assets; PNGs remain the editable source of truth.""" from pathlib import Path from PIL import Image ROOT = Path(__file__).resolve().parents[1] LAYERS = ROOT / "components/platform/assets/lcd_layers/jichubiaoqing" GENERATED = ROOT / "components/platform/src/generated/emote_gfx" def pixels565(image): result = [] for r, g, b, a in image.convert("RGBA").getdata(): color = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3) result.append((color >> 8, color & 255, a)) return result def encode(pixels): out = bytearray() for pixel in pixels: if out and tuple(out[-3:]) == pixel and out[-4] < 255: out[-4] += 1 else: out.extend((1, *pixel)) # Round-trip validation for every generated asset. decoded = [] for i in range(0, len(out), 4): decoded.extend([tuple(out[i + 1:i + 4])] * out[i]) assert decoded == pixels return out def generate(name, image, x, y): data = encode(pixels565(image)) rows = [" " + ", ".join(f"0x{v:02x}" for v in data[i:i + 24]) + "," for i in range(0, len(data), 24)] text = ('/* Generated by tools/compress_idle_expression.py; do not edit. */\n' '#include "lcd_emote_assets.h"\n\n' f'static const uint8_t {name}_rle[] = {{\n' + '\n'.join(rows) + '\n};\n\n' f'const lcd_emote_rle_asset_t {name}_rle_asset = {{\n' f' .width = {image.width}, .height = {image.height}, .x = {x}, .y = {y},\n' f' .compressed_size = sizeof({name}_rle), .data = {name}_rle,\n}};\n') GENERATED.mkdir(parents=True, exist_ok=True) (GENERATED / f"{name}.c").write_text(text) print(f"{name}: {image.width}x{image.height} at ({x},{y}), " f"raw={image.width * image.height * 3}B compressed={len(data)}B") def main(): base = Image.open(LAYERS / "idle_base.png").convert("RGBA") # Pure black is supplied by the display, preserving all nonblack RGB565 pixels. rgba = list(base.getdata()) rgba = [(r, g, b, 255 if ((r >> 3) or (g >> 2) or (b >> 3)) else 0) for r, g, b, _ in rgba] base.putdata(rgba) bbox = base.getbbox() assert bbox is not None generate("idle_base", base.crop(bbox), bbox[0], bbox[1]) eyes = Image.open(LAYERS / "idle_eyes_open.png").convert("RGBA") assert eyes.size == (238, 93), "eye geometry must match the mesh layout" generate("idle_eyes_open", eyes, 41, 65) if __name__ == "__main__": main()