From bbd958baca829cb28402d317a88c1acd28282f69 Mon Sep 17 00:00:00 2001 From: Zeilenschubser Date: Sun, 28 Dec 2025 17:25:00 +0100 Subject: [PATCH] refactor: cli + rewritten by claude --- anim.py | 304 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 218 insertions(+), 86 deletions(-) diff --git a/anim.py b/anim.py index dbe803d..b7eeb69 100644 --- a/anim.py +++ b/anim.py @@ -1,93 +1,225 @@ +#!/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 -startx, starty = 16,25 +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 -image_on = Image.open("file_on_m.bmp") -image_off = Image.open("file_off_m.bmp") -# im = image.convert(mode='') -# im_pixels = im.load() -# # access pixels via [x, y] -# for col in range(im.size[0]): -# for row in range(im.size[1]): -# brightness = sum(im_pixels[col, row]) -# if brightness < 255*2: -# im_pixels[col, row] = (0, 0, 0) -# else: -# im_pixels[col, row] = (255,255,255) -# im.save('file_on_m.bmp') + 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}") -def bmp_to_bitmap_fn(im): - def fn(img): - x = [xc if sum(im.getpixel((xc,yc))) == 0 else None for xc in range(im.size[0]) for yc in range(im.size[1])] - y = [yc if sum(im.getpixel((xc,yc))) == 0 else None for xc in range(im.size[0]) for yc in range(im.size[1])] - x = list(filter(lambda x: x!=None, x)) - y = list(filter(lambda y: y!=None, y)) - xmin, xmax = min(x), max(x) - ymin, ymax = min(y), max(y) - # print(xmin, xmax, ymin, ymax) - xsize = (xmax-xmin) - ysize = (ymax-ymin) - print(len(img)) - for y in range(ysize): - for x in range(0,xsize): - # print(xmin+x+bit, ymin+y) - if im.size[0] >= x and sum(im.getpixel((xmin+x,ymin+y))) == 0: - # print( im.size[0], x,x+bit, (xmin+x+bit,ymin+y)) - pos = ((y * xsize) + x) // 8 - bit = ((y * xsize) + x) % 8 - print(pos, 1<= 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() \ No newline at end of file