Skip to content

Instantly share code, notes, and snippets.

@mumblequatch
Created May 19, 2026 14:52
Show Gist options
  • Select an option

  • Save mumblequatch/aa46494a47dead91f5d13fee2afd0283 to your computer and use it in GitHub Desktop.

Select an option

Save mumblequatch/aa46494a47dead91f5d13fee2afd0283 to your computer and use it in GitHub Desktop.
image_optimize_km.sh — Keyboard Maestro batch image optimizer (magick + pngquant) with a native one-shot NSAlert dialog
#!/bin/bash
# Image optimizer — Keyboard Maestro driver
#
# Thin wrapper around ImageMagick (+ pngquant for PNG output). Wire to a KM
# hotkey via a single "Execute Shell Script" action:
# ~/Scripts/image_optimize_km.sh
# (Set the action to "Ignore Results".)
#
# Flow:
# 1. Reads Finder selection (filtered to image files). Terminal args win if
# invoked directly with file paths.
# 2. Prompts for: max longest-edge, output format, quality, destination.
# 3. Shows confirm dialog with source total + chosen settings.
# 4. Opens a Terminal window with per-file progress and size deltas.
# 5. Final notification + reveals last output folder.
#
# Supports: jpg/jpeg, png, gif, bmp, tiff/tif, heic/heif, avif, webp, psd,
# tga, ico — anything ImageMagick reads.
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
# --- Sanity check ---------------------------------------------------
if ! command -v magick >/dev/null 2>&1; then
osascript -e 'display notification "ImageMagick not found. brew install imagemagick" with title "Image Optimizer" sound name "Basso"'
exit 1
fi
# --- Gather input files ---------------------------------------------
IMG_EXT_RE='\.(jpe?g|png|gif|bmp|tiff?|heic|heif|avif|webp|psd|tga|ico)$'
INPUT_FILES=()
if [ $# -gt 0 ]; then
for f in "$@"; do INPUT_FILES+=( "$f" ); done
else
SELECTION=$(osascript <<'AS'
tell application "Finder"
set sel to selection as alias list
set out to ""
repeat with i in sel
set out to out & POSIX path of i & linefeed
end repeat
return out
end tell
AS
)
while IFS= read -r line; do
[ -z "$line" ] && continue
if echo "$line" | grep -qiE "$IMG_EXT_RE"; then
INPUT_FILES+=( "$line" )
fi
done <<< "$SELECTION"
fi
N_FILES=${#INPUT_FILES[@]}
if [ "$N_FILES" -eq 0 ]; then
osascript -e 'display notification "Select image file(s) in Finder first." with title "Image Optimizer" sound name "Basso"'
exit 1
fi
# --- Tally source size ----------------------------------------------
TOTAL_BYTES=0
for f in "${INPUT_FILES[@]}"; do
[ -f "$f" ] || continue
SZ=$(stat -f %z "$f" 2>/dev/null || echo 0)
TOTAL_BYTES=$((TOTAL_BYTES + SZ))
done
TOTAL_MB=$(awk -v b="$TOTAL_BYTES" 'BEGIN{printf "%.1f", b/1048576}')
# --- Single combined dialog (NSAlert + accessoryView via JXA) -------
# Native macOS Cocoa dialog with all four controls inline.
DIALOG_OUT=$(
N_FILES_VAR="$N_FILES" TOTAL_MB_VAR="$TOTAL_MB" \
osascript -l JavaScript <<'JXA'
ObjC.import('Cocoa')
function run() {
const env = $.NSProcessInfo.processInfo.environment
const nFiles = env.objectForKey('N_FILES_VAR').js
const totalMb = env.objectForKey('TOTAL_MB_VAR').js
const W = 380, H = 175
const view = $.NSView.alloc.initWithFrame($.NSMakeRect(0, 0, W, H))
function addLabel(text, x, y, w) {
const lbl = $.NSTextField.alloc.initWithFrame($.NSMakeRect(x, y, w, 20))
lbl.stringValue = text
lbl.editable = false
lbl.bordered = false
lbl.drawsBackground = false
lbl.selectable = false
view.addSubview(lbl)
return lbl
}
function addTextField(value, x, y, w) {
const tf = $.NSTextField.alloc.initWithFrame($.NSMakeRect(x, y, w, 22))
tf.stringValue = value
view.addSubview(tf)
return tf
}
function addPopup(items, x, y, w, defaultIdx) {
const pop = $.NSPopUpButton.alloc.initWithFramePullsDown($.NSMakeRect(x, y, w, 26), false)
items.forEach(it => pop.addItemWithTitle(it))
if (defaultIdx >= 0 && defaultIdx < items.length) pop.selectItemAtIndex(defaultIdx)
view.addSubview(pop)
return pop
}
const LBL_W = 105, FLD_X = 115, FLD_W = 255, ROW_H = 34
let y = H - 32
addLabel("Max dimension:", 0, y, LBL_W)
const sizePop = addPopup(
["Keep original size", "2400", "1600", "1200", "1000", "800", "Custom…"],
FLD_X, y - 4, FLD_W, 0)
y -= ROW_H
addLabel("Format:", 0, y, LBL_W)
const formatPop = addPopup(
["Keep original", "WebP", "JPEG", "PNG"],
FLD_X, y - 4, FLD_W, 2)
y -= ROW_H
addLabel("Quality (1-100):", 0, y, LBL_W)
const qualityTF = addTextField("85", FLD_X, y, FLD_W)
y -= ROW_H
addLabel("Destination:", 0, y, LBL_W)
const destPop = addPopup(
["Save alongside original (add _opt suffix)",
"Save into 'optimized' subfolder",
"Replace original (keep .bak backup)"],
FLD_X, y - 4, FLD_W, 0)
const alert = $.NSAlert.alloc.init
alert.messageText = "Image Optimizer"
alert.informativeText = nFiles + " file(s) · " + totalMb + " MB total"
alert.accessoryView = view
alert.addButtonWithTitle("Run")
alert.addButtonWithTitle("Cancel")
// Bring the dialog to the front without pinning it on top.
// Accessory policy gives osascript a key window without a Dock icon;
// activateIgnoringOtherApps puts it in front initially. The alert window
// is a normal-level window, so other apps can still come forward by click.
const app = $.NSApplication.sharedApplication
app.setActivationPolicy($.NSApplicationActivationPolicyAccessory)
app.activateIgnoringOtherApps(true)
const r = alert.runModal
if (r != 1000) return "CANCEL"
return [
ObjC.unwrap(sizePop.titleOfSelectedItem),
ObjC.unwrap(formatPop.titleOfSelectedItem),
ObjC.unwrap(qualityTF.stringValue),
ObjC.unwrap(destPop.titleOfSelectedItem),
].join("\t")
}
JXA
)
[ "$DIALOG_OUT" = "CANCEL" ] && exit 0
# Parse the tab-delimited result
IFS=$'\t' read -r MAX_DIM FORMAT QUALITY DEST <<< "$DIALOG_OUT"
# Handle "Custom…" → follow-up dialog asking for the long-edge px value
if [ "$MAX_DIM" = "Custom…" ]; then
MAX_DIM=$(osascript <<'EOF'
set theResp to display dialog "Longest edge (px):" default answer "1200" with title "Image Optimizer" buttons {"Cancel","OK"} default button "OK"
if button returned of theResp is "Cancel" then return "CANCEL"
return text returned of theResp
EOF
)
[ "$MAX_DIM" = "CANCEL" ] && exit 0
fi
# Normalize size: "Keep original size" → 0 (no resize); otherwise extract digits
case "$MAX_DIM" in
Keep*|keep*|"") MAX_DIM="0" ;;
esac
MAX_DIM=$(echo "$MAX_DIM" | tr -cd '0-9')
[ -z "$MAX_DIM" ] && MAX_DIM="0"
# Normalize quality
QUALITY=$(echo "$QUALITY" | tr -cd '0-9')
[ -z "$QUALITY" ] && QUALITY=82
[ "$QUALITY" -lt 1 ] 2>/dev/null && QUALITY=1
[ "$QUALITY" -gt 100 ] 2>/dev/null && QUALITY=100
# FORMAT and DEST pass through to the worker case statements;
# any value not matching the exact arms falls into the documented defaults
# ("Keep original" → keep, "Replace original ..." → replace + .bak).
# --- Build temp script with params baked in, run in Terminal -------
TMPSCRIPT=$(mktemp /tmp/imgopt_run.XXXXXX).sh
cat > "$TMPSCRIPT" <<HEADER
#!/bin/bash
export PATH="/opt/homebrew/bin:/usr/local/bin:\$PATH"
HEADER
printf 'MAX_DIM=%q\n' "$MAX_DIM" >> "$TMPSCRIPT"
printf 'FORMAT=%q\n' "$FORMAT" >> "$TMPSCRIPT"
printf 'QUALITY=%q\n' "$QUALITY" >> "$TMPSCRIPT"
printf 'DEST=%q\n' "$DEST" >> "$TMPSCRIPT"
echo 'INPUT_FILES=()' >> "$TMPSCRIPT"
for f in "${INPUT_FILES[@]}"; do
printf 'INPUT_FILES+=( %q )\n' "$f" >> "$TMPSCRIPT"
done
cat >> "$TMPSCRIPT" <<'BODY'
human_size() {
awk -v b="$1" 'BEGIN{u="B";if(b>=1024){b/=1024;u="KB"};if(b>=1024){b/=1024;u="MB"};printf "%.1f %s",b,u}'
}
N=${#INPUT_FILES[@]}
echo ""
echo "════════════════════════════════════════════"
echo " IMAGE OPTIMIZER — $N file(s)"
echo " resize=${MAX_DIM} format=$FORMAT quality=$QUALITY"
echo "════════════════════════════════════════════"
echo ""
FAIL=0
SUM_IN=0
SUM_OUT=0
LAST_OUT_DIR=""
for i in "${!INPUT_FILES[@]}"; do
SRC="${INPUT_FILES[$i]}"
SRC_BASE="$(basename "$SRC")"
SRC_DIR="$(cd "$(dirname "$SRC")" && pwd)"
SRC_STEM="${SRC_BASE%.*}"
SRC_EXT="${SRC_BASE##*.}"
SRC_EXT_LC=$(echo "$SRC_EXT" | tr '[:upper:]' '[:lower:]')
case "$FORMAT" in
WebP) OUT_EXT="webp" ;;
JPEG) OUT_EXT="jpg" ;;
PNG) OUT_EXT="png" ;;
*)
# "Keep original format" — normalize a couple of weird extensions
case "$SRC_EXT_LC" in
jpeg) OUT_EXT="jpg" ;;
tiff) OUT_EXT="tif" ;;
heif) OUT_EXT="heic" ;;
*) OUT_EXT="$SRC_EXT_LC" ;;
esac ;;
esac
case "$DEST" in
"Save alongside original (add _opt suffix)")
OUT_DIR="$SRC_DIR"
OUT_PATH="$OUT_DIR/${SRC_STEM}_opt.$OUT_EXT" ;;
"Save into 'optimized' subfolder")
OUT_DIR="$SRC_DIR/optimized"
mkdir -p "$OUT_DIR"
OUT_PATH="$OUT_DIR/${SRC_STEM}.$OUT_EXT" ;;
*)
OUT_DIR="$SRC_DIR"
cp -p "$SRC" "$SRC.bak"
OUT_PATH="$OUT_DIR/${SRC_STEM}.$OUT_EXT" ;;
esac
LAST_OUT_DIR="$OUT_DIR"
IN_SZ=$(stat -f %z "$SRC" 2>/dev/null || echo 0)
# Build magick args (resize only downward with `>`)
ARGS=( "$SRC" )
[ "$MAX_DIM" != "0" ] && ARGS+=( -resize "${MAX_DIM}x${MAX_DIM}>" )
ARGS+=( -strip -quality "$QUALITY" )
printf "[%d/%d] %s\n" "$((i+1))" "$N" "$SRC_BASE"
OK=0
if [ "$OUT_EXT" = "png" ] && command -v pngquant >/dev/null 2>&1; then
QMAX="$QUALITY"
QMIN=$((QMAX - 20))
[ "$QMIN" -lt 40 ] && QMIN=40
if magick "${ARGS[@]}" png:- 2>/dev/null \
| pngquant --quality "${QMIN}-${QMAX}" --speed 1 --force --output "$OUT_PATH" - 2>/dev/null; then
OK=1
fi
# Fallback: plain magick if pngquant pipe failed (e.g. very low color depth source)
[ $OK -eq 0 ] && magick "${ARGS[@]}" "$OUT_PATH" 2>/dev/null && OK=1
else
magick "${ARGS[@]}" "$OUT_PATH" 2>/dev/null && OK=1
fi
if [ $OK -eq 1 ] && [ -f "$OUT_PATH" ] && [ -s "$OUT_PATH" ]; then
OUT_SZ=$(stat -f %z "$OUT_PATH" 2>/dev/null || echo 0)
SUM_IN=$((SUM_IN + IN_SZ))
SUM_OUT=$((SUM_OUT + OUT_SZ))
if [ "$IN_SZ" -gt 0 ]; then
PCT=$(awk -v a="$OUT_SZ" -v b="$IN_SZ" 'BEGIN{printf "%.0f", (1-a/b)*100}')
else
PCT="?"
fi
printf " %s → %s (-%s%%) %s\n" "$(human_size "$IN_SZ")" "$(human_size "$OUT_SZ")" "$PCT" "$(basename "$OUT_PATH")"
else
echo " FAILED — magick could not write $OUT_PATH"
FAIL=$((FAIL+1))
fi
done
echo ""
echo "────────────────────────────────────────────"
if [ "$SUM_IN" -gt 0 ]; then
PCT_TOT=$(awk -v a="$SUM_OUT" -v b="$SUM_IN" 'BEGIN{printf "%.0f", (1-a/b)*100}')
echo "Total: $(human_size "$SUM_IN") → $(human_size "$SUM_OUT") (-${PCT_TOT}%)"
fi
if [ "$FAIL" -eq 0 ]; then
osascript -e "display notification \"$N image(s) optimized.\" with title \"Image Optimizer\" sound name \"Glass\""
[ -n "$LAST_OUT_DIR" ] && open "$LAST_OUT_DIR"
echo ""
echo "Done. Press any key to close..."
else
osascript -e "display notification \"$FAIL of $N failed. See terminal.\" with title \"Image Optimizer\" sound name \"Basso\""
echo ""
echo "$FAIL of $N failed. Press any key to close..."
fi
read -n 1 -s
BODY
chmod +x "$TMPSCRIPT"
osascript <<EOF
tell application "Terminal"
activate
do script "$TMPSCRIPT; rm -f $TMPSCRIPT"
end tell
EOF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment