#!/usr/bin/env python3 """ Convert BMP images to binary animation format. written by flop, refactored by claude sonnet 4.5 2025-12-28 """ import argparse from PIL import Image STATIC = 1 ANIMATION = 2 def bmp_to_bitmap_fn(im): """Create a function that converts image to bitmap data.""" def fn(img): # Find bounding box of black pixels x = [xc for xc in range(im.size[0]) for yc in range(im.size[1]) if sum(im.getpixel((xc, yc))) == 0] y = [yc for xc in range(im.size[0]) for yc in range(im.size[1]) if sum(im.getpixel((xc, yc))) == 0] if not x or not y: return img, 0, 0 xmin, xmax = min(x), max(x) ymin, ymax = min(y), max(y) xsize = (xmax - xmin) ysize = (ymax - ymin) print(f"Image bounds: x={xmin}-{xmax}, y={ymin}-{ymax}, size={xsize}x{ysize}") for y in range(ysize): for x in range(xsize): if im.size[0] >= x and sum(im.getpixel((xmin + x, ymin + y))) == 0: pos = ((y * xsize) + x) // 8 bit = ((y * xsize) + x) % 8 if pos < len(img): img[pos] |= 1 << bit return img, xsize, ysize return fn def get_image_dimensions(image_paths): """Get the maximum dimensions needed for all images.""" max_width = 0 max_height = 0 for path in image_paths: img = Image.open(path) # Find bounding box x = [xc for xc in range(img.size[0]) for yc in range(img.size[1]) if sum(img.getpixel((xc, yc))) == 0] y = [yc for xc in range(img.size[0]) for yc in range(img.size[1]) if sum(img.getpixel((xc, yc))) == 0] if x and y: width = max(x) - min(x) height = max(y) - min(y) max_width = max(max_width, width) max_height = max(max_height, height) return max_width, max_height def create_animation_binary(image_paths, output_path, startx, starty, update_interval, mode): """ Create binary animation file from BMP images. Args: image_paths: List of image file paths output_path: Output binary file path startx: X coordinate for animation start starty: Y coordinate for animation start update_interval: Frame update interval mode: 'frame' for one image per frame, 'differential' for alternating states """ # Determine dimensions frame_width, frame_height = get_image_dimensions(image_paths) print(f"Using frame dimensions: {frame_width}x{frame_height}") # Build animation based on mode if mode == 'differential': # Differential mode: alternate between images if len(image_paths) != 2: raise ValueError("Differential mode requires exactly 2 images (off and on states)") image_off = Image.open(image_paths[0]) image_on = Image.open(image_paths[1]) PNG_file_OFF = bmp_to_bitmap_fn(image_off) PNG_file_ON = bmp_to_bitmap_fn(image_on) # Create pattern: multiple off frames followed by on frame animation = [PNG_file_OFF] * 14 + [PNG_file_ON] elif mode == 'frame': # Frame mode: each image is a separate frame animation = [] for path in image_paths: img = Image.open(path) animation.append(bmp_to_bitmap_fn(img)) else: raise ValueError(f"Unknown mode: {mode}. Use 'frame' or 'differential'") # Single frame case: write as static object if len(animation) == 1: print("Single frame detected, creating static object") with open(output_path, "wb") as fx: # Write magic header fx.write(bytearray([0x42, 0x4e, 0x17, 0xee])) # Write number of objects fx.write(bytearray([1])) # Write static object fx.write(bytearray([STATIC, startx, starty])) fx.write(bytearray([frame_width, frame_height])) img = bytearray([0] * ((frame_width * frame_height + 7) // 8)) img, xsize, ysize = animation[0](img) fx.write(img) file_size = fx.tell() print(f"Total size: {file_size} bytes") if file_size >= 50000: print("WARNING: File size exceeds 50000 bytes!") print(f"Static image created: {output_path}") return # Multiple frames: write as animation with open(output_path, "wb") as fx: # Write magic header fx.write(bytearray([0x42, 0x4e, 0x17, 0xee])) # Write number of objects fx.write(bytearray([2])) # Write static background object (empty) fx.write(bytearray([STATIC, 0, 0])) fx.write(bytearray([120, 60])) fx.write(bytearray([0] * ((120 * 60 + 7) // 8))) # Write animation object fx.write(bytearray([ANIMATION, startx, starty])) fx.write(bytearray([frame_width, frame_height, len(animation), update_interval])) # Write animation frames for i, bmp_fun in enumerate(animation): img = bytearray([0] * ((frame_width * frame_height + 7) // 8)) img, xsize, ysize = bmp_fun(img) print(f"Frame {i}: start={fx.tell()}") fx.write(img) file_size = fx.tell() print(f"Total size: {file_size} bytes") if file_size >= 50000: print("WARNING: File size exceeds 50000 bytes!") print(f"Animation binary created: {output_path}") def main(): parser = argparse.ArgumentParser( description='Convert BMP images to binary animation format', formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument( 'images', nargs='+', help='BMP image file(s). Single image creates static object, multiple create animation.' ) parser.add_argument( '--output', type=str, default='animation.bin', help='Output binary file path' ) parser.add_argument( '--start-x', type=int, default=0, help='X coordinate for start position' ) parser.add_argument( '--start-y', type=int, default=0, help='Y coordinate for start position' ) parser.add_argument( '--update-interval', type=int, default=3, help='Update interval (0=every tick, 1=every second tick, etc.)' ) parser.add_argument( '--mode', choices=['frame', 'differential'], default='frame', help='Animation mode: "frame" = each image is a frame, "differential" = alternate between 2 images (requires exactly 2 images)' ) args = parser.parse_args() create_animation_binary( args.images, args.output, args.start_x, args.start_y, args.update_interval, args.mode ) if __name__ == '__main__': main()