712db9c597
Add B1 directory-type seed mechanism to AGENT_WORKSPACE_SEED_FILES: - Extended agentWorkspaceFilesForRun with collectDirectorySeedFiles helper - arm2d-skill (SKILL.md + references/ + python/) auto-injected to .agents/skills/arm2d-skill/ - Enables Code Agent to follow ARM-2D constraints and call asset scripts in case Skill source: https://github.com/notLabyet/HWLabOA
190 lines
5.2 KiB
Python
190 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
GIF to PNG Sprite Sheet Converter
|
|
Flattens GIF animation frames into a single PNG sprite sheet
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
from PIL import Image
|
|
|
|
|
|
def parse_args():
|
|
"""Parse command line arguments"""
|
|
parser = argparse.ArgumentParser(
|
|
description='Convert GIF animation to a flattened PNG sprite sheet',
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
python gif2png.py animation.gif -n 5
|
|
python gif2png.py animation.gif -n 8 --dim 64 64
|
|
python gif2png.py animation.gif -n 4 --dim 100 100 -o output.png
|
|
"""
|
|
)
|
|
|
|
parser.add_argument(
|
|
'input',
|
|
type=str,
|
|
help='Input GIF file path'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'-n', '--frames-per-row',
|
|
type=int,
|
|
default=5,
|
|
metavar='N',
|
|
help='Number of frames per row (default: 5)'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--dim',
|
|
type=int,
|
|
nargs=2,
|
|
metavar=('WIDTH', 'HEIGHT'),
|
|
help='Resize each frame to specified width and height, e.g.: --dim 64 64'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'-o', '--output',
|
|
type=str,
|
|
metavar='PATH',
|
|
help='Output file path (default: same name as input with .png extension)'
|
|
)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
def process_gif(input_path, frames_per_row, target_size=None, output_path=None):
|
|
"""
|
|
Process GIF file and flatten frames into a PNG sprite sheet
|
|
|
|
Args:
|
|
input_path: Input GIF file path
|
|
frames_per_row: Number of frames per row
|
|
target_size: (width, height) tuple, or None to keep original size
|
|
output_path: Output file path, or None to auto-generate
|
|
"""
|
|
# Open GIF file
|
|
try:
|
|
gif = Image.open(input_path)
|
|
except FileNotFoundError:
|
|
print(f"Error: File not found - {input_path}")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"Error: Cannot open file - {e}")
|
|
sys.exit(1)
|
|
|
|
# Check if file is GIF format
|
|
if gif.format != 'GIF':
|
|
print(f"Warning: Input file format is {gif.format}, not standard GIF format")
|
|
|
|
# Extract all frames
|
|
frames = []
|
|
try:
|
|
while True:
|
|
# Copy current frame (seek changes current frame)
|
|
frame = gif.copy()
|
|
|
|
# Convert to RGBA mode to support transparency
|
|
if frame.mode != 'RGBA':
|
|
frame = frame.convert('RGBA')
|
|
|
|
# Resize if target size specified
|
|
if target_size:
|
|
frame = frame.resize(target_size, Image.Resampling.LANCZOS)
|
|
|
|
frames.append(frame)
|
|
|
|
# Move to next frame
|
|
gif.seek(gif.tell() + 1)
|
|
except EOFError:
|
|
pass # Reached end of frames
|
|
|
|
if not frames:
|
|
print("Error: No frames extracted")
|
|
sys.exit(1)
|
|
|
|
print(f"Successfully extracted {len(frames)} frames")
|
|
|
|
# Get frame dimensions (all frames have same size after resize)
|
|
frame_width, frame_height = frames[0].size
|
|
|
|
# Calculate grid layout
|
|
total_frames = len(frames)
|
|
num_cols = min(frames_per_row, total_frames)
|
|
num_rows = (total_frames + frames_per_row - 1) // frames_per_row # Ceiling division
|
|
|
|
print(f"Layout: {num_rows} rows x {num_cols} columns")
|
|
print(f"Frame size: {frame_width}x{frame_height}")
|
|
|
|
# Create canvas
|
|
canvas_width = num_cols * frame_width
|
|
canvas_height = num_rows * frame_height
|
|
|
|
print(f"Output image size: {canvas_width}x{canvas_height}")
|
|
|
|
# Create canvas with transparent background
|
|
sprite_sheet = Image.new('RGBA', (canvas_width, canvas_height), (0, 0, 0, 0))
|
|
|
|
# Arrange frames row by row
|
|
for idx, frame in enumerate(frames):
|
|
row = idx // frames_per_row
|
|
col = idx % frames_per_row
|
|
|
|
x = col * frame_width
|
|
y = row * frame_height
|
|
|
|
sprite_sheet.paste(frame, (x, y), frame) # Use frame as mask to preserve transparency
|
|
|
|
# Determine output path
|
|
if output_path is None:
|
|
input_path = Path(input_path)
|
|
output_path = input_path.with_suffix('.png')
|
|
else:
|
|
output_path = Path(output_path)
|
|
|
|
# Save PNG file
|
|
try:
|
|
# Optimize PNG file size
|
|
sprite_sheet.save(
|
|
output_path,
|
|
'PNG',
|
|
optimize=True,
|
|
compress_level=9
|
|
)
|
|
print(f"Saved: {output_path.absolute()}")
|
|
except Exception as e:
|
|
print(f"Error: Failed to save file - {e}")
|
|
sys.exit(1)
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
|
|
# Validate arguments
|
|
if args.frames_per_row <= 0:
|
|
print("Error: Frames per row must be greater than 0")
|
|
sys.exit(1)
|
|
|
|
if args.dim:
|
|
width, height = args.dim
|
|
if width <= 0 or height <= 0:
|
|
print("Error: Dimensions must be greater than 0")
|
|
sys.exit(1)
|
|
target_size = (width, height)
|
|
else:
|
|
target_size = None
|
|
|
|
# Process GIF
|
|
process_gif(
|
|
input_path=args.input,
|
|
frames_per_row=args.frames_per_row,
|
|
target_size=target_size,
|
|
output_path=args.output
|
|
)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main() |