refactor: cli + rewritten by claude

main.feat.python_and_ts_converters
Zeilenschubser 2 weeks ago
parent 1571d11322
commit bbd958baca
  1. 304
      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 from PIL import Image
STATIC = 1 STATIC = 1
ANIMATION = 2 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") xmin, xmax = min(x), max(x)
image_off = Image.open("file_off_m.bmp") ymin, ymax = min(y), max(y)
# im = image.convert(mode='') xsize = (xmax - xmin)
# im_pixels = im.load() ysize = (ymax - ymin)
# # 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')
print(f"Image bounds: x={xmin}-{xmax}, y={ymin}-{ymax}, size={xsize}x{ysize}")
def bmp_to_bitmap_fn(im): for y in range(ysize):
def fn(img): for x in range(xsize):
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])] if im.size[0] >= x and sum(im.getpixel((xmin + x, ymin + y))) == 0:
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])] pos = ((y * xsize) + x) // 8
x = list(filter(lambda x: x!=None, x)) bit = ((y * xsize) + x) % 8
y = list(filter(lambda y: y!=None, y)) if pos < len(img):
xmin, xmax = min(x), max(x) img[pos] |= 1 << bit
ymin, ymax = min(y), max(y)
# print(xmin, xmax, ymin, ymax) return img, xsize, ysize
xsize = (xmax-xmin) return fn
ysize = (ymax-ymin)
print(len(img)) def get_image_dimensions(image_paths):
for y in range(ysize): """Get the maximum dimensions needed for all images."""
for x in range(0,xsize): max_width = 0
# print(xmin+x+bit, ymin+y) max_height = 0
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)) for path in image_paths:
pos = ((y * xsize) + x) // 8 img = Image.open(path)
bit = ((y * xsize) + x) % 8 # Find bounding box
print(pos, 1<<bit) x = [xc for xc in range(img.size[0]) for yc in range(img.size[1])
if pos < len(img): if sum(img.getpixel((xc, yc))) == 0]
img[pos] |= 1 << bit y = [yc for xc in range(img.size[0]) for yc in range(img.size[1])
return img, xsize, ysize if sum(img.getpixel((xc, yc))) == 0]
return fn
if x and y:
PNG_file_OFF = bmp_to_bitmap_fn(image_off) width = max(x) - min(x)
PNG_file_ON = bmp_to_bitmap_fn(image_on) height = max(y) - min(y)
max_width = max(max_width, width)
max_height = max(max_height, height)
with open("animation.bin", "wb") as fx:
# return max_width, max_height
fx.write(bytearray([0x42,0x4e,0x17,0xee])) #magic header
fx.write(bytearray([2])) # objecets def create_animation_binary(image_paths, output_path, startx, starty,
update_interval, mode):
fx.write(bytearray([STATIC, 0,0])) """
fx.write(bytearray([120,60])) Create binary animation file from BMP images.
fx.write(bytearray([0]*((120*60+7)//8)))
Args:
image_paths: List of image file paths
fx.write(bytearray([ANIMATION, startx, starty])) # we start at 0,0 for now output_path: Output binary file path
# print(PNG_file_OFF([0]*120*60)[1],PNG_file_OFF([0]*120*60)[2]) startx: X coordinate for animation start
# print(PNG_file_ON([0]*120*60)[1],PNG_file_ON([0]*120*60)[2]) starty: Y coordinate for animation start
ww, hh = 100, 10 update_interval: Frame update interval
uu = 3# update interval, 0 every tick, 1 every second tick mode: 'frame' for one image per frame, 'differential' for alternating states
animation = [ """
PNG_file_OFF,
PNG_file_OFF, # Determine dimensions
PNG_file_OFF, frame_width, frame_height = get_image_dimensions(image_paths)
PNG_file_OFF, print(f"Using frame dimensions: {frame_width}x{frame_height}")
PNG_file_OFF,
PNG_file_OFF, # Build animation based on mode
PNG_file_OFF, if mode == 'differential':
PNG_file_OFF, # Differential mode: alternate between images
PNG_file_OFF, if len(image_paths) != 2:
PNG_file_OFF, raise ValueError("Differential mode requires exactly 2 images (off and on states)")
PNG_file_OFF,
PNG_file_OFF, image_off = Image.open(image_paths[0])
PNG_file_OFF, image_on = Image.open(image_paths[1])
PNG_file_OFF,
PNG_file_ON, PNG_file_OFF = bmp_to_bitmap_fn(image_off)
] PNG_file_ON = bmp_to_bitmap_fn(image_on)
fx.write(bytearray([ww,hh,len(animation),uu]))
for bmp_fun in animation: # Create pattern: multiple off frames followed by on frame
img = bytearray([0]*((ww*hh+7)//8)) animation = [PNG_file_OFF] * 14 + [PNG_file_ON]
img,xsize,ysize = bmp_fun(img)
elif mode == 'frame':
print("framestart=",fx.tell()) # Frame mode: each image is a separate frame
fx.write(img) animation = []
print("frameend=",fx.tell()) for path in image_paths:
img = Image.open(path)
print("size:", fx.tell()) animation.append(bmp_to_bitmap_fn(img))
assert fx.tell() < 50000, "do not use too much image space" 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()
Loading…
Cancel
Save