Last active
June 10, 2025 17:39
-
-
Save hsandt/d922a14e1f8b10faa1dee2a05894729a to your computer and use it in GitHub Desktop.
Convert all image files in current folder to target format
This file contains hidden or 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/bash | |
# Gist: https://gist.github.com/hsandt/d922a14e1f8b10faa1dee2a05894729a | |
help() { | |
echo "Convert all image files in current folder to target format | |
! This doesn't add a suffix to file paths, so if SOURCE_FORMAT == TARGET_FORMAT, | |
! it will overwrite files in-place. Make sure to backup if needed! | |
This generates a file 'converted_[source_format]_to_[target_format].txt' to remember | |
the operation. | |
Note: this may create and delete a local tmp folder. | |
" | |
usage | |
} | |
usage() { | |
echo "Usage: convert_image.sh SOURCE_FORMAT TARGET_FORMAT [OPTIONS] | |
ARGUMENTS | |
SOURCE_FORMAT Format of files to convert (jpg, etc.) (case-sensitive) | |
TARGET_FORMAT Format of files to convert (avif, etc.) | |
SOURCE_FORMAT == TARGET_FORMAT is supported, but will overwrite existing files. | |
OPTIONS | |
-q, --quality QUALITY Quality of target file (default: 90) | |
CAUTION: ignored when using Krita conversion (for avif) | |
For PNG, the quality value sets the zlib compression level (quality / 10) and filter-type (quality % 10) | |
See https://imagemagick.org/script/command-line-options.php#quality | |
-s, --output-size OUTPUT_SIZE Final image size. Argument format is same as convert / gm mogrify -resize (X%, WxH, etc.). | |
Default: 100% | |
-h, --help Show this help message | |
" | |
} | |
# Default arguments | |
quality=90 | |
output_size="100%" | |
# Read arguments | |
positional_args=() | |
while [[ $# -gt 0 ]]; do | |
case $1 in | |
-h | --help ) | |
help | |
exit 0 | |
;; | |
-q | --quality ) | |
if [[ $# -lt 2 ]] ; then | |
echo "Missing argument for $1" | |
usage | |
exit 1 | |
fi | |
quality="$2" | |
shift # past argument | |
shift # past value | |
;; | |
-s | --output-size ) | |
if [[ $# -lt 2 ]] ; then | |
echo "Missing argument for $1" | |
usage | |
exit 1 | |
fi | |
output_size="$2" | |
shift # past argument | |
shift # past value | |
;; | |
-* ) # unknown option | |
echo "Unknown option: '$1'" | |
usage | |
exit 1 | |
;; | |
* ) # store positional argument for later | |
positional_args+=("$1") | |
shift # past argument | |
;; | |
esac | |
done | |
if ! [[ ${#positional_args[@]} -eq 2 ]]; then | |
echo "Wrong number of positional arguments: found ${#positional_args[@]}, expected 2." | |
echo "Passed positional arguments: ${positional_args[@]}" | |
usage | |
exit 1 | |
fi | |
source_format="${positional_args[0]}" | |
target_format="${positional_args[1]}" | |
if [[ "$target_format" == "$source_format" ]]; then | |
echo "WARNING: TARGET_FORMAT is same as SOURCE_FORMAT: '$target_format'. | |
Conversion may still be relevant e.g. to reduce size by adjusting quality, | |
but note that target files will be overwritten." | |
fi | |
# Remember operation, esp. if we converted from a lossy format like .jpg, | |
# to note that user should not expect very high quality even if it's .avif at high quality | |
# (jpg->avif is in fact the most common operation and is meant to spare file size | |
# since the file has undergone lossy compression anyway) | |
# We will fill this with list of converted files | |
conversion_log_filename="converted_${source_format}_to_${target_format}.txt" | |
# Source: https://stackoverflow.com/questions/24577551/in-graphicsmagick-how-can-i-specify-the-output-file-on-a-bulk-of-files | |
# Removed +profile option since we are not converting to thumbnail so we don't remove color profile info | |
echo "= START conversion =" >> "$conversion_log_filename" | |
for f in *.${source_format}; do | |
# if no file ending with .${source_format}, | |
# it will iterate once with f="*.${source_format}" | |
# in which case we must do nothing | |
if ! [[ -f "$f" ]]; then | |
echo "No files found with extension '$f' (note that it is case-sensitive)." | |
# It is not an error, though, so exit with 0 | |
exit 0 | |
fi | |
mkdir -p "tmp" | |
# Create a copy with sanitized name because convert and gm mogrify use colon `:` to indicate format, | |
# so replace each colon with hyphen `-` | |
# Note that it is safe to rename files inside the loop as the list of files has already been evaluated | |
# Edge case: a target file with sanitized name + new format suffix may already exist, but in this case, | |
# it's most likely born from a previous conversation of the original file and can safely be overwritten | |
sanitized_f=`echo $f | tr : -` | |
cp "$f" "tmp/$sanitized_f" | |
# Keep sanitized file name for target (not required since we could rename output file after conversion, but easier) | |
target_f="${sanitized_f%.*}.${target_format}" | |
conversion_info="$f -> $target_f (quality: $quality, output_size: $output_size)" | |
success=true | |
if [[ "$target_format" == "avif" ]]; then | |
# WIP !! | |
# First rescale picture since Krita command-line doesn't support | |
# Export Advanced to target size | |
# To avoid loss before actual conversion, keep same format and maximum quality for now | |
convert "tmp/$sanitized_f" -resize "$output_size" -quality 100 "tmp/resized_$sanitized_f" | |
# `convert` supports avif, but pretty slow | |
# convert "$f" -quality $quality "$target_f" | |
# EXPERIMENTAL: use Krita, a bit faster (3s vs 5s) than convert, for avif | |
# But cannot choose quality, which is around 90 apparently | |
# if Krita is not open, will open a document dimensions prompt window | |
# NOTE: Krita preserves EXIF on export, so no need to use exiftool | |
flatpak run org.kde.krita "tmp/resized_$sanitized_f" --export --export-filename "$target_f" | |
else | |
if hash gm 2> /dev/null && [[ "$OSTYPE" != "msys" ]]; then | |
# For common formats, unless on Windows, use `gm mogrify` (doesn't support avif) | |
# It is faster than `convert` | |
# In case "$target_format" == "$source_format", create new file in tmp dir first | |
# so we can copy EXIT metadata from the original being it's overwritten | |
gm mogrify -format $target_format -resize "$output_size" -quality $quality "tmp/$sanitized_f" | |
elif hash magick 2> /dev/null; then | |
# Some issues with gm on Windows ("No decode delegate for this image format (file.png)") so using magick | |
# (don't use just `convert`, deprecated and conflicting with Windows built-in) | |
magick "tmp/$sanitized_f" -resize "$output_size" -quality $quality "tmp/$target_f" | |
elif hash exiftool 2> /dev/null; then | |
# EXPERIMENTAL: old exiftool doesn't support webp and silently fails with PNG, but latest should work | |
# with both: https://exiftool.org/forum/index.php?topic=13797.msg75207#msg75207 | |
# exiftool -overwrite_original_in_place -tagsFromFile "$f" "tmp/$target_f" | |
echo "WARNING: exiftool is still experimental, currently disabled" | |
success=false | |
else | |
echo "ERROR: no gm, magick, exiftool executable found" | |
success=false | |
fi | |
if [[ "$success" == true ]]; then | |
# In case source_format == target_format, we must save the original file size before it gets overwritten in-place, | |
# to print stats later | |
# Ex: 3214547 | |
f_size=`stat -c %s "$f"` | |
# Ex: 3.2M | |
f_size_readable=`du -h "$f" | awk '{ print $1 }'` | |
# If "$target_format" == "$source_format", the move will overwrite the original file | |
# Else, we still want to overwrite any existing file with target format, so in both cases we want -f | |
mv -f "tmp/$target_f" "$target_f" | |
fi | |
fi | |
# Cleanup tmp dir, which should exist at this point, so no need for -f | |
rm -r "tmp" | |
if [[ "$success" == false ]]; then | |
# Print error to terminal and also log | |
echo "$conversion_info" | tee -a "$conversion_log_filename" | |
echo " Failed, STOP." | tee -a "$conversion_log_filename" | |
exit 1 | |
fi | |
# Log file conversion and info | |
echo "$conversion_info" >> "$conversion_log_filename" | |
# Print size change and relative percentage (using original file size saved earlier) | |
target_f_size=`stat -c %s "$target_f"` | |
target_f_size_readable=`du -h "$target_f" | awk '{ print $1 }'` | |
conversion_percentage=$((100*$target_f_size/$f_size)) | |
size_change_info="$f_size_readable -> $target_f_size_readable (${conversion_percentage}%)" | |
echo " $size_change_info" >> "$conversion_log_filename" | |
if [[ $conversion_percentage -ge 100 ]]; then | |
echo "OOPS, $f -> $target_f gave bigger size! $size_change_info" | |
echo " OOPS, bigger size!" >> "$conversion_log_filename" | |
elif [[ $conversion_percentage -ge 75 ]]; then | |
echo "MEH, $f -> $target_f did not reduce size below 75%! $size_change_info" | |
echo " MEH, new size is not below 75%!" >> "$conversion_log_filename" | |
fi | |
done | |
echo "Finished conversion." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment