Created
July 27, 2024 21:10
-
-
Save javabean/6360b087d6d5cf3608ad685723e02f87 to your computer and use it in GitHub Desktop.
image-best-format.sh
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 | |
# 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