Last active
October 27, 2023 16:52
-
-
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`.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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