#!/usr/bin/env python3 """Convert PNG images to the binary RGB565 RLE format used by lcd_direct.""" import argparse import struct from pathlib import Path try: from PIL import Image except ImportError as exc: raise SystemExit("Pillow is required. Install it with: python -m pip install Pillow") from exc MAX_RUN_COUNT = 0xFFFF def parse_rgb(value): text = value.strip() if text.startswith("#"): text = text[1:] if len(text) != 6: raise argparse.ArgumentTypeError("expected RRGGBB or #RRGGBB") try: return tuple(int(text[index:index + 2], 16) for index in (0, 2, 4)) except ValueError as exc: raise argparse.ArgumentTypeError("background must be hexadecimal RGB") from exc def rgb888_to_rgb565(red, green, blue): return ((red & 0xF8) << 8) | ((green & 0xFC) << 3) | (blue >> 3) def image_to_runs(path, background): with Image.open(path) as image: rgba = image.convert("RGBA") width, height = rgba.size bg_red, bg_green, bg_blue = background runs = [] current_color = None current_count = 0 for red, green, blue, alpha in rgba.getdata(): if alpha < 255: red = ((red * alpha) + (bg_red * (255 - alpha))) // 255 green = ((green * alpha) + (bg_green * (255 - alpha))) // 255 blue = ((blue * alpha) + (bg_blue * (255 - alpha))) // 255 color = rgb888_to_rgb565(red, green, blue) if color == current_color and current_count < MAX_RUN_COUNT: current_count += 1 continue if current_color is not None: runs.append((current_color, current_count)) current_color = color current_count = 1 if current_color is not None: runs.append((current_color, current_count)) return width, height, runs def write_rle(path, runs): path.parent.mkdir(parents=True, exist_ok=True) with path.open("wb") as output: for color, count in runs: output.write(struct.pack(" {out_path} ({width}x{height}, {len(runs)} runs)") def main(): parser = argparse.ArgumentParser( description="Convert PNG files to lcd_direct RGB565 RLE binary assets.", ) parser.add_argument("input", type=Path, help="PNG file or directory containing PNG files.") parser.add_argument( "-o", "--output", type=Path, help="Output .rle file for one input file, or output directory for an input directory.", ) parser.add_argument( "-r", "--recursive", action="store_true", help="Recursively convert PNG files when input is a directory.", ) parser.add_argument( "--background", type=parse_rgb, default=(0, 0, 0), help="RGB background for transparent pixels, as RRGGBB or #RRGGBB. Default: 000000.", ) args = parser.parse_args() input_path = args.input.resolve() if not input_path.exists(): raise SystemExit(f"input path does not exist: {input_path}") output = args.output.resolve() if args.output is not None else None inputs = list(iter_inputs(input_path, args.recursive)) if not inputs: raise SystemExit(f"no PNG files found in: {input_path}") if input_path.is_file() and output is not None and output.suffix.lower() != ".rle": raise SystemExit("single-file output must use a .rle suffix") for input_file in inputs: convert_file(input_file, input_path, output, args.background) if __name__ == "__main__": main()