#!/usr/bin/env python3 """ spritedata2png.py - Extract RGB565 sprite data from SpriteData.h to PNG files Usage: # Export all sprites as spritesheets (frames side-by-side): python spritedata2png.py ../../Apps/TamaTac/main/Source/SpriteData.h -o sprites/ # Scale up for easier viewing/editing (4x): python spritedata2png.py SpriteData.h -o sprites/ --scale 4 # Export individual frames instead of spritesheets: python spritedata2png.py SpriteData.h -o sprites/ --individual Output: Default: One PNG per sprite as a horizontal spritesheet. egg_idle.png (48x24 = 2 frames), adult_idle.png (72x24 = 3 frames), etc. With --individual: One PNG per frame. egg_idle_frame0.png, egg_idle_frame1.png, etc. Transparent pixels (0xF81F) become fully transparent in the PNG. Output is compatible with sprite2c.py --spritedata for round-trip conversion. """ import argparse import os import re import sys from collections import OrderedDict try: from PIL import Image except ImportError: print("Error: Pillow is required. Install with: pip install Pillow") sys.exit(1) TRANSPARENT_RGB565 = 0xF81F def rgb565_to_rgb(val): """Convert RGB565 to 8-bit RGB tuple.""" r = ((val >> 11) & 0x1F) << 3 g = ((val >> 5) & 0x3F) << 2 b = (val & 0x1F) << 3 # Fill in lower bits for better color accuracy r |= r >> 5 g |= g >> 6 b |= b >> 5 return (r, g, b) def parse_sprite_arrays(header_text): """Parse all sprite_*_frame* arrays from SpriteData.h content. Returns: OrderedDict of { "sprite_name_frameN": [uint16 values, ...], ... } """ pattern = re.compile( r'constexpr\s+uint16_t\s+(sprite_\w+)\s*\[\d+\]\s*=\s*\{([^}]+)\}\s*;', re.DOTALL ) arrays = OrderedDict() hex_pattern = re.compile(r'0x([0-9A-Fa-f]+)') for match in pattern.finditer(header_text): name = match.group(1) data_str = match.group(2) values = [int(h.group(1), 16) for h in hex_pattern.finditer(data_str)] arrays[name] = values return arrays def group_frames(arrays): """Group individual frame arrays by sprite name. Input keys like "sprite_egg_idle_frame0", "sprite_egg_idle_frame1" become grouped under "egg_idle" with ordered frame data. Returns: OrderedDict of { "egg_idle": [ [pixels0], [pixels1], ... ], ... } """ grouped = OrderedDict() frame_pattern = re.compile(r'^sprite_(.+)_frame(\d+)$') for array_name, pixels in arrays.items(): match = frame_pattern.match(array_name) if not match: continue sprite_name = match.group(1) frame_idx = int(match.group(2)) if sprite_name not in grouped: grouped[sprite_name] = [] # Ensure list is large enough while len(grouped[sprite_name]) <= frame_idx: grouped[sprite_name].append(None) grouped[sprite_name][frame_idx] = pixels # Remove any None gaps for name in grouped: grouped[name] = [f for f in grouped[name] if f is not None] return grouped def create_frame_png(pixels, width, height, scale=1): """Create a PIL Image from a single frame of RGB565 pixel data.""" img = Image.new("RGBA", (width, height)) for y in range(height): for x in range(width): idx = y * width + x if idx >= len(pixels): break val = pixels[idx] if val == TRANSPARENT_RGB565: img.putpixel((x, y), (0, 0, 0, 0)) else: r, g, b = rgb565_to_rgb(val) img.putpixel((x, y), (r, g, b, 255)) if scale > 1: img = img.resize((width * scale, height * scale), Image.NEAREST) return img def create_spritesheet(frame_list, width, height, scale=1): """Create a horizontal spritesheet from multiple frames.""" num_frames = len(frame_list) sheet_width = width * num_frames sheet = Image.new("RGBA", (sheet_width, height)) for i, pixels in enumerate(frame_list): frame_img = create_frame_png(pixels, width, height, scale=1) sheet.paste(frame_img, (i * width, 0)) if scale > 1: sheet = sheet.resize((sheet_width * scale, height * scale), Image.NEAREST) return sheet def main(): parser = argparse.ArgumentParser(description="Extract RGB565 sprites from SpriteData.h to PNG") parser.add_argument("input", help="Path to SpriteData.h") parser.add_argument("--output", "-o", default=".", help="Output directory (default: current dir)") parser.add_argument("--width", "-W", type=int, default=24, help="Sprite width (default: 24)") parser.add_argument("--height", "-H", type=int, default=24, help="Sprite height (default: 24)") parser.add_argument("--scale", "-s", type=int, default=1, help="Scale factor for output PNGs (default: 1)") parser.add_argument("--individual", "-i", action="store_true", help="Export individual frame PNGs instead of spritesheets") args = parser.parse_args() if not os.path.isfile(args.input): print(f"Error: {args.input} not found") sys.exit(1) os.makedirs(args.output, exist_ok=True) with open(args.input, "r", encoding="utf-8") as f: content = f.read() arrays = parse_sprite_arrays(content) if not arrays: print("No sprite arrays found in input file.") sys.exit(1) expected_pixels = args.width * args.height print(f"Found {len(arrays)} sprite frame(s) in {os.path.basename(args.input)}") print(f"Sprite size: {args.width}x{args.height}, output scale: {args.scale}x") print() if args.individual: # Export one PNG per frame for name, pixels in arrays.items(): if len(pixels) != expected_pixels: print(f" SKIP {name}: {len(pixels)} pixels (expected {expected_pixels})") continue img = create_frame_png(pixels, args.width, args.height, args.scale) output_path = os.path.join(args.output, f"{name}.png") img.save(output_path) print(f" {name}.png") else: # Export spritesheets (one PNG per sprite, frames side-by-side) grouped = group_frames(arrays) for sprite_name, frame_list in grouped.items(): # Validate all frames valid_frames = [f for f in frame_list if len(f) == expected_pixels] if not valid_frames: print(f" SKIP {sprite_name}: no valid frames") continue if len(valid_frames) == 1: # Single frame: just save as-is img = create_frame_png(valid_frames[0], args.width, args.height, args.scale) else: # Multiple frames: create horizontal spritesheet img = create_spritesheet(valid_frames, args.width, args.height, args.scale) output_path = os.path.join(args.output, f"{sprite_name}.png") img.save(output_path) frame_desc = f"{len(valid_frames)} frame(s)" if len(valid_frames) > 1: frame_desc += f", {args.width * len(valid_frames)}x{args.height}" print(f" {sprite_name}.png ({frame_desc})") print(f"\nExported to {os.path.abspath(args.output)}") if __name__ == "__main__": main()