← Home

Even better GIF conversion with gifski

2025-12-29

#!/bin/bash

# Default values
SCALE_DEFAULT="2.5"
FPS_DEFAULT="20"
DROP_FRAMES_DEFAULT="1"
MAX_SIZE_MB=40
# Lowered threshold: ~80M pixels usually hits the 30-40MB range with gifski
HEURISTIC_THRESHOLD=80000000 

SCALE="$SCALE_DEFAULT"
FPS="$FPS_DEFAULT"
DROP_FRAMES="$DROP_FRAMES_DEFAULT"
LOOP_FLAG=0

# Check for required dependencies
for cmd in ffmpeg ffprobe gifski bc; do
    if ! command -v "$cmd" &> /dev/null; then
        echo "Error: $cmd is not installed." >&2
        exit 1
    fi
done

# Parse arguments
while [ $# -gt 0 ]; do
    case "$1" in
        -s) SCALE="$2"; shift 2 ;;
        -f) FPS="$2"; shift 2 ;;
        -l) LOOP_FLAG=1; shift ;;
        -d) DROP_FRAMES="$2"; shift 2 ;;
        *)  VIDEO="$1"; shift ;;
    esac
done

if [ -z "$VIDEO" ] || [ ! -f "$VIDEO" ]; then
    echo "Usage: $0 [-s scale] [-f fps] [-l] [-d drop_frames] <video>"
    exit 1
fi

TMPDIR=${TMPDIR:-/tmp}
TMP_PREFIX="giffify_$(basename "$VIDEO" | sed 's/[^a-zA-Z0-9_-]/_/g')_$$"

# 1. Efficient Metadata Extraction
video_info=$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height,duration -of csv=p=0:s=x "$VIDEO")
width=$(echo "$video_info" | cut -d'x' -f1)
height=$(echo "$video_info" | cut -d'x' -f2)
duration=$(echo "$video_info" | cut -d'x' -f3)

# 2. Fast Heuristic Check
scaled_w=$(echo "$width / $SCALE" | bc)
scaled_h=$(echo "$height / $SCALE" | bc)
total_frames=$(echo "$duration * $FPS" | bc)
[ "$LOOP_FLAG" = "1" ] && total_frames=$(echo "$total_frames * 2" | bc)

total_pixels=$(echo "$scaled_w * $scaled_h * $total_frames" | bc)

# Use bc to handle the comparison of potential floats
is_large=$(echo "$total_pixels > $HEURISTIC_THRESHOLD" | bc)

if [ "$is_large" -eq 1 ]; then
    echo "Video pixel volume (${total_pixels%.*}px) suggests a large file. Sampling..."
    sample_gif="$TMPDIR/${TMP_PREFIX}_sample.gif"
    start_time=$(echo "$duration / 2" | bc)
    
    ffmpeg -v quiet -ss "$start_time" -t 1 -i "$VIDEO" \
        -vf "fps=${FPS},scale=iw/${SCALE}:-1:flags=lanczos" -f yuv4mpegpipe - | \
        gifski -o "$sample_gif" - &>/dev/null
    
    sample_size_kb=$(du -k "$sample_gif" | cut -f1)
    rm -f "$sample_gif"

    total_duration="$duration"
    [ "$LOOP_FLAG" = "1" ] && total_duration=$(echo "$duration * 2" | bc -l)
    est_mb=$(echo "$sample_size_kb * $total_duration / 1024" | bc -l)
    
    if [ "$(echo "$est_mb > $MAX_SIZE_MB" | bc -l)" -eq 1 ]; then
        echo "⚠️  Warning: Estimated size is high (~${est_mb%.*}MB)" >&2
        suggested_scale=$(echo "$SCALE + 1.0" | bc)
        suggested_fps=$(echo "$FPS * $MAX_SIZE_MB / $est_mb" | bc)
        [ "$suggested_fps" -lt 10 ] && suggested_fps=10

        echo "   - Try: -s ${suggested_scale} or -f ${suggested_fps}" >&2
        read -p "   Continue, Optimize, or Abort? [C/o/a] " -r choice
        case "$choice" in
            [Oo]*) SCALE="$suggested_scale"; FPS="$suggested_fps"; echo "   🔧 Optimizing..." ;;
            [Aa]*) echo "   ❌ Aborted."; exit 1 ;;
            *) echo "   ⚠️  Continuing..." ;;
        esac
    fi
else
    echo "Video within safe pixel limits (${total_pixels%.*}px). Skipping estimate."
fi

# 3. Final Generation
if [ "$LOOP_FLAG" = "1" ]; then
    OUTPUT="loop_$(basename "${VIDEO%.*}.gif")"
    echo "Creating boomerang loop -> $OUTPUT"
    ffmpeg -v quiet -i "$VIDEO" -vf "fps=${FPS},scale=iw/${SCALE}:-1:flags=lanczos,split[orig][rev];[rev]reverse[rev_done];[orig][rev_done]concat=n=2:v=1:a=0" -f yuv4mpegpipe - | gifski -o "$OUTPUT" -
else
    OUTPUT="$(basename "${VIDEO%.*}.gif")"
    echo "Creating GIF -> $OUTPUT"
    ffmpeg -v quiet -i "$VIDEO" -vf "fps=${FPS},scale=iw/${SCALE}:-1:flags=lanczos" -f yuv4mpegpipe - | gifski -o "$OUTPUT" -
fi

# 4. Final Report
file_size=$(stat -c%s "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null)
actual_mb=$(echo "$file_size / 1024 / 1024" | bc -l)
echo "Done! Final size: ${actual_mb%.*}MB"

Usage:

Dependencies: