Created
January 26, 2026 15:42
-
-
Save tgroshon/491d7292673c369e7909cf8b2003a299 to your computer and use it in GitHub Desktop.
MOV to GIF on macOS
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 characters
| #!/bin/zsh | |
| # mov2gif - Convert MOV video to optimized GIF | |
| # Usage: mov2gif input.mov [output.gif] [options] | |
| set -e | |
| # Defaults | |
| fps=10 | |
| width=320 | |
| start="00:00:00" | |
| duration="" | |
| usage() { | |
| cat <<EOF | |
| Usage: $(basename $0) input.mov [output.gif] [options] | |
| Options: | |
| -f, --fps NUM Frame rate (default: 10) | |
| -w, --width NUM Width in pixels, height auto-scaled (default: 320) | |
| -s, --start TIME Start time, e.g., 00:00:05 (default: 00:00:00) | |
| -d, --duration TIME Duration, e.g., 00:00:10 (default: full length) | |
| --setup Install dependencies (ffmpeg, imagemagick) via Homebrew | |
| -h, --help Show this help | |
| EOF | |
| exit 1 | |
| } | |
| # Parse arguments | |
| input="" | |
| output="" | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| -f|--fps) fps="$2"; shift 2 ;; | |
| -w|--width) width="$2"; shift 2 ;; | |
| -s|--start) start="$2"; shift 2 ;; | |
| -d|--duration) duration="$2"; shift 2 ;; | |
| --setup) | |
| echo "Installing dependencies..." | |
| brew install ffmpeg imagemagick | |
| echo "Setup complete!" | |
| exit 0 ;; | |
| -h|--help) usage ;; | |
| -*) echo "Unknown option: $1"; usage ;; | |
| *) | |
| if [[ -z "$input" ]]; then | |
| input="$1" | |
| elif [[ -z "$output" ]]; then | |
| output="$1" | |
| fi | |
| shift ;; | |
| esac | |
| done | |
| [[ -z "$input" ]] && usage | |
| [[ ! -f "$input" ]] && echo "Error: File not found: $input" && exit 1 | |
| # After parsing, before conversion | |
| if ! command -v ffmpeg &>/dev/null || ! command -v magick &>/dev/null; then | |
| echo "Error: Missing dependencies. Run '$0 --setup' to install." | |
| exit 1 | |
| fi | |
| # Default output name | |
| [[ -z "$output" ]] && output="${input:r}.gif" | |
| # Build duration flag | |
| duration_flag="" | |
| [[ -n "$duration" ]] && duration_flag="-t $duration" | |
| # Temp file for unoptimized GIF | |
| tmp_gif=$(mktemp /tmp/gif_convert.XXXXXX.gif) | |
| trap "rm -f $tmp_gif" EXIT | |
| echo "Converting $input -> $output" | |
| echo " FPS: $fps, Width: $width, Start: $start${duration:+, Duration: $duration}" | |
| # Convert with ffmpeg (using palettegen for better colors) | |
| palette=$(mktemp /tmp/palette.XXXXXX.png) | |
| trap "rm -f $tmp_gif $palette" EXIT | |
| ffmpeg -hide_banner -loglevel warning \ | |
| -ss "$start" -i "$input" $duration_flag \ | |
| -vf "fps=$fps,scale=$width:-1:flags=lanczos,palettegen=stats_mode=diff" \ | |
| -y "$palette" | |
| ffmpeg -hide_banner -loglevel warning \ | |
| -ss "$start" -i "$input" $duration_flag \ | |
| -i "$palette" \ | |
| -lavfi "fps=$fps,scale=$width:-1:flags=lanczos [x]; [x][1:v] paletteuse=dither=bayer:bayer_scale=5" \ | |
| -y "$tmp_gif" | |
| # Optimize with ImageMagick | |
| echo "Optimizing..." | |
| magick "$tmp_gif" -layers Optimize "$output" | |
| # Report sizes | |
| orig_size=$(stat -f%z "$tmp_gif" 2>/dev/null || stat -c%s "$tmp_gif") | |
| final_size=$(stat -f%z "$output" 2>/dev/null || stat -c%s "$output") | |
| savings=$(( (orig_size - final_size) * 100 / orig_size )) | |
| echo "Done! $output" | |
| echo " Size: $(( final_size / 1024 ))KB (saved ${savings}% from optimization)" | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment