|
#!/usr/bin/env python3 |
|
import os |
|
import sys |
|
import glob |
|
import subprocess |
|
import numpy as np |
|
from PIL import Image |
|
import time |
|
import re |
|
|
|
def get_nontransparent_bbox(im): |
|
""" |
|
Returns the bounding box (left, top, right, bottom) of the non-transparent region. |
|
If the image is completely transparent, returns the full image box. |
|
""" |
|
im = im.convert("RGBA") |
|
arr = np.array(im) |
|
# Mask: True where alpha channel is non-zero (non-transparent) |
|
mask = arr[..., 3] != 0 |
|
coords = np.argwhere(mask) |
|
if coords.size == 0: |
|
return (0, 0, im.width, im.height) |
|
y0, x0 = coords.min(axis=0) |
|
y1, x1 = coords.max(axis=0) |
|
# PIL crop: right and bottom are non-inclusive, so add 1 |
|
return (x0, y0, x1 + 1, y1 + 1) |
|
|
|
def safe_directory_name(name): |
|
""" |
|
Creates a directory-safe string by replacing non-alphanumeric characters with underscores. |
|
""" |
|
return re.sub(r'[^A-Za-z0-9_\-]', '_', name) |
|
|
|
def main(): |
|
# Prompt user for the input GIF file |
|
gif_file = input("Enter the name of the GIF file (in current directory): ").strip() |
|
if not os.path.isfile(gif_file): |
|
print(f"Error: File '{gif_file}' does not exist.") |
|
sys.exit(1) |
|
|
|
# Create a unique directory name for extracted frames |
|
base_name = os.path.splitext(os.path.basename(gif_file))[0] |
|
safe_name = safe_directory_name(base_name) |
|
frames_dir = safe_name + "_frames" |
|
if os.path.exists(frames_dir): |
|
# Append a timestamp to ensure uniqueness |
|
frames_dir += "_" + str(int(time.time())) |
|
os.makedirs(frames_dir, exist_ok=True) |
|
print(f"Extracting frames to directory: {frames_dir}") |
|
|
|
# Extract frames using ffmpeg |
|
ffmpeg_cmd = f'ffmpeg -i "{gif_file}" "{frames_dir}/%04d.png"' |
|
print(f"Running: {ffmpeg_cmd}") |
|
subprocess.run(ffmpeg_cmd, shell=True, check=True) |
|
|
|
# List extracted PNG files (sorted) |
|
png_files = sorted(glob.glob(os.path.join(frames_dir, "*.png"))) |
|
if not png_files: |
|
print("Error: No PNG frames found after extraction.") |
|
sys.exit(1) |
|
|
|
# Compute the union bounding box of non-transparent areas from all frames |
|
global_left, global_top = float('inf'), float('inf') |
|
global_right, global_bottom = 0, 0 |
|
|
|
for png in png_files: |
|
im = Image.open(png) |
|
left, top, right, bottom = get_nontransparent_bbox(im) |
|
global_left = min(global_left, left) |
|
global_top = min(global_top, top) |
|
global_right = max(global_right, right) |
|
global_bottom = max(global_bottom, bottom) |
|
|
|
print(f"Global bounding box: left={global_left}, top={global_top}, right={global_right}, bottom={global_bottom}") |
|
|
|
# Create a directory for cropped images |
|
cropped_dir = "cropped" |
|
if os.path.exists(cropped_dir): |
|
cropped_dir = safe_name + "_cropped" |
|
if os.path.exists(cropped_dir): |
|
cropped_dir += "_" + str(int(time.time())) |
|
os.makedirs(cropped_dir, exist_ok=True) |
|
|
|
# Crop each PNG to the union bounding box and save into the cropped directory |
|
for png in png_files: |
|
im = Image.open(png) |
|
cropped = im.crop((global_left, global_top, global_right, global_bottom)) |
|
filename = os.path.basename(png) |
|
cropped.save(os.path.join(cropped_dir, filename)) |
|
print(f"Cropped and saved {filename}") |
|
|
|
# Extract the frame rate from the original GIF using ffprobe |
|
ffprobe_cmd = ( |
|
f'ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate ' |
|
f'-of default=noprint_wrappers=1:nokey=1 "{gif_file}"' |
|
) |
|
fps_output = subprocess.check_output(ffprobe_cmd, shell=True).decode().strip() |
|
num, denom = fps_output.split('/') |
|
fps = float(num) / float(denom) |
|
print(f"Extracted frame rate: {fps} fps") |
|
|
|
# Use gifski to create the output GIF from the cropped images |
|
output_gif = safe_name + "_output.gif" |
|
gifski_cmd = f'gifski --fps {fps} -o "{output_gif}" {cropped_dir}/*.png' |
|
print(f"Running: {gifski_cmd}") |
|
subprocess.run(gifski_cmd, shell=True, check=True) |
|
print(f"Created output GIF: {output_gif}") |
|
|
|
if __name__ == "__main__": |
|
main() |