#!/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 subtitles -b" echo " use subtitle track -n <int> 0" [[ $hasgsic ]] && echo "optimise with gifsicle -g" 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() { rm -f ${tmp_pref}-palette.png rm -f ${tmp_pref}-in } abort() { [[ "$1" != "" ]] && print_error $1 rm_tmps exit 1 } local start="00:00:00" local length="" local fps=10 local width=480 local subs="" local strack=0 local gsic="" 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]+$' # tmp var used to hold '-t' if length is used local t="" # really annoying, but no other good way to do optional args if [[ $hasgsic ]]; then while getopts :s:t:f:w:bhn:g 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) subs=true ;; n) [[ ! $(echo $OPTARG | grep -oE "$zintpat") ]] \ && usage "sub track index must be a non-negative integer" strack=$OPTARG ;; g) gsic=true ;; h) usage ;; [?]) usage "unrecognised option" ;; esac done else while getopts :s:t:f:w:bhn: 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) subs=true ;; n) [[ ! $(echo $OPTARG | grep -oE "$zintpat") ]] \ && usage "sub track index must be a non-negative integer" strack=$OPTARG ;; 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 tmp_pref="make-gif" tmp_pref="${2:h}/$tmp_pref" echo $tmp_pref while [[ -f "${tmp_pref}-palette.png" ]] || [[ -f "${tmp_pref}-in" ]]; do tmp_pref="${tmp_pref}-1" done 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]}" # convert if [[ $subs ]]; then echo "pass 1..." ffmpeg -loglevel 16 -y -ss $start $t $length -i "${tmp_pref}-in" -copyts \ -vf "subtitles=${tmp_pref}-in:si=$strack,setsar=1/1,fps=$fps,scale=${width}:${height}:flags=lanczos,palettegen" \ ${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 \ "subtitles=${tmp_pref}-in:si=$strack,setsar=1/1,fps=$fps,scale=${width}:${height}:flags=lanczos[x];[x][1:v]paletteuse" \ "$2" || abort [[ $? -ne 0 ]] && fferr=true else echo "pass 1..." ffmpeg -loglevel 16 -y -ss "$start" $t $length -i "${tmp_pref}-in" \ -vf "setsar=1/1,fps=$fps,scale=${width}:${height}:flags=lanczos,palettegen" \ ${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 -filter_complex \ "setsar=1/1,fps=$fps,scale=${width}:${height}:flags=lanczos[x];[x][1:v]paletteuse" \ "$2" [[ $? -ne 0 ]] && fferr=true fi [[ $fferr ]] && abort rm_tmps # gifsicle if [[ -f "$2" && $gsic ]]; then local gsictmp="$2.out" while [[ -f "$gsictmp" ]]; do gsictmp="$gsictmp.out" done echo "optimising..." gifsicle -O3 -i "$2" -o "$gsictmp" [[ $? -eq 0 ]] && rm -f "$2" && mv "$gsictmp" "$2" fi