Skip to content

Instantly share code, notes, and snippets.

@hsandt
Last active June 10, 2025 17:39
Show Gist options
  • Select an option

  • Save hsandt/d922a14e1f8b10faa1dee2a05894729a to your computer and use it in GitHub Desktop.

Select an option

Save hsandt/d922a14e1f8b10faa1dee2a05894729a to your computer and use it in GitHub Desktop.

Revisions

  1. hsandt revised this gist Jun 10, 2025. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion convert_image.sh
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    #!/bin/bash

    # Source: https://stackoverflow.com/questions/5784661/how-do-you-convert-an-entire-directory-with-ffmpeg
    # Gist: https://gist.github.com/hsandt/d922a14e1f8b10faa1dee2a05894729a

    help() {
    echo "Convert all image files in current folder to target format
  2. hsandt created this gist Jun 10, 2025.
    221 changes: 221 additions & 0 deletions convert_image.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,221 @@
    #!/bin/bash

    # Source: https://stackoverflow.com/questions/5784661/how-do-you-convert-an-entire-directory-with-ffmpeg

    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."