image utility

Flag: 📍 Modified: February 14, 2026 7:58 PM Category: Coding Notes List: Work Created: November 20, 2025 6:10 PM Master Type: Notes Hide: No Starred: No Status: Unassigned

#!/bin/bash
# ImageTool.command — Simple batch image utility with folder drag-drop
# Includes: convert, resize, rotate, compress %, compress target size,
# zip (standard/target/split), thumbnails, batch rename.

shopt -s nullglob

SUPPORTED_EXT="psd PSD tif TIF tiff TIFF png PNG jpg JPG jpeg JPEG gif GIF bmp BMP"

##############################################
# FOLDER SELECTION (drag-and-drop aware)
##############################################

if [ -n "$1" ]; then
    WORKDIR="$1"
else
    WORKDIR=$(osascript <<EOF
set f to choose folder with prompt "Select a folder of images:"
POSIX path of f
EOF
)
fi

[ -d "$WORKDIR" ] || { echo "Folder not found."; exit 1; }

##############################################
# FILE SCAN
##############################################

scan_files() {
    FILES=()
    for ext in $SUPPORTED_EXT; do
        for f in "$WORKDIR"/*.$ext; do FILES+=("$f"); done
    done
}

scan_files
[ ${#FILES[@]} -gt 0 ] || { echo "No supported images found."; exit 1; }

##############################################
# MENU
##############################################

menu() {
    clear
    echo "==========================================="
    echo "                IMAGE TOOL"
    echo "Folder: $WORKDIR"
    echo "Files: ${#FILES[@]}"
    echo "==========================================="
    echo "1. Convert"
    echo "2. Resize"
    echo "3. Rotate"
    echo "4. Compress"
    echo "5. Zip"
    echo "6. Thumbnails"
    echo "7. Batch Rename"
    echo "8. Quit"
    echo "-------------------------------------------"
    printf "Choose option: "
}

##############################################
# UTILITIES
##############################################

parse_size() {
    local s="$1"
    case "$s" in
        *m|*M) echo $(( ${s%?} * 1024 * 1024 ));;
        *k|*K) echo $(( ${s%?} * 1024 ));;
        *) echo "$s";;
    esac
}

png_lossy() {
    local f="$1"
    local out="$2"
    local q="$3"
    local colors=$(( 256 - (q * 2) ))
    if [ $colors -lt 16 ]; then colors=16; fi
    magick "$f" -colors "$colors" -define png:compression-level=9 "$out"
}

##############################################
# IMAGE OPERATIONS
##############################################

convert_menu() {
    printf "Convert to format (psd/tif/png/jpg/gif): "
    read -r OUTEXT
    for f in "${FILES[@]}"; do
        base="${f%.*}"
        magick "$f" "$base.$OUTEXT"
    done
    scan_files
}

resize_menu() {
    echo "1 = by width"
    echo "2 = by height"
    read -r mode

    if [ "$mode" = "1" ]; then
        printf "Width px: "
        read -r W
        geom="${W}x"
    else
        printf "Height px: "
        read -r H
        geom="x$H"
    fi

    for f in "${FILES[@]}"; do
        base="${f%.*}"
        magick "$f" -resize "$geom" "$base"_resized."${f##*.}"
    done
}

rotate_menu() {
    echo "1 = Landscape"
    echo "2 = Portrait"
    echo "3 = 90° CW"
    echo "4 = 90° CCW"
    echo "5 = 180°"
    read -r opt

    for f in "${FILES[@]}"; do
        out="${f%.*}_rot.${f##*.}"
        case "$opt" in
            1)
                w=$(magick identify -format "%w" "$f")
                h=$(magick identify -format "%h" "$f")
                if [ "$h" -gt "$w" ]; then magick "$f" -rotate 90 "$out"; else cp "$f" "$out"; fi
                ;;
            2)
                w=$(magick identify -format "%w" "$f")
                h=$(magick identify -format "%h" "$f")
                if [ "$w" -gt "$h" ]; then magick "$f" -rotate 90 "$out"; else cp "$f" "$out"; fi
                ;;
            3) magick "$f" -rotate 90 "$out" ;;
            4) magick "$f" -rotate -90 "$out" ;;
            5) magick "$f" -rotate 180 "$out" ;;
        esac
    done
}

##############################################
# COMPRESSION (% and TARGET-SIZE)
##############################################

compress_percent() {
    local Q="$1"
    for f in "${FILES[@]}"; do
        base="${f%.*}"
        out="$base"_compressed."${f##*.}"

        case "${f##*.}" in
            png|PNG) png_lossy "$f" "$out" "$Q" ;;
            *) magick "$f" -quality "$Q" "$out" ;;
        esac
    done
}

compress_target() {
    printf "Target size (ex: 1m, 900k, 1500k): "
    read -r TARGET_RAW
    TARGET=$(parse_size "$TARGET_RAW")
    TOL_PCT=5

    for f in "${FILES[@]}"; do
        ext="${f##*.}"
        base="${f%.*}"
        out="$base"_target."$ext"

        low_q=5
        high_q=95
        best_q=$high_q

        while [ $low_q -le $high_q ]; do
            mid_q=$(( (low_q + high_q) / 2 ))
            tmp="$base"_tmp."$ext"

            if [[ "$ext" == "png" || "$ext" == "PNG" ]]; then
                png_lossy "$f" "$tmp" "$mid_q"
            else
                magick "$f" -quality "$mid_q" "$tmp"
            fi

            size=$(stat -f%z "$tmp")
            low_limit=$(( TARGET - (TARGET * TOL_PCT / 100) ))
            high_limit=$(( TARGET + (TARGET * TOL_PCT / 100) ))

            if [ $size -ge $low_limit ] && [ $size -le $high_limit ]; then
                mv "$tmp" "$out"
                break
            fi

            if [ $size -gt $TARGET ]; then
                low_q=$(( mid_q + 1 ))
            else
                best_q=$mid_q
                high_q=$(( mid_q - 1 ))
            fi

            rm -f "$tmp"
        done

        if [ ! -f "$out" ]; then
            if [[ "$ext" == "png" || "$ext" == "PNG" ]]; then
                png_lossy "$f" "$out" "$best_q"
            else
                magick "$f" -quality "$best_q" "$out"
            fi
        fi
    done
}

compress_menu() {
    echo "1 = Quality %"
    echo "2 = Target File Size"
    read -r mode

    if [ "$mode" = "1" ]; then
        printf "Quality %: "
        read -r Q
        compress_percent "$Q"
    else
        compress_target
    fi
}

##############################################
# ZIP ROUTINES (cleaned)
##############################################

zip_standard() {
    printf "Zip filename: "
    read -r ZNAME
    zip -9 -r "$ZNAME.zip" "$WORKDIR"
}

zip_target() {
    printf "Target zip size (e.g. 50m, 200m): "
    read -r TARGET_RAW
    TARGET=$(parse_size "$TARGET_RAW")

    tmpdir=$(mktemp -d)
    basezip="$tmpdir/base.zip"

    zip -9 -r "$basezip" "$WORKDIR" >/dev/null
    original=$(stat -f%z "$basezip")

    BEST_ZIP="./zip_target.zip"
    BEST_DIFF=$(( original - TARGET ))
    cp "$basezip" "$BEST_ZIP"

    methods=("deflate" "bzip2" "lzma")

    for m in "${methods[@]}"; do
        tmp="$tmpdir/m_$m.zip"
        zip -Z "$m" -9 -r "$tmp" "$WORKDIR" 2>/dev/null || continue

        zsize=$(stat -f%z "$tmp")
        diff=$(( zsize > TARGET ? zsize - TARGET : TARGET - zsize ))

        if [ $diff -lt $BEST_DIFF ]; then
            BEST_DIFF=$diff
            cp "$tmp" "$BEST_ZIP"
        fi
    done

    final=$(stat -f%z "$BEST_ZIP")
    if [ $final -gt $(( TARGET * 2 )) ]; then
        echo "⚠️  Could not approach target. Consider Zip + Split."
    fi

    rm -rf "$tmpdir"
}

zip_split() {
    printf "Zip filename: "
    read -r ZNAME
    printf "Split size (e.g. 200m): "
    read -r SIZE
    zip -9 -r "$ZNAME.zip" "$WORKDIR"
    split -b "$SIZE" "$ZNAME.zip" "$ZNAME.zip.part_"
}

zip_menu() {
    echo "1 = Standard zip"
    echo "2 = Zip to target size"
    echo "3 = Zip + Split"
    read -r mode

    case "$mode" in
        1) zip_standard ;;
        2) zip_target ;;
        3) zip_split ;;
    esac
}

##############################################
# THUMBNAILS
##############################################

thumb_menu() {
    printf "Thumbnail size px: "
    read -r T
    printf "Thumbnail quality: "
    read -r Q

    mkdir -p "$WORKDIR/thumbnails"

    for f in "${FILES[@]}"; do
        base=$(basename "$f")
        magick "$f" -resize "${T}x${T}" -quality "$Q" \
            "$WORKDIR/thumbnails/${base%.*}_thumb.jpg"
    done
}

##############################################
# BATCH RENAME
##############################################

batch_rename() {
    printf "Base name prefix: "
    read -r BASE
    printf "Start number: "
    read -r START

    num=$START
    for f in "${FILES[@]}"; do
        ext="${f##*.}"
        mv "$f" "$WORKDIR/${BASE}${num}.$ext"
        num=$(( num + 1 ))
    done
    scan_files
}

##############################################
# MAIN LOOP
##############################################

while true; do
    menu
    read -r choice

    case "$choice" in
        1) convert_menu ;;
        2) resize_menu ;;
        3) rotate_menu ;;
        4) compress_menu ;;
        5) zip_menu ;;
        6) thumb_menu ;;
        7) batch_rename ;;
        8) exit 0 ;;
        *) echo "Invalid." ;;
    esac

    printf "\nPress Enter to continue..."
    read -r _
done