Skip to content

Instantly share code, notes, and snippets.

@javabean
Created July 27, 2024 21:10
Show Gist options
  • Save javabean/6360b087d6d5cf3608ad685723e02f87 to your computer and use it in GitHub Desktop.
Save javabean/6360b087d6d5cf3608ad685723e02f87 to your computer and use it in GitHub Desktop.
image-best-format.sh
#!/bin/sh
# shellcheck disable=SC3043
set -e
set -u
#(set -o | grep -q pipefail) && set -o pipefail
#(set -o | grep -q posix) && set -o posix
#shopt -s failglob
#set -x
# Compute the most optimal (file size) format for an input image
# input:
# * a set of images to study
# * a threshold from wich a format change is recommanded (net gain of x% in file size)
# output:
# * an Excel spreadsheet detailing the most optimal format for each image, along with the transcoded image sizes for all supported output formats
# * the transcoded images where changing format makes sense
# If running this script publicly, don't forget to setup an ImageMagick policy:
# https://imagemagick.org/script/security-policy.php
# Copyright 2020-2024 Cédrik LIME
CONST_OUTPUT_FORMATS="${OUTPUT_FORMATS:-GIF JPEG PNG WebP AVIF}"
##############################################################################
# Shell utilities
##############################################################################
# Usage:
# local reset_e=0 ; internal_is_shell_attribute_set "e" && set +e && reset_e=1
# ...
# [ "$reset_e" -eq 1 ] && set -e
internal_is_shell_attribute_set() { # attribute, like "e"
# Alternative implementation (e.g. for set -x): [ ${-/x} != ${-} ] && tracing=1 || tracing=0
#local search_attribute=$1
case "$-" in
*"$1"*) return 0 ;;
*) return 1 ;;
esac
}
internal_is_shell_option_set() { # option, like "pipefail"
# Note: bash-specific alternative: `test -o`
local search_option="$1"
case $(set -o | grep "$search_option" | cut -f2) in
on) return 0 ;;
off) return 1 ;;
*) echo "Error: unknown shell option value \"$search_option\"!" >&2; return 1 ;;
esac
}
# shellcheck disable=SC2034
LOG_LEVEL_TRACE=0
# shellcheck disable=SC2034
LOG_LEVEL_DEBUG=1
# shellcheck disable=SC2034
LOG_LEVEL_INFO=2
# shellcheck disable=SC2034
LOG_LEVEL_WARN=3
# shellcheck disable=SC2034
LOG_LEVEL_ERROR=4
LOG_LEVEL=$LOG_LEVEL_INFO
log() {
local REQUESTED_LOG_LEVEL="${1}"
shift
# shellcheck disable=SC2086
if [ $REQUESTED_LOG_LEVEL -ge $LOG_LEVEL ]; then
echo "$@"
fi
}
##############################################################################
print_usage() {
cat << EOT
Optimise images by computing the best (file size) format.
Supported input formats: all of underlying ImageMagick (see https://imagemagick.org/script/formats.php).
Supported output formats: JPEG, PNG, WebP, GIF.
${0##*/} will output (on stdout) the results in tabular format (tsv).
Usage
${0##*/} [-q|-v] [-t threshold] [-o directory] file [file...]
-t threshold in percent for which we report a better image format, from 0 to 100; default: 10
-o directory to which write images in new format
no transcoding will be done if not specified
-q quiet
-v verbose
-h this help
E.g.: ${0##*/} -t 5 myImage1.jpg myImage2.png
E.g.: ${0##*/} -q -t 5 -d target_directory myImage1.jpg myImage2.png
EOT
}
##############################################################################
# See https://imagemagick.org/script/defines.php and https://imagemagick.org/script/webp.php
IMAGICK_STD_DEFINES="-define preserve-timestamp=true -define heic:depth-image=true -define jpeg:arithmetic-coding=on -define jpeg:optimize-coding=on -define webp:alpha-compression=1 -define webp:auto-filter=true -define webp:thread-level=1"
compute_image_size_for_format() {
local FORMAT="$1"
local FILE="$2"
local reset_e=0
internal_is_shell_attribute_set "e" && set +e && reset_e=1
local optimised_size=$(magick -define stream:buffer-size=0 $IMAGICK_STD_DEFINES "$FILE" "$FORMAT":- | wc -c | sed -e 's/^[[:space:]]*//')
[ "$reset_e" -eq 1 ] && set -e
echo "$optimised_size"
}
convert_image_to_format() {
local FORMAT="$1"
local SOURCE_FILE="$2"
local TARGET_FILE="$3"
local reset_e=0
internal_is_shell_attribute_set "e" && set +e && reset_e=1
magick -define preserve-timestamp=true $IMAGICK_STD_DEFINES "$SOURCE_FILE" "${FORMAT}:${TARGET_FILE}"
touch -r "$SOURCE_FILE" "$TARGET_FILE"
[ "$reset_e" -eq 1 ] && set -e
}
##############################################################################
main() {
local THRESHOLD_PERCENT="${THRESHOLD_PERCENT:-10}"
local OUTPUT_DIR="${OUTPUT_DIR:-}"
# Options
while getopts "t:o:qvh" option; do
case "$option" in
t) THRESHOLD_PERCENT="$OPTARG" ;;
o) OUTPUT_DIR="$OPTARG" ;;
q) LOG_LEVEL=$LOG_LEVEL_ERROR ;;
v) LOG_LEVEL=$LOG_LEVEL_DEBUG ;;
h) print_usage; exit 1 ;;
*) print_usage; exit 1 ;;
esac
done
shift $((OPTIND - 1)) # Shift off the options and optional --
if [ -n "$THRESHOLD_PERCENT" ]; then
case "$THRESHOLD_PERCENT" in
*[![:digit:]]*)
print_usage
exit 1
;;
esac
if [ "$THRESHOLD_PERCENT" -lt 0 ] || [ "$THRESHOLD_PERCENT" -gt 100 ]; then
print_usage
exit 1
fi
fi
# $# should be at least 1 (the file to optimize), however it may be strictly
# greater than 1 if multiple files are specified.
if [ $# -eq 0 ]; then
print_usage
exit 1
fi
if [ -n "$OUTPUT_DIR" ]; then
mkdir -p "${OUTPUT_DIR}"
fi
log $LOG_LEVEL_INFO "file_name size\c"
for format in $CONST_OUTPUT_FORMATS; do
log $LOG_LEVEL_INFO " ${format}\c"
done
log $LOG_LEVEL_INFO " best_image_format"
for file in "$@"; do
[ -s "$file" ] || continue
local original_size=$(wc -c < "$file" | sed -e 's/^[[:space:]]*//')
local best_size=$original_size
local best_format=
log $LOG_LEVEL_INFO "${file} ${original_size}\c"
for format in $CONST_OUTPUT_FORMATS; do
local optimised_size=$(compute_image_size_for_format "${format}" "${file}")
log $LOG_LEVEL_INFO " ${optimised_size}\c"
if [ "${optimised_size}" -lt "${best_size}" ]; then
best_size=$optimised_size
if [ "$(expr \( "${original_size}" - "${optimised_size}" \) '*' 100)" -gt "$(expr "${original_size}" '*' "${THRESHOLD_PERCENT}")" ]; then
best_format=$format
fi
fi
done
log $LOG_LEVEL_INFO " ${best_format}"
if [ -n "$best_format" ] && [ -n "$OUTPUT_DIR" ]; then
local target_file="${OUTPUT_DIR}/$(basename "${file%.*}").$(echo ${best_format} | tr '[:upper:]' '[:lower:]')"
convert_image_to_format "${best_format}" "${file}" "${target_file}"
fi
done
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment