Skip to content

Instantly share code, notes, and snippets.

@hsandt
Last active June 10, 2025 17:39
Show Gist options
  • Save hsandt/d922a14e1f8b10faa1dee2a05894729a to your computer and use it in GitHub Desktop.
Save hsandt/d922a14e1f8b10faa1dee2a05894729a to your computer and use it in GitHub Desktop.
Convert all image files in current folder to target format
#!/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