refactor: gif handling

main.feat.python_and_ts_converters
Zeilenschubser 2 weeks ago
parent bbd958baca
commit 8f72f59558
  1. 142
      anim.py

@ -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()

Loading…
Cancel
Save