Last active
March 29, 2025 13:21
-
-
Save judge2020/fdf9387c30647da989dc68744b9e7863 to your computer and use it in GitHub Desktop.
Revisions
-
judge2020 revised this gist
Mar 29, 2025 . No changes.There are no files selected for viewing
-
judge2020 created this gist
Mar 28, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,12 @@ ## Trim transparency from bounds of a gif Sometimes you have a gif that has a transparent pixel border. This is inconvenient when you don't want this for chat-app emojis or stickers. Courtest of chatgpt here's a script that takes in a gif via python input and outputs the trimmed gif. ## prerequisites - ffmpeg - gifski (no video mode needed) - python3 - numpy This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,110 @@ #!/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()