Last active
July 17, 2026 13:58
-
-
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 generate new files, and fills a log file 'converted_[source_format]_to_[target_format].txt' | |
| to remember we did this operation. | |
| DEPENDENCIES: | |
| - GraphicsMagick (Linux, gm binary must be in PATH) | |
| - ImageMagick (Windows, magick binary must be in PATH) | |
| Optional dependencies: | |
| - Krita (experimental, when using --krita) | |
| 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 (webp, avif, etc.) | |
| SOURCE_FORMAT == TARGET_FORMAT is supported. It will add '_processed' suffix | |
| to avoid overwriting existing file, unless using option -o, --allow-overwrite. | |
| KNOWN ISSUES | |
| - converting jpg to avif make cause color profile loss | |
| (see "Output .avif image is washed out" at https://github.com/ImageMagick/ImageMagick/issues/7308) | |
| OPTIONS | |
| -h, --help Show this help message | |
| -q, --quality QUALITY Quality of target file (default: 90) | |
| CAUTION: ignored when using --krita | |
| 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% | |
| -o, --allow-overwrite When SOURCE_FORMAT == TARGET_FORMAT, overwrite source file with output file instead of adding '_processed' suffix | |
| -t, --trash Trash source files after conversion (can be restored from Trash) | |
| -n, --dry-run Enable dry run | |
| -k, --krita Experimental: convert via Krita command-line (only flatpak install is supported) | |
| " | |
| } | |
| # Default arguments | |
| quality=90 | |
| output_size="100%" | |
| allow_overwrite=false | |
| trash=false | |
| dry_run=false | |
| krita=false | |
| # 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 | |
| ;; | |
| -o | --allow-overwrite ) | |
| allow_overwrite=true | |
| shift # past option | |
| ;; | |
| -t | --trash ) | |
| trash=true | |
| shift # past option | |
| ;; | |
| -n | --dry-run ) | |
| dry_run=true | |
| shift # past option | |
| ;; | |
| -k | --krita ) | |
| krita=true | |
| shift # past option | |
| ;; | |
| -* ) # 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]}" | |
| suffix="" | |
| if [[ "$target_format" == "$source_format" && "$allow_overwrite" == false ]]; then | |
| suffix="_processed" | |
| 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%.*}${suffix}.${target_format}" | |
| conversion_info="$f -> $target_f (quality: $quality, output_size: $output_size)" | |
| success=true | |
| if [[ "$krita" == "true" ]]; then | |
| # !! EXPERIMENTAL !! | |
| # 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" -auto-orient -quality 100 "tmp/resized_$sanitized_f" | |
| if hash flatpak 2> /dev/null; then | |
| # 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 | |
| echo "ERROR: no flatpak, so cannot run Krita via flatpak" | |
| success=false | |
| fi | |
| else | |
| if hash gm 2> /dev/null && [[ "$OSTYPE" != "msys" ]]; then | |
| # 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 | |
| # -auto-orient will apply JPG EXIF rotation info (for photos) and avoid ending up with bad rotations in other output formats | |
| gm mogrify -format $target_format -resize "$output_size" -auto-orient -quality $quality "tmp/$sanitized_f" | |
| if [[ -n "$suffix" ]]; then | |
| # gm automatically creates target file with original file basename, so we need to manually rename to | |
| # final target file name with suffix | |
| intermediate_target_f="${sanitized_f%.*}.${target_format}" | |
| mv "tmp/$intermediate_target_f" "tmp/$target_f" | |
| fi | |
| 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" -auto-orient -quality $quality "tmp/$target_f" | |
| else | |
| echo "ERROR: no gm nor magick 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