Files
vid2ani/vid2ani.sh
2026-03-04 12:17:47 +01:00

364 lines
10 KiB
Bash
Executable File

#!/bin/bash
# Description: Video to GIF/APNG/WEBP converter
# By: MDHEXT, Nabi KaramAliZadeh, Pathduck
# Version: 6.1
# Url: https://github.com/Pathduck/vid2ani/
# License: GNU General Public License v3.0 (GPLv3)
# Enable error handling
#set -euo pipefail
### Start Main ###
main() {
# Define ANSI Colors
OFF=$(tput sgr0)
RED=$(tput setaf 1)
GREEN=$(tput setaf 10)
YELLOW=$(tput setaf 11)
BLUE=$(tput setaf 12)
CYAN=$(tput setaf 14)
# Check for blank input or help commands
if [[ $# -eq 0 ]]; then print_help; exit; fi
case "$1" in
-h) print_help; exit;;
-?) print_help; exit;;
--help) print_help; exit;;
esac
# Check if ffmpeg exists on PATH, if not exit
if ! command -v 'ffmpeg' >/dev/null 2>&1; then
echo ${RED}"FFmpeg not found in PATH, please install it first"${OFF}; exit 1
fi
# Assign input and output
input="$1"
output=$(basename "${input%.*}")
# Validate input file
if [[ ! -f "$input" ]]; then
echo ${RED}"Input file not found: $input"${OFF}; exit 1
fi
# Set uname for later use
uname_os=$(uname)
# Create working directory
if [[ $uname_os == *"CYGWIN"* ]]; then
WD=$(cygpath -w "$(mktemp -d -t vid2ani-XXXXXX)")
else
WD=$(mktemp -d -t vid2ani-XXXXXX)
fi
# Cleanup on exit, interrupt, termination
trap 'rm -rf "$WD"' EXIT INT TERM
# Clearing input vars and setting defaults
fps=15
mode=1
dither=0
scale="-1"
filetype="gif"
webp_lossy=""
webp_lossy_q=75
loglevel="error"
bayerscale=""
colormax=""
start_time=""
end_time=""
crop=""
errorswitch=""
picswitch=""
playswitch=""
# Parse Arguments, first shift input one left
shift
while [[ $# -gt 0 ]]; do
case "$1" in
-o) [[ ${2##*/} == *.* ]] && output="${2%.*}" || output="$2"; shift;;
-t) filetype="$2"; shift;;
-r) scale="$2"; shift;;
-l) if [[ "$2" =~ ^[0-9]+$ ]]; then
webp_lossy=1; webp_lossy_q="$2"; shift
else webp_lossy=1; fi ;;
-f) fps="$2"; shift;;
-s) start_time="$2"; shift;;
-e) end_time="$2"; shift;;
-d) dither="$2"; shift;;
-b) bayerscale="$2"; shift;;
-m) mode="$2"; shift;;
-c) colormax="$2"; shift;;
-v) loglevel="$2"; shift;;
-x) crop="$2"; shift;;
-k) errorswitch=1;;
-p) picswitch=1;;
-y) playswitch=1;;
*) echo ${RED}"Unknown option $1"${OFF}; exit 1;;
esac
shift
done
# Validate if output file is set and not starts with a -
[[ -z $output || $output == -* ]] && { echo ${RED}"Missing value for -o"${OFF}; exit 1; }
# Validate if output is a directory; strip trailing slash and use input filename
if [[ -d "$output" ]]; then
output="${output%/}/"$(basename "${input%.*}")
echo $output
fi
# Validate output file extension
case $filetype in
gif) output="$output.gif";;
png) output="$output.png"; filetype="apng";;
apng) output="$output.png";;
webp) output="$output.webp";;
*) echo ${RED}"Invalid file type: $filetype"${OFF}; exit 1;;
esac
# Validate Palettegen
if [[ $mode -lt 1 || $mode -gt 3 ]]; then
echo ${RED}"Not a valid palettegen (-m) mode"${OFF}; exit 1
fi
# Validate Dithering
if [[ $dither -gt 8 || $dither -lt 0 ]]; then
echo ${RED}"Not a valid dither (-d) algorithm"${OFF}; exit 1
fi
# Validate Bayerscale
if [[ -n $bayerscale ]]; then
if [[ $bayerscale -gt 5 || $bayerscale -lt 0 ]]; then
echo ${RED}"Not a valid bayerscale (-b) value"${OFF}; exit 1
fi
if [[ $dither -ne 1 ]]; then
echo ${RED}"Bayerscale (-b) only works with Bayer dithering"${OFF}; exit 1
fi
fi
# Validate Lossy WEBP
if [[ -n $webp_lossy ]]; then
if [[ "$filetype" != "webp" ]]; then
echo ${RED}"Lossy (-l) is only valid for filetype webp"${OFF}; exit 1
fi
if [[ $webp_lossy_q -gt 100 || $webp_lossy_q -lt 0 ]]; then
echo ${RED}"Not a valid lossy (-l) quality value"${OFF}; exit 1
fi
fi
# Validate Clipping
if [[ -n "$start_time" && -z "$end_time" ]]; then
echo ${RED}"End time (-e) is required when Start time (-s) is specified."${OFF}; exit 1
elif [[ -n "$end_time" && -z "$start_time" ]]; then
echo ${RED}"Start time (-s) is required when End time (-e) is specified."${OFF}; exit 1
elif [[ -n "$end_time" && -n "$start_time" ]]; then
trim="-ss $start_time -to $end_time"
fi
# Validate Framerate
if [[ $fps -le 0 ]]; then
echo ${RED}"Framerate (-f) must be greater than 0."${OFF}; exit 1
fi
# Validate Max Colors
if [[ -n $colormax && ( $colormax -lt 3 || $colormax -gt 256 ) ]]; then
echo ${RED}"Max colors (-c) must be between 3 and 256."${OFF}; exit 1
fi
# Putting together command to generate palette
palette="$WD/palette_%05d.png"
filters="fps=$fps"
[[ -n "$crop" ]] && filters+=",crop=$crop"
filters+=",scale=$scale:-1:flags=lanczos"
# Fix paths for Cygwin before running ffmpeg/ffplay
if [[ $uname_os == *"CYGWIN"* ]]; then
input=$(cygpath -w "$input")
output=$(cygpath -w "$output")
palette=$(cygpath -w "$palette")
fi
# FFplay preview
if [[ -n $playswitch ]]; then
# Check if ffplay exists on PATH, if not exit
if ! command -v 'ffplay' >/dev/null 2>&1; then
echo ${RED}"FFplay not found in PATH, please install it first"${OFF}; exit 1
fi
echo ${YELLOW}"$(ffplay -version | head -n2)"${OFF}
ffplay -v ${loglevel} -i "${input}" -vf "${filters}" -an -loop 0 -ss ${start_time:-0} -t ${end_time:-3}
exit 0
fi
# APNG muxer does not support multiple palettes, fallback to palettegen diff mode
if [[ $filetype == "apng" && $mode -eq 2 ]]; then
echo ${YELLOW}"APNG does not support multiple palettes - falling back to Palettegen mode 1 (diff)"${OFF}
mode=1
fi
# Palettegen encode mode
case $mode in
1) encode="palettegen=stats_mode=diff";;
2) encode="palettegen=stats_mode=single";;
3) encode="palettegen";;
*) echo ${RED}"Invalid palettegen (-m) mode"${OFF}; exit 1;;
esac
# Max colors
if [[ -n $colormax ]]; then
if [[ $mode -le 2 ]]; then mcol=":max_colors=${colormax}"; fi
if [[ $mode -eq 3 ]]; then mcol="=max_colors=${colormax}"; fi
fi
# Displaying FFmpeg version string and output file
echo ${YELLOW}"$(ffmpeg -version | head -n2)"${OFF}
echo ${GREEN}Output file:${OFF} $output
# Executing command to generate palette
echo ${GREEN}"Generating palette..."${OFF}
ffmpeg -v ${loglevel} ${trim:-} -i "${input}" -vf "${filters},${encode}${mcol:-}" -y "${palette}"
# Checking if the palette file is in the Working Directory, if not cleaning up
if [[ ! -f "$WD/palette_00001.png" ]]; then
echo ${RED}"Palette generation failed: $palette not found."${OFF}; exit 1
fi
## Setting variables to put the encode command together ##
# Palettegen decode mode
if [[ -n $mode ]]; then
case $mode in
1) decode="paletteuse";;
2) decode="paletteuse=new=1";;
3) decode="paletteuse";;
*) echo ${RED}"Invalid palettegen (-m) mode"${OFF}; exit 1;;
esac
fi
# Error diffusion
if [[ -n $errorswitch ]]; then
case $mode in
1) errordiff="=diff_mode=rectangle";;
2) errordiff=":diff_mode=rectangle";;
3) errordiff="=diff_mode=rectangle";;
*) echo ${RED}"Invalid palettegen (-m) mode"${OFF}; exit 1;;
esac
fi
# Prepare dithering and encoding options
case $dither in
0) ditheralg="none";;
1) ditheralg="bayer";;
2) ditheralg="heckbert";;
3) ditheralg="floyd_steinberg";;
4) ditheralg="sierra2";;
5) ditheralg="sierra2_4a";;
6) ditheralg="sierra3";;
7) ditheralg="burkes";;
8) ditheralg="atkinson";;
*) echo ${RED}"Invalid dither (-d ) mode"${OFF}; exit 1;;
esac
# Paletteuse error diffusion
if [[ $mode -ne 2 ]]; then
if [[ -n $errorswitch ]]; then ditherenc=":dither=$ditheralg"; fi
if [[ -z $errorswitch ]]; then ditherenc="=dither=$ditheralg"; fi
else
ditherenc=":dither=$ditheralg"
fi
# Checking for Bayer Scale and adjusting command
if [[ -z $bayerscale ]]; then bayer=""; fi
if [[ -n $bayerscale ]]; then bayer=":bayer_scale=$bayerscale"; fi
# WEBP pixel format and lossy quality
if [[ $filetype == "webp" && -n $webp_lossy ]]; then
type_opts="-lossless 0 -pix_fmt yuva420p -quality $webp_lossy_q"
elif [[ $filetype == "webp" && -z $webp_lossy ]]; then
type_opts="-lossless 1"
fi
# Executing the encoding command
echo ${GREEN}"Encoding animation..."${OFF}
ffmpeg -v ${loglevel} ${trim:-} -i "${input}" -thread_queue_size 512 -i "${palette}" \
-lavfi "${filters} [x]; [x][1:v] ${decode}${errordiff:-}${ditherenc}${bayer}" \
-f ${filetype} ${type_opts:-} -loop 0 -plays 0 -y "${output}"
# Checking if output file was created
if [[ ! -f "$output" ]]; then
echo ${RED}"Failed to generate animation: $output not found"${OFF}; exit 1
fi
# Open output file if picswitch is enabled
if [[ -n $picswitch ]]; then
xdg-open "$output"
fi
echo ${GREEN}"Done."${OFF}
}
### End Main ###
### Function to print the help message ###
print_help() {
cat << EOF
${GREEN}Video to GIF/APNG/WEBP converter v6.1${OFF}
${BLUE}By MDHEXT, Nabi KaramAliZadeh, Pathduck${OFF}
${GREEN}Usage:${OFF}
$(basename "$0") [input_file] [arguments]
${GREEN}Arguments:${OFF}
-o Output file. Default is the same as input file, sans extension
-t Output file type: 'gif' (default), 'apng', 'png', 'webp'
-r Resize (scale) output width in pixels. Default is original input size
-l Enable lossy WebP compression and quality, range 0-100 (default 75)
-f Framerate in frames per seconds (default 15)
-c Maximum colors usable per palette, range 3-256 (default 256)
-s Start time of the animation (HH:MM:SS.MS)
-e End time of the animation (HH:MM:SS.MS)
-x Crop the input video (out_w:out_h:x:y)
Note that cropping occurs before output is scaled
-d Dithering algorithm to be used (default 0)
-b Bayer Scale setting, range 0-5 (default 2)
-m Palettegen mode: 1 (diff, default), 2 (single), 3 (full)
-k Enables paletteuse error diffusion
-y Preview animation using FFplay (part of FFmpeg)
Useful for testing cropping, but will not use exact start/end times
-p Opens the resulting animation in the default image viewer
-v Set FFmpeg log level (default: error)
${GREEN}Dithering Algorithms:${OFF}
0: None
1: Bayer
2: Heckbert
3: Floyd Steinberg
4: Sierra2
5: Sierra2_4a
6: Sierra3
7: Burkes
8: Atkinson
${GREEN}Palettegen Modes:${OFF}
1: diff - only what moves affects the palette
2: single - one palette per frame
3: full - one palette for the whole animation
${GREEN}About Bayerscale:${OFF}
When bayer dithering is selected, the Bayer Scale option defines the
scale of the pattern (how much the crosshatch pattern is visible).
A low value means more visible pattern for less banding, a higher value
means less visible pattern at the cost of more banding.
${GREEN}People who made this project come to fruition:${OFF}
ubitux, Nabi KaramAliZadeh, MDHEXT, Pathduck
Along with the very kind and patient people in the Batch Discord Server.
Without these people's contributions, this script would not be possible.
Thank you all for your contributions and assistance!
EOF
}
### End print_help ###
# Call Main function
main "$@"; exit;