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 |
from PIL import Image |
||||||
|
|
||||||
STATIC = 1 |
STATIC = 1 |
||||||
ANIMATION = 2 |
ANIMATION = 2 |
||||||
|
|
||||||
startx, starty = 16,25 |
|
||||||
|
|
||||||
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') |
|
||||||
|
|
||||||
|
|
||||||
def bmp_to_bitmap_fn(im): |
def bmp_to_bitmap_fn(im): |
||||||
|
"""Create a function that converts image to bitmap data.""" |
||||||
def fn(img): |
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])] |
# Find bounding box of black pixels |
||||||
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 = [xc for xc in range(im.size[0]) for yc in range(im.size[1]) |
||||||
x = list(filter(lambda x: x!=None, x)) |
if sum(im.getpixel((xc, yc))) == 0] |
||||||
y = list(filter(lambda y: y!=None, y)) |
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) |
xmin, xmax = min(x), max(x) |
||||||
ymin, ymax = min(y), max(y) |
ymin, ymax = min(y), max(y) |
||||||
# print(xmin, xmax, ymin, ymax) |
|
||||||
xsize = (xmax - xmin) |
xsize = (xmax - xmin) |
||||||
ysize = (ymax - ymin) |
ysize = (ymax - ymin) |
||||||
print(len(img)) |
|
||||||
|
print(f"Image bounds: x={xmin}-{xmax}, y={ymin}-{ymax}, size={xsize}x{ysize}") |
||||||
|
|
||||||
for y in range(ysize): |
for y in range(ysize): |
||||||
for x in range(0,xsize): |
for x in range(xsize): |
||||||
# print(xmin+x+bit, ymin+y) |
|
||||||
if im.size[0] >= x and sum(im.getpixel((xmin + x, ymin + y))) == 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)) |
|
||||||
pos = ((y * xsize) + x) // 8 |
pos = ((y * xsize) + x) // 8 |
||||||
bit = ((y * xsize) + x) % 8 |
bit = ((y * xsize) + x) % 8 |
||||||
print(pos, 1<<bit) |
|
||||||
if pos < len(img): |
if pos < len(img): |
||||||
img[pos] |= 1 << bit |
img[pos] |= 1 << bit |
||||||
|
|
||||||
return img, xsize, ysize |
return img, xsize, ysize |
||||||
return fn |
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_OFF = bmp_to_bitmap_fn(image_off) |
||||||
PNG_file_ON = bmp_to_bitmap_fn(image_on) |
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])) |
||||||
|
|
||||||
with open("animation.bin", "wb") as fx: |
# Write number of objects |
||||||
# |
fx.write(bytearray([1])) |
||||||
fx.write(bytearray([0x42,0x4e,0x17,0xee])) #magic header |
|
||||||
fx.write(bytearray([2])) # objecets |
|
||||||
|
|
||||||
|
# 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([STATIC, 0, 0])) |
||||||
fx.write(bytearray([120, 60])) |
fx.write(bytearray([120, 60])) |
||||||
fx.write(bytearray([0] * ((120 * 60 + 7) // 8))) |
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])) |
||||||
|
|
||||||
fx.write(bytearray([ANIMATION, startx, starty])) # we start at 0,0 for now |
# Write animation frames |
||||||
# print(PNG_file_OFF([0]*120*60)[1],PNG_file_OFF([0]*120*60)[2]) |
for i, bmp_fun in enumerate(animation): |
||||||
# print(PNG_file_ON([0]*120*60)[1],PNG_file_ON([0]*120*60)[2]) |
img = bytearray([0] * ((frame_width * frame_height + 7) // 8)) |
||||||
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) |
img, xsize, ysize = bmp_fun(img) |
||||||
|
print(f"Frame {i}: start={fx.tell()}") |
||||||
print("framestart=",fx.tell()) |
|
||||||
fx.write(img) |
fx.write(img) |
||||||
print("frameend=",fx.tell()) |
|
||||||
|
|
||||||
print("size:", fx.tell()) |
file_size = fx.tell() |
||||||
assert fx.tell() < 50000, "do not use too much image space" |
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