Skip to content

Instantly share code, notes, and snippets.

@Winterhuman
Last active October 27, 2023 16:52
Show Gist options
  • Save Winterhuman/e65fe54f3e47b0c26b0e6ad980327f0a to your computer and use it in GitHub Desktop.
Save Winterhuman/e65fe54f3e47b0c26b0e6ad980327f0a to your computer and use it in GitHub Desktop.
POSIX sh script to create AVIF for target SSIM value using binary search, requires `imagemagick` and `cavif`.
#!/bin/sh
# Licensed under the Zero-Clause BSD terms: https://opensource.org/license/0bsd
# Requires `imagemagick` and `cavif`.
## Arguments
# 1: /path/to/input
# 2: /path/to/output (optional, default output: 'input_no_ext'-'quality'.avif)
# 3: Target SSIM value (default value: 96%)
# Since the SSIM score is output as a 6 decimal place number (e.g. 0.960000),
# the value given will be converted into the closest allowed value as shown:
# 96 = 96% (all values can be prefixed with '0.')
# 855 = 85.5%
# 123456 = 12.3456%
# 5 = 50%
# 1000000 (100%) = 10%
pquit() { printf "\033[1m\033[31m%b\033[0;39m" "$1"; exit 1; }
pstat() { printf "\033[1m\033[34m%b\033[0;39m\033[1m%b\033[0;39m\n" "$1" "$2"; }
clean() { if ! rm -r "$tmp"; then pquit "Failed to delete '$tmp'!\n"; fi }
trap clean EXIT
tmp="$(mktemp -d)"
if [ -z "$1" ]; then pquit "No input image given!\n"; fi
if [ ! -f "$1" ]; then pquit "'$1' does not exist, or isn't a file!\n"; fi
input="$1"
pstat "Input: " "$input"
# `cavif` only supports JPG & PNG for its input, so convert all other image
# formats to PNG.
tmp_input="${tmp}/input.png"
mime="$(file -ib "$input")"
if [ "${mime%;*}" = "image/png" ] || [ "${mime%;*}" = "image/jpeg" ]; then
if ! cp "$input" "$tmp_input"; then
pquit "Failed to copy '$input' to '$tmp_input'!\n"; fi
else
if ! convert "$input" "$tmp_input"; then
pquit "Failed to convert '$input' to PNG!\n"; fi
fi
fancy_percent() {
fancy_percent_digit="$(printf "%d" "$1" | cut -c -2)"
fancy_percent_decimal="$(printf "%s" "$1" | cut -c 3-)"
printf "%s.%s%%" "${fancy_percent_digit}" "${fancy_percent_decimal}"
}
ideal="${3:-0.96}"
ideal="$(printf "%d0000" "${ideal#*.}" | cut -c -6)"
pstat "Target: " "~$(fancy_percent "$ideal")"
# Only complain about an existing output if it was explicitly given.
if [ -n "$2" ]; then
final_output="$2"
pstat "Output: " "$final_output\n"
if [ -e "$final_output" ]; then
pquit "'$final_output' already exists!\n"; fi
else
printf "\n"
fi
bad_quality="1"
quality="50"
break_quality="99"
upper_quality="$break_quality"
# Use binary search to find the lowest quality image which meets or exceeds the
# target SSIM score.
while [ "$bad_quality" -le "$(( "$upper_quality" - 1 ))" ]; do
trial="${tmp}/${quality}-trial.avif"
pstat "Quality: " "$quality"
if ! cavif --quiet --overwrite --threads "$(nproc)" --speed 1 --quality "$quality" "$tmp_input" --output "$trial"; then
pquit "Failed to create '$trial' from '$tmp_input'!\n"; fi
# The order you pass the images in for `compare` matters; the trial must
# come first.
score="$(compare -metric SSIM "$trial" "$tmp_input" null: 2>&1)"
score_int="$(printf "%d0000" "${score#*.}" | cut -c -6)"
score_int_fancy="$(fancy_percent "$score_int")"
if [ "$score_int" -lt "$ideal" ]; then
pstat "\033[31mBad score: " "${score_int_fancy}\n"
if ! rm "$trial"; then
pquit "Failed to remove '$trial'!\n"; fi
bad_quality="$(( "$quality" + 1 ))"
else
pstat "\033[32mGood score: " "${score_int_fancy}\n"
good_trial="$trial"
upper_quality="$quality"
fi
if [ "$quality" -ge "$break_quality" ]; then break; fi
# This has to go at the end since otherwise '$good_trial' will be reset
# to the upcoming '$quality' value, and not the last good one.
quality="$(( ("$bad_quality" + "$upper_quality") / 2 ))"
done
final_output="${2:-${input%.*}-${quality}.avif}"
if [ -z "$2" ]; then
pstat "Output: " "$final_output"; fi
if ! mv "$good_trial" "$final_output"; then
# Give user enough time to move the good trial themselves if desired.
printf "\033[1m\033[31mFailed to move '%s' to '%s'! Closing in 30 seconds.\033[0;39m\n" "$good_trial" "$final_output"
sleep 30
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment