diff options
author | katherine <shmibs@shmibbles.me> | 2017-03-24 01:23:39 -0700 |
---|---|---|
committer | katherine <shmibs@shmibbles.me> | 2017-03-24 01:23:39 -0700 |
commit | b189b570267d1b6c057c4b4121c191d6f988fad8 (patch) | |
tree | db15bcb0a9b5abfc0be73d66e58b6a8a70522f91 | |
download | make-gif-b189b570267d1b6c057c4b4121c191d6f988fad8.tar.gz |
initial commit
-rw-r--r-- | Readme.md | 24 | ||||
-rwxr-xr-x | make-gif | 202 |
2 files changed, 226 insertions, 0 deletions
diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..73f470a --- /dev/null +++ b/Readme.md @@ -0,0 +1,24 @@ +make-gif +======== + +despite the recent rise of libvpx, there's still something magical about the +humble animated gif and it's status as not-quite-video-or-image. however, given +the limitations of the file format, creating gifs which can hold their own +against these modern video formats is not easy. + +this script aims to provide a featureful frontend to +[ffmpeg](https://ffmpeg.org/) which makes creating high-quality gifs from video +files as painless as possible, with support for things like embedding +subtitles, various dithering algorithms, cropping, and post-optimisation (via +[gifsicle](https://www.lcdf.org/gifsicle/)) + +just clone and run `make-gif --help` to get started! + +dependencies +============ + +in addition to being run via zsh, make-gif makes use the following external +commands: + +* **required**: `ffmpeg`, `ffprobe`, `grep` +* **optional**: `gifsicle` diff --git a/make-gif b/make-gif new file mode 100755 index 0000000..320a4a0 --- /dev/null +++ b/make-gif @@ -0,0 +1,202 @@ +#!/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 |