# Convert PNG/JPG icons to custom RGB565 raw format with header # Output file format (little-endian): [uint16 width][uint16 height][pixels: width*height*2 bytes] # Usage: python tools/convert_icons.py --src assets/icons_src --dst data/icons --size 48 48 import argparse import os from PIL import Image def rgb888_to_rgb565(r, g, b): # Swap r/b to match display byte order used in utils::rgbTo565 r, b = b, r r5 = (r * 31) // 255 g6 = (g * 63) // 255 b5 = (b * 31) // 255 return (r5 << 11) | (g6 << 5) | b5 def convert_one(src_path, dst_path, out_w, out_h): img = Image.open(src_path).convert('RGBA') img = img.resize((out_w, out_h), Image.LANCZOS) # Premultiply against black background for simple alpha handling bg = Image.new('RGBA', (out_w, out_h), (0, 0, 0, 255)) img = Image.alpha_composite(bg, img) # Write header + pixels os.makedirs(os.path.dirname(dst_path), exist_ok=True) with open(dst_path, 'wb') as f: f.write(bytes([out_w & 0xFF, (out_w >> 8) & 0xFF, out_h & 0xFF, (out_h >> 8) & 0xFF])) px = img.load() for y in range(out_h): for x in range(out_w): r, g, b, a = px[x, y] c = rgb888_to_rgb565(r, g, b) f.write(bytes([c & 0xFF, (c >> 8) & 0xFF])) print(f"Converted: {src_path} -> {dst_path}") def main(): parser = argparse.ArgumentParser() parser.add_argument('--src', default='assets/icons_src', help='Source icon folder') parser.add_argument('--dst', default='data/icons', help='Destination folder in LittleFS data') parser.add_argument('--size', nargs=2, type=int, default=[48, 48], help='Output icon size WxH') args = parser.parse_args() out_w, out_h = args.size mapping = { 'timer': 'timer.r565', 'web': 'web.r565', 'game': 'game.r565', } for name, out_file in mapping.items(): src_file = os.path.join(args.src, f'{name}.png') if not os.path.exists(src_file): print(f"WARN: missing {src_file}, skip") continue dst_file = os.path.join(args.dst, out_file) convert_one(src_file, dst_file, out_w, out_h) print('Done. You can now upload FS: pio run -t uploadfs') if __name__ == '__main__': main()