aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkatherine <shmibs@shmibbles.me>2017-03-24 01:23:39 -0700
committerkatherine <shmibs@shmibbles.me>2017-03-24 01:23:39 -0700
commitb189b570267d1b6c057c4b4121c191d6f988fad8 (patch)
treedb15bcb0a9b5abfc0be73d66e58b6a8a70522f91
downloadmake-gif-b189b570267d1b6c057c4b4121c191d6f988fad8.tar.gz
initial commit
-rw-r--r--Readme.md24
-rwxr-xr-xmake-gif202
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