Even better GIF conversion with gifski
2025-12-29
- gifski
- better boomerang loop
- nags about size
#!/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:
giffify video.mp4- convert to GIF with gifski (better quality)giffify -l video.mp4- convert with boomerang loopgiffify -s 2 -f 20 video.mp4- custom scale (higher is smaller) and FPS
Dependencies:
ffmpeg- video processingffprobe- metadata extractiongifski- high-quality GIF encoderbc- calculations