#!/usr/bin/env zsh # export a clip from a video as a gif local callstr=$0 local hasgsic [[ $(whence gifsicle) ]] && hasgsic=true print_error() { echo -e "\e[1;31merror:\e[0m $1\n" } usage() { [[ -z $1 ]] || print_error $1 echo "Usage: $callstr [OPTIONS...] <infile> <outfile>" echo "Create an animated gif from a video" echo "" echo -e " \e[1mdescription opt longform arg default val\e[0m" echo " start time -s --start <time> 00:00:00" echo " length in seconds -l --length <num> full length" echo " gif fps -f --fps <num> 10" echo " output pixel width -w --width <num> 480" echo " use subtitles -b --sub [int] track 0, if enabled" echo " (optionally specify track)" echo " number of colours -c --colours <int> 256" echo " dithering algorithm -d --dither <str> sierra2_4a" echo "redraw only changed rectangle -r --rect" [[ $hasgsic ]] && echo " optimise with gifsicle -o --optimise" echo " print this help -h --help" 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() { [[ -z $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}|0\.[0-9]*[1-9])$' local intpat='^[1-9][0-9]*$' local zintpat='^[0-9]+$' local dithpat='^(none|bayer[0-5]|heckbert|floyd_steinberg|sierra2|sierra2_4a)$' zparseopts -D -E -M -A args \ s: -start:=s \ l: -length:=l \ f: -fps:=f \ w: -width:=w \ b:: -sub::=b \ c: -colours:=c -colors:=c \ d: -dither:=d \ r -rect=r \ o -optimise=o -optimize=o \ h -help=h local optarg for opt in ${(@k)args}; do unset optarg [[ -z $args[$opt] ]] || optarg=$args[$opt] case $opt in -s) [[ ! $(echo $optarg | grep -oE "$timepat") ]] \ && usage "malformed start timestamp" start="$optarg" ;; -l) [[ ! $(echo $optarg | grep -oE "$numpat") ]] \ && usage "length must be a positive rational number" length=(-t $optarg) ;; -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) [[ ! -z $optarg ]] && [[ ! $(echo $optarg | grep -oE "$zintpat") ]] \ && usage "sub track index must be a non-negative integer" [[ -z $optarg ]] && strack=0 || start=$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 none, bayer<0-5>, heckbert,\nfloyd_steinberg, sierra2, sierra2_4a" dithalg=$optarg [[ $(echo $optarg | grep -o bayer) ]] \ && dithalg="bayer:bayer_scale=$(echo $optarg | grep -oE '[0-5]')" ;; -o) gsic=true; [[ -z $hasgsic ]] && usage "gifsicle program not found" ;; -r) rectmode=":diff_mode=rectangle" ;; -h) usage ;; esac done # check some error conditions if [[ ${#@} -gt 2 ]]; then for opt in $@; do [[ $opt[1] == '-' ]] && usage "unrecognised option $opt" done usage "trailing file arguments detected" fi [[ ${#@} -eq 1 ]] && usage "no output file specified" [[ ${#@} -eq 0 ]] && usage "no input file specified" [[ $1 != "%d.png" ]] && [[ ! -f $1 ]] && usage "input file not found" [[ ${2:e} != "gif" ]] && usage "output file must have a .gif file extension" # make links local tmp_pref_i tmp_pref_i="${2:h}/make-gif" 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 [[ -z $as ]] || [[ ${as[1]} -eq 0 ]] || [[ ${as[2]} -eq 0 ]] \ || let "height = (${width} * ${as[2]}) / ${as[1]}" # convenience var local substr [[ $subs ]] && substr="subtitles=${tmp_pref}-in:si=$strack," # convert echo "pass 1..." ffmpeg -loglevel 16 -y -ss "$start" ${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 ${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