#!/usr/bin/env zsh # export a clip from a video as a gif local callstr="$0" local hasgsic=$(whence gifsicle) print_error() { echo -e "\e[1;31merror:\e[0m $1\n" } usage() { [[ "$1" != "" ]] && print_error $1 echo "Usage: $callstr [OPTIONS...] <infile> <outfile>" echo "" echo " description option default val" echo " start time -s <time> 00:00:00" echo " length in seconds -t <num> full length" echo " gif fps -f <num> 10" echo " gif pixel width -w <num> 480" echo " use subtitle track -b <int> none" echo " number of colours -c <int> 256" echo " dithering algorithm -d <str> sierra2_4a" echo "redraw only changed rect -r" [[ $hasgsic ]] && echo " optimise with gifsicle -o" echo " print this help -h" exit 1 } # preface used to ensure unique inputs, just in case links exist. see below local tmp_pref rm_tmps() { if [[ ${tmp_pref} ]]; then rm -f ${tmp_pref}-palette.png rm -f ${tmp_pref}-in fi } abort() { [[ "$1" != "" ]] && print_error $1 rm_tmps exit 1 } trap 'abort' SIGABRT SIGHUP SIGINT SIGQUIT SIGTERM local start="00:00:00" local length="" local fps=10 local width=480 local subs="" local strack=0 local gsic="" local dithalg="sierra2_4a" local colour_count=256 local rect="" local timepat='^(([0-9][0-9]:){1,2}[0-9][0-9]|[0-9]+)(\.[0-9]+){0,1}$' local numpat='^[1-9][0-9]*(\.[0-9]+){0,1}$' local intpat='^[1-9][0-9]*$' local zintpat='^[0-9]+$' local dithpat='^(bayer[0-5]|heckbert|floyd_steinberg|sierra2|sierra2_4a)$' # used to hold '-t' if length is used local t="" # used to hold subtitle options, if present local substr="" # really annoying, but no other good way to do optional args if [[ $hasgsic ]]; then while getopts :s:t:f:w:b:c:d:rho opt; do case "$opt" in s) [[ ! $(echo $OPTARG | grep -oE "$timepat") ]] \ && usage "malformed start timestamp" start="$OPTARG" ;; t) [[ ! $(echo $OPTARG | grep -oE "$numpat") ]] \ && usage "length must be a positive rational number" length=$OPTARG t="-t" ;; f) [[ ! $(echo $OPTARG | grep -oE "$numpat") ]] \ && usage "fps must be a positive rational number" fps=$OPTARG ;; w) [[ ! $(echo $OPTARG | grep -oE "$intpat") ]] \ && usage "width must be a positive integer" width=$OPTARG ;; b) [[ ! $(echo $OPTARG | grep -oE "$zintpat") ]] \ && usage "sub track index must be a non-negative integer" strack=$OPTARG subs=true ;; c) [[ ! $(echo $OPTARG | grep -oE "$intpat") ]] \ || [[ $OPTARG -gt 256 || $OPTARG -lt 2 ]] \ && usage "colour count must be an integer in the range 2-256" colour_count=$OPTARG ;; d) [[ ! $(echo $OPTARG | grep -oE "$dithpat") ]] \ && usage "dithering algorithm must be one of bayer<0-5>, heckbert, floyd_steinberg, sierra2, sierra2_4a" dithalg=$OPTARG [[ $(echo $OPTARG | grep -o bayer) ]] \ && dithalg="bayer:bayer_scale=$(echo $OPTARG | grep -oE '[0-5]')" ;; o) gsic=true ;; r) rectmode=":diff_mode=rectangle" ;; h) usage ;; [?]) usage "unrecognised option" ;; esac done else while getopts :s:t:f:w:b:c:d:rh opt; do case "$opt" in s) [[ ! $(echo $OPTARG | grep -oE "$timepat") ]] \ && usage "malformed start timestamp" start="$OPTARG" ;; t) [[ ! $(echo $OPTARG | grep -oE "$numpat") ]] \ && usage "length must be a positive rational number" length=$OPTARG t="-t" ;; f) [[ ! $(echo $OPTARG | grep -oE "$numpat") ]] \ && usage "fps must be a positive rational number" fps=$OPTARG ;; w) [[ ! $(echo $OPTARG | grep -oE "$intpat") ]] \ && usage "width must be a positive integer" width=$OPTARG ;; b) [[ ! $(echo $OPTARG | grep -oE "$zintpat") ]] \ && usage "sub track index must be a non-negative integer" strack=$OPTARG subs=true ;; c) [[ ! $(echo $OPTARG | grep -oE "$intpat") ]] \ || [[ $OPTARG -gt 256 || $OPTARG -lt 2 ]] \ && usage "colour count must be an integer in the range 2-256" colour_count=$OPTARG ;; d) [[ ! $(echo $OPTARG | grep -oE "$dithpat") ]] \ && usage "dithering algorithm must be one of bayer<0-5>, heckbert, floyd_steinberg, sierra2, sierra2_4a" dithalg=$OPTARG [[ $(echo $OPTARG | grep -o bayer) ]] \ && dithalg="bayer:bayer_scale=$(echo $OPTARG | grep -oE '[0-5]')" ;; r) rectmode=":diff_mode=rectangle" ;; h) usage ;; [?]) usage "unrecognised option" ;; esac done fi shift $OPTIND-1 # check some error conditions [[ ${#@} -gt 2 ]] && usage "trailing arguments detected" [[ ${#@} -lt 2 ]] && usage "no output file specified" [[ "${2:e}" != "gif" ]] && usage "output file must have a .gif file extension" [[ "$1" != "%d.png" ]] && [[ ! -f "$1" ]] && usage "input file not found" # make links local tmp_pref_i tmp_pref_i="make-gif" tmp_pref_i="${2:h}/${tmp_pref_i}" while [[ -f "${tmp_pref_i}-palette.png" ]] || [[ -f "${tmp_pref_i}-in" ]]; do tmp_pref_i="${tmp_pref_i}-1" done tmp_pref="${tmp_pref_i}" ln -s "${1:a}" "${tmp_pref}-in" \ || abort "could not write to output dir" # get output height using aspect ratio local height local as height=-1 ffprobe -loglevel -8 -print_format json -show_streams "${tmp_pref}-in" \ | grep -m 1 display_aspect_ratio | grep -Eo '[0-9]+:[0-9]+' \ | IFS=':' read -A as [[ "$as" != "" ]] && let "height = ${width} * ${as[2]} / ${as[1]}" [[ $subs ]] && substr="subtitles=${tmp_pref}-in:si=$strack," # convert echo "pass 1..." ffmpeg -loglevel 16 -y -ss "$start" $t $length -i "${tmp_pref}-in" -copyts -filter_complex \ "${substr}setsar=1/1,fps=$fps,scale=${width}:${height}:flags=lanczos,palettegen=max_colors=${colour_count}" \ ${tmp_pref}-palette.png [[ $? -ne 0 ]] && fferr=true [[ ! $fferr ]] && echo "pass 2..." && ffmpeg -loglevel 24 \ -ss $start $t $length \ -i "${tmp_pref}-in" -i ${tmp_pref}-palette.png -copyts -filter_complex \ "${substr}setsar=1/1,fps=$fps,scale=${width}:${height}:flags=lanczos[x];[x][1:v]paletteuse=dither=${dithalg}${rectmode}" \ "$2" [[ $? -ne 0 ]] && fferr=true [[ $fferr ]] && abort rm_tmps # gifsicle if [[ -f "$2" && $gsic ]]; then echo "optimising..." gifsicle --batch -O3 -i "$2" fi