|
|
|
|
@ -1,12 +1,12 @@ |
|
|
|
|
#!/usr/bin/env python3 |
|
|
|
|
""" |
|
|
|
|
Convert BMP images to binary animation format. |
|
|
|
|
Convert BMP/GIF images to binary animation format. |
|
|
|
|
|
|
|
|
|
written by flop, refactored by claude sonnet 4.5 |
|
|
|
|
written by me, refactored by claude sonnet 4.5 |
|
|
|
|
2025-12-28 |
|
|
|
|
""" |
|
|
|
|
import argparse |
|
|
|
|
from PIL import Image |
|
|
|
|
from PIL import Image, ImageSequence |
|
|
|
|
|
|
|
|
|
STATIC = 1 |
|
|
|
|
ANIMATION = 2 |
|
|
|
|
@ -41,18 +41,97 @@ def bmp_to_bitmap_fn(im): |
|
|
|
|
return img, xsize, ysize |
|
|
|
|
return fn |
|
|
|
|
|
|
|
|
|
def get_image_dimensions(image_paths): |
|
|
|
|
"""Get the maximum dimensions needed for all images.""" |
|
|
|
|
def extract_frames_from_image(image_path): |
|
|
|
|
""" |
|
|
|
|
Extract all frames from an image file (GIF or static). |
|
|
|
|
Returns a list of PIL Image objects. |
|
|
|
|
""" |
|
|
|
|
img = Image.open(image_path) |
|
|
|
|
frames = [] |
|
|
|
|
|
|
|
|
|
# Convert to RGB if needed |
|
|
|
|
if img.mode not in ('RGB', 'L'): |
|
|
|
|
img = img.convert('RGB') |
|
|
|
|
|
|
|
|
|
if image_path.endswith(".gif"): |
|
|
|
|
with Image.open(image_path) as im_gif: |
|
|
|
|
index = 0 |
|
|
|
|
for img in ImageSequence.Iterator(im_gif): |
|
|
|
|
frame_copy = img.copy() |
|
|
|
|
if frame_copy.mode != 'RGB': |
|
|
|
|
frame_copy = frame_copy.convert('RGB') |
|
|
|
|
frames.append(frame_copy) |
|
|
|
|
if len(frames) == 0: |
|
|
|
|
try: |
|
|
|
|
# Try to extract all frames (works for GIFs) |
|
|
|
|
for frame_num in range(img.n_frames): |
|
|
|
|
print("frame is ", frame_num) |
|
|
|
|
img.seek(frame_num) |
|
|
|
|
# Copy the frame to prevent issues with lazy loading |
|
|
|
|
frame_copy = img.copy() |
|
|
|
|
if frame_copy.mode != 'RGB': |
|
|
|
|
frame_copy = frame_copy.convert('RGB') |
|
|
|
|
frames.append(frame_copy) |
|
|
|
|
print(f"Extracted frame {frame_num} from {image_path}") |
|
|
|
|
except AttributeError: |
|
|
|
|
# Not an animated image, just use the single frame |
|
|
|
|
frames.append(img) |
|
|
|
|
print(f"Loaded static image from {image_path}") |
|
|
|
|
|
|
|
|
|
return frames |
|
|
|
|
|
|
|
|
|
def extract_all_frames(image_paths, mode): |
|
|
|
|
""" |
|
|
|
|
Extract frames from all input images based on mode. |
|
|
|
|
|
|
|
|
|
Args: |
|
|
|
|
image_paths: List of image file paths |
|
|
|
|
mode: 'frame' for sequential frames, 'differential' for alternating pattern |
|
|
|
|
|
|
|
|
|
Returns: |
|
|
|
|
List of PIL Image objects representing animation frames |
|
|
|
|
""" |
|
|
|
|
all_frames = [] |
|
|
|
|
|
|
|
|
|
if mode == 'differential': |
|
|
|
|
# Differential mode: extract frames from each image, then create alternating pattern |
|
|
|
|
if len(image_paths) != 2: |
|
|
|
|
raise ValueError("Differential mode requires exactly 2 images (off and on states)") |
|
|
|
|
|
|
|
|
|
off_frames = extract_frames_from_image(image_paths[0]) |
|
|
|
|
on_frames = extract_frames_from_image(image_paths[1]) |
|
|
|
|
|
|
|
|
|
# Use only the first frame from each for differential mode |
|
|
|
|
off_frame = off_frames[0] |
|
|
|
|
on_frame = on_frames[0] |
|
|
|
|
|
|
|
|
|
# Create pattern: multiple off frames followed by on frame |
|
|
|
|
all_frames = [off_frame] * 14 + [on_frame] |
|
|
|
|
print(f"Created differential animation: 14 off frames + 1 on frame") |
|
|
|
|
|
|
|
|
|
elif mode == 'frame': |
|
|
|
|
# Frame mode: concatenate all frames from all images |
|
|
|
|
for path in image_paths: |
|
|
|
|
frames = extract_frames_from_image(path) |
|
|
|
|
all_frames.extend(frames) |
|
|
|
|
|
|
|
|
|
print(f"Total frames collected: {len(all_frames)}") |
|
|
|
|
else: |
|
|
|
|
raise ValueError(f"Unknown mode: {mode}. Use 'frame' or 'differential'") |
|
|
|
|
|
|
|
|
|
return all_frames |
|
|
|
|
|
|
|
|
|
def get_frame_dimensions(frames): |
|
|
|
|
"""Get the maximum dimensions needed for all frames.""" |
|
|
|
|
max_width = 0 |
|
|
|
|
max_height = 0 |
|
|
|
|
|
|
|
|
|
for path in image_paths: |
|
|
|
|
img = Image.open(path) |
|
|
|
|
for frame in frames: |
|
|
|
|
# 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] |
|
|
|
|
x = [xc for xc in range(frame.size[0]) for yc in range(frame.size[1]) |
|
|
|
|
if sum(frame.getpixel((xc, yc))) == 0] |
|
|
|
|
y = [yc for xc in range(frame.size[0]) for yc in range(frame.size[1]) |
|
|
|
|
if sum(frame.getpixel((xc, yc))) == 0] |
|
|
|
|
|
|
|
|
|
if x and y: |
|
|
|
|
width = max(x) - min(x) |
|
|
|
|
@ -65,7 +144,7 @@ def get_image_dimensions(image_paths): |
|
|
|
|
def create_animation_binary(image_paths, output_path, startx, starty, |
|
|
|
|
update_interval, mode): |
|
|
|
|
""" |
|
|
|
|
Create binary animation file from BMP images. |
|
|
|
|
Create binary animation file from BMP/GIF images. |
|
|
|
|
|
|
|
|
|
Args: |
|
|
|
|
image_paths: List of image file paths |
|
|
|
|
@ -76,33 +155,18 @@ def create_animation_binary(image_paths, output_path, startx, starty, |
|
|
|
|
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}") |
|
|
|
|
# Extract all frames from input images based on mode |
|
|
|
|
frames = extract_all_frames(image_paths, mode) |
|
|
|
|
|
|
|
|
|
# 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)") |
|
|
|
|
if not frames: |
|
|
|
|
raise ValueError("No frames extracted from input images") |
|
|
|
|
|
|
|
|
|
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] |
|
|
|
|
# Determine dimensions |
|
|
|
|
frame_width, frame_height = get_frame_dimensions(frames) |
|
|
|
|
print(f"Using frame dimensions: {frame_width}x{frame_height}") |
|
|
|
|
|
|
|
|
|
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'") |
|
|
|
|
# Convert frames to bitmap functions |
|
|
|
|
animation = [bmp_to_bitmap_fn(frame) for frame in frames] |
|
|
|
|
|
|
|
|
|
# Single frame case: write as static object |
|
|
|
|
if len(animation) == 1: |
|
|
|
|
@ -165,14 +229,14 @@ def create_animation_binary(image_paths, output_path, startx, starty, |
|
|
|
|
|
|
|
|
|
def main(): |
|
|
|
|
parser = argparse.ArgumentParser( |
|
|
|
|
description='Convert BMP images to binary animation format', |
|
|
|
|
description='Convert BMP/GIF 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.' |
|
|
|
|
help='Image file(s) (BMP or GIF). GIF frames are extracted automatically. Single frame creates static object, multiple frames create animation.' |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
parser.add_argument( |
|
|
|
|
@ -207,7 +271,7 @@ def main(): |
|
|
|
|
'--mode', |
|
|
|
|
choices=['frame', 'differential'], |
|
|
|
|
default='frame', |
|
|
|
|
help='Animation mode: "frame" = each image is a frame, "differential" = alternate between 2 images (requires exactly 2 images)' |
|
|
|
|
help='Animation mode: "frame" = each image/frame is a frame, "differential" = alternate between 2 images (requires exactly 2 images)' |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
args = parser.parse_args() |
|
|
|
|
|