Skip to content

Instantly share code, notes, and snippets.

@judge2020
Last active March 29, 2025 13:21
Show Gist options
  • Select an option

  • Save judge2020/fdf9387c30647da989dc68744b9e7863 to your computer and use it in GitHub Desktop.

Select an option

Save judge2020/fdf9387c30647da989dc68744b9e7863 to your computer and use it in GitHub Desktop.

Revisions

  1. judge2020 revised this gist Mar 29, 2025. No changes.
  2. judge2020 created this gist Mar 28, 2025.
    12 changes: 12 additions & 0 deletions _README.md
    Original 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
    110 changes: 110 additions & 0 deletions crop_and_create_gif.py
    Original 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()