parent
1571d11322
commit
bbd958baca
@ -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<<bit) |
||||
if pos < len(img): |
||||
img[pos] |= 1 << bit |
||||
return img, xsize, ysize |
||||
return fn |
||||
|
||||
PNG_file_OFF = bmp_to_bitmap_fn(image_off) |
||||
PNG_file_ON = bmp_to_bitmap_fn(image_on) |
||||
|
||||
|
||||
with open("animation.bin", "wb") as fx: |
||||
# |
||||
fx.write(bytearray([0x42,0x4e,0x17,0xee])) #magic header |
||||
fx.write(bytearray([2])) # objecets |
||||
|
||||
fx.write(bytearray([STATIC, 0,0])) |
||||
fx.write(bytearray([120,60])) |
||||
fx.write(bytearray([0]*((120*60+7)//8))) |
||||
|
||||
|
||||
fx.write(bytearray([ANIMATION, startx, starty])) # we start at 0,0 for now |
||||
# print(PNG_file_OFF([0]*120*60)[1],PNG_file_OFF([0]*120*60)[2]) |
||||
# print(PNG_file_ON([0]*120*60)[1],PNG_file_ON([0]*120*60)[2]) |
||||
ww, hh = 100, 10 |
||||
uu = 3# update interval, 0 every tick, 1 every second tick |
||||
animation = [ |
||||
PNG_file_OFF, |
||||
PNG_file_OFF, |
||||
PNG_file_OFF, |
||||
PNG_file_OFF, |
||||
PNG_file_OFF, |
||||
PNG_file_OFF, |
||||
PNG_file_OFF, |
||||
PNG_file_OFF, |
||||
PNG_file_OFF, |
||||
PNG_file_OFF, |
||||
PNG_file_OFF, |
||||
PNG_file_OFF, |
||||
PNG_file_OFF, |
||||
PNG_file_OFF, |
||||
PNG_file_ON, |
||||
] |
||||
fx.write(bytearray([ww,hh,len(animation),uu])) |
||||
for bmp_fun in animation: |
||||
img = bytearray([0]*((ww*hh+7)//8)) |
||||
img,xsize,ysize = bmp_fun(img) |
||||
|
||||
print("framestart=",fx.tell()) |
||||
fx.write(img) |
||||
print("frameend=",fx.tell()) |
||||
|
||||
print("size:", fx.tell()) |
||||
assert fx.tell() < 50000, "do not use too much image space" |
||||
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() |
||||
Loading…
Reference in new issue