Skip to content

Instantly share code, notes, and snippets.

@modellking
Created February 11, 2026 00:14
Show Gist options
  • Select an option

  • Save modellking/bff8044235129ac87373cd67d3bd88a5 to your computer and use it in GitHub Desktop.

Select an option

Save modellking/bff8044235129ac87373cd67d3bd88a5 to your computer and use it in GitHub Desktop.
Nautilus image resize
#!/usr/bin/bash
# Coded by: Vladislav Grigoryev <vg[dot]aetera[at]gmail[dot]com>
# Edited by: Simon "modellking"
# License: GNU General Public License (GPL) version 3+
# Description: Resize images with ImageMagick from Nautilus
# Requires: bash coreutils ImageMagick nautilus zenity
# Build suggested resolutions: same aspect ratio with a common dimension (e.g. 1080)
# or integer multiple/divisor of current size. Outputs pipe-separated combo values;
# integer multiples are shown as "WxH (Nx)" in the GUI.
build_suggested_sizes() {
local w="$1" h="$2"
local max_dim=12000
[ $(( 2 * w )) -gt "$max_dim" ] && max_dim=$(( 2 * w ))
[ $(( 2 * h )) -gt "$max_dim" ] && max_dim=$(( 2 * h ))
local common_heights="480 720 1080 1440 2160"
local common_widths="640 1280 1920 2560 3840"
local factors="0.2 0.25 0.5 2 3 4 5"
local seen=""
local lines=""
# Same aspect ratio, one side = common value (display = "WxH")
for th in $common_heights; do
local tw=$(( (w * th + h/2) / h ))
[ "$tw" -gt 0 ] && [ "$tw" -le "$max_dim" ] || continue
local key="${tw}x${th}"
case "|${seen}|" in *"|${key}|"*) continue ;; esac
seen="${seen}|${key}|"
lines="${lines}${lines:+$'\n'}$(( tw * th )):${key}"
done
for tw in $common_widths; do
local th=$(( (h * tw + w/2) / w ))
[ "$th" -gt 0 ] && [ "$th" -le "$max_dim" ] || continue
local key="${tw}x${th}"
case "|${seen}|" in *"|${key}|"*) continue ;; esac
seen="${seen}|${key}|"
lines="${lines}${lines:+$'\n'}$(( tw * th )):${key}"
done
# Integer multiple or divisor of current size (display = "WxH (Nx)" or "WxH (~Nx)" when not exact)
for f in $factors; do
local nw nh exact=1
case "$f" in
0.2) nw=$(( w / 5 )); nh=$(( h / 5 )); [ "$(( w % 5 ))" -ne 0 ] || [ "$(( h % 5 ))" -ne 0 ] && exact=0 ;;
0.25) nw=$(( w / 4 )); nh=$(( h / 4 )); [ "$(( w % 4 ))" -ne 0 ] || [ "$(( h % 4 ))" -ne 0 ] && exact=0 ;;
0.5) nw=$(( w / 2 )); nh=$(( h / 2 )); [ "$(( w % 2 ))" -ne 0 ] || [ "$(( h % 2 ))" -ne 0 ] && exact=0 ;;
1) nw=$w; nh=$h ;;
2) nw=$(( w * 2 )); nh=$(( h * 2 )) ;;
3) nw=$(( w * 3 )); nh=$(( h * 3 )) ;;
4) nw=$(( w * 4 )); nh=$(( h * 4 )) ;;
5) nw=$(( w * 5 )); nh=$(( h * 5 )) ;;
*) continue ;;
esac
[ "$nw" -gt 0 ] && [ "$nh" -gt 0 ] && [ "$nw" -le "$max_dim" ] && [ "$nh" -le "$max_dim" ] || continue
local key="${nw}x${nh}"
case "|${seen}|" in *"|${key}|"*) continue ;; esac
seen="${seen}|${key}|"
if [ "$exact" -eq 0 ]; then
lines="${lines}${lines:+$'\n'}$(( nw * nh )):${nw}x${nh} (~${f}x)"
else
lines="${lines}${lines:+$'\n'}$(( nw * nh )):${nw}x${nh} (${f}x)"
fi
done
# Sort by pixels and output pipe-separated for zenity combo
[ -z "$lines" ] && lines="$(( w * h )):${w}x${h}"
echo "$lines" | sort -t: -k1 -n | cut -d: -f2- | paste -sd'|'
}
script_init() {
IFS="|" read -r IMG_SIZE IMG_WSIZE IMG_HSIZE IMG_OUT < <(zenity \
--forms \
--title="Image converter" \
--width=400 \
--height=380 \
--text="${DIALOG_TEXT}" \
--ok-label="Resize" \
--add-combo="Size" \
--combo-values="${COMBO_VALUES}" \
--add-entry="Width" \
--add-entry="Height" \
--add-list="Output" \
--list-values="copy / default|replace")
}
# Collect "w h" per line for each selected file (from paths or from pre-fetched list).
# If $2 is non-empty, use it as newline-separated "w h"; else read from paths $1.
# Sets global BATCH_COLLECT_RES_RESULT (newline-separated "w h" lines).
batch_collect_res() {
local all_res=()
if [ -n "$2" ]; then
while read -r line; do [ -n "$line" ] && all_res+=("$line"); done <<< "$2"
else
while IFS= read -r path; do
[ -z "$path" ] && continue
[ -f "$path" ] || continue
wh=$(identify -format "%w %h" "$path" 2>/dev/null) || continue
read -r w h <<< "$wh"
[ -n "$w" ] && [ -n "$h" ] && all_res+=("$w $h")
done < <(printf '%s\n' "$1")
fi
BATCH_COLLECT_RES_RESULT=$(printf '%s\n' "${all_res[@]}")
}
# Build multi-line "Original resolutions:" text for batch (one resolution per line, full list).
# Uses pre-collected res_lines (newline-separated "w h") or reads from paths $1.
batch_original_text() {
local all_res=()
if [ -n "$2" ]; then
while read -r line; do [ -n "$line" ] && all_res+=("$line"); done <<< "$2"
else
while read -r path; do
[ -f "$path" ] || continue
read -r w h < <(identify -format "%w %h" "$path" 2>/dev/null) || continue
all_res+=("$w $h")
done <<< "$1"
fi
[ ${#all_res[@]} -eq 0 ] && echo "" && return
local min_w=99999 max_w=0 min_h=99999 max_h=0
local r w h
for r in "${all_res[@]}"; do
read -r w h <<< "$r"
[ "$w" -lt "$min_w" ] && min_w=$w
[ "$w" -gt "$max_w" ] && max_w=$w
[ "$h" -lt "$min_h" ] && min_h=$h
[ "$h" -gt "$max_h" ] && max_h=$h
done
local ext_minw="" ext_maxw="" ext_minh="" ext_maxh=""
for r in "${all_res[@]}"; do
read -r w h <<< "$r"
[ "$w" -eq "$min_w" ] && [ -z "$ext_minw" ] && ext_minw="${w}×${h}"
[ "$w" -eq "$max_w" ] && [ -z "$ext_maxw" ] && ext_maxw="${w}×${h}"
[ "$h" -eq "$min_h" ] && [ -z "$ext_minh" ] && ext_minh="${w}×${h}"
[ "$h" -eq "$max_h" ] && [ -z "$ext_maxh" ] && ext_maxh="${w}×${h}"
done
local display_list=()
local seen=""
for s in "$ext_minw" "$ext_maxw" "$ext_minh" "$ext_maxh"; do
[ -z "$s" ] && continue
[[ "|${seen}|" == *"|${s}|"* ]] && continue
seen="${seen}|${s}|"
display_list+=("$s")
done
local total_unique=0
seen=""
for r in "${all_res[@]}"; do
read -r w h <<< "$r"
s="${w}×${h}"
[[ "|${seen}|" != *"|${s}|"* ]] && { seen="${seen}|${s}|"; total_unique=$(( total_unique + 1 )); }
done
if [ "$total_unique" -gt 4 ]; then
display_list+=("...")
fi
# One resolution per line to avoid overflow; full information
echo "Original resolutions:"
printf '%s\n' "${display_list[@]}"
}
# Return 0 if all images divide evenly by factor (0.2/0.25/0.5), else 1.
batch_scale_needs_tilde() {
local res_lines="$1" factor="$2" w h
while read -r w h; do
case "$factor" in
0.2) [ "$(( w % 5 ))" -eq 0 ] && [ "$(( h % 5 ))" -eq 0 ] || return 1 ;;
0.25) [ "$(( w % 4 ))" -eq 0 ] && [ "$(( h % 4 ))" -eq 0 ] || return 1 ;;
0.5) [ "$(( w % 2 ))" -eq 0 ] && [ "$(( h % 2 ))" -eq 0 ] || return 1 ;;
esac
done <<< "$res_lines"
return 0
}
# Given scale factor (0.2, 0.25, 0.5, 1, 2, 3, 4, 5), output target "WxH" for dimensions w h.
scale_to_size() {
local w=$1 h=$2 scale=$3
local tw th
case "$scale" in
0.2) tw=$(( w / 5 )); th=$(( h / 5 )) ;;
0.25) tw=$(( w / 4 )); th=$(( h / 4 )) ;;
0.5) tw=$(( w / 2 )); th=$(( h / 2 )) ;;
1) tw=$w; th=$h ;;
2) tw=$(( w * 2 )); th=$(( h * 2 )) ;;
3) tw=$(( w * 3 )); th=$(( h * 3 )) ;;
4) tw=$(( w * 4 )); th=$(( h * 4 )) ;;
5) tw=$(( w * 5 )); th=$(( h * 5 )) ;;
*) echo ""; return ;;
esac
echo "${tw}x${th}"
}
script_exec() {
if [ -n "${IMG_WSIZE}" ] || [ -n "${IMG_HSIZE}" ]
then IMG_SIZE="${IMG_WSIZE}x${IMG_HSIZE}"
IMG_SCALE=""
else
# Detect integer scale: "3840x2160 (2x)", "128x128 (~0.5x)", or multiplier-only "2x" / "~0.5x"
IMG_SIZE_RAW="${IMG_SIZE}"
if [[ "$IMG_SIZE_RAW" =~ \ \(~?([0-9.]+)x\) ]]; then
IMG_SCALE="${BASH_REMATCH[1]}"
IMG_SIZE="${IMG_SIZE_RAW%% (*}"
elif [[ "$IMG_SIZE_RAW" =~ ^~?[0-9.]+x$ ]]; then
IMG_SCALE="${IMG_SIZE_RAW#\~}"
IMG_SCALE="${IMG_SCALE%x}"
IMG_SIZE=""
else
IMG_SCALE=""
IMG_SIZE="${IMG_SIZE_RAW%% (*}"
fi
fi
if [ -z "${IMG_SIZE/ /}" ] && [ -z "${IMG_SCALE}" ]
then IMG_SIZE="${IMG_DSIZE}x${IMG_DSIZE}"
fi
while read -r IMG_PATH
do
IMG_OPATH="${IMG_PATH%/*}/resized.${IMG_PATH##*/}"
if [ -n "${IMG_SCALE}" ]; then
read -r w h < <(identify -format "%w %h" "${IMG_PATH}" 2>/dev/null) || true
if [ -n "$w" ] && [ -n "$h" ]; then
IMG_SIZE=$(scale_to_size "$w" "$h" "$IMG_SCALE")
fi
[ -z "$IMG_SIZE" ] && continue
fi
convert "${IMG_PATH}" -resize "${IMG_SIZE}" "${IMG_OPATH}"
if [ "${IMG_OUT}" = "replace" ]
then mv -f "${IMG_OPATH}" "${IMG_PATH}"
fi
done <<< "${IMG_PATH}" | zenity \
--progress \
--pulsate \
--auto-close \
--no-cancel \
--title="Image converter" \
--text="Processing files..."
}
# Programmatic entry point: given newline-separated image paths, set COMBO_VALUES, DIALOG_TEXT,
# IMG_DSIZE, FILE_COUNT (and RESIZE_DEBUG_* for tests). Uses first image only for dropdown labels.
resize_generate_dialog_state() {
local paths="$1"
IMG_PATH="$paths"
FIRST_IMG="${paths%%$'\n'*}"
RESIZE_DEBUG_FIRST_IMG="$FIRST_IMG"
RESIZE_DEBUG_FIRST_W=""
RESIZE_DEBUG_FIRST_H=""
IMG_DSIZE="1024"
IMG_W=""
IMG_H=""
if [ -n "$FIRST_IMG" ] && [ -f "$FIRST_IMG" ]; then
read -r IMG_W IMG_H < <(identify -format "%w %h" "$FIRST_IMG" 2>/dev/null) || true
RESIZE_DEBUG_FIRST_W="$IMG_W"
RESIZE_DEBUG_FIRST_H="$IMG_H"
fi
if [ -n "${IMG_W}" ] && [ -n "${IMG_H}" ] && [ "${IMG_W}" -gt 0 ] && [ "${IMG_H}" -gt 0 ]; then
COMBO_VALUES=$(build_suggested_sizes "$IMG_W" "$IMG_H")
IMG_DSIZE="${IMG_W}x${IMG_H}"
FILE_COUNT=$(($(wc -l <<< "${IMG_PATH}")))
if [ "$FILE_COUNT" -gt 1 ]; then
batch_collect_res "${IMG_PATH}" ""
ALL_RES_LINES="${BATCH_COLLECT_RES_RESULT:-}"
# Only show multiplier-only labels ("2x") when resolutions differ; same res -> keep "WxH (2x)" style.
ALL_SAME=1
unique_count=$(printf '%s\n' "$ALL_RES_LINES" | grep -v '^[[:space:]]*$' | sort -u | wc -l)
[ "$unique_count" -gt 1 ] && ALL_SAME=0
RESIZE_DEBUG_ALL_SAME="$ALL_SAME"
RESIZE_DEBUG_UNIQUE_COUNT="$unique_count"
if [ "$ALL_SAME" -eq 0 ]; then
COMBO_BATCH=""
IFS='|' read -ra combo_arr <<< "$COMBO_VALUES"
for v in "${combo_arr[@]}"; do
if [[ "$v" =~ \(~?([0-9.]+)x\) ]]; then
f="${BASH_REMATCH[1]}"
case "$f" in
0.2|0.25|0.5)
if batch_scale_needs_tilde "$ALL_RES_LINES" "$f"; then v="${f}x"; else v="~${f}x"; fi ;;
*) v="${f}x" ;;
esac
fi
COMBO_BATCH="${COMBO_BATCH}${COMBO_BATCH:+|}${v}"
done
COMBO_VALUES="$COMBO_BATCH"
fi
ORIG_BLOCK=$(batch_original_text "${IMG_PATH}" "$ALL_RES_LINES")
DIALOG_TEXT="${ORIG_BLOCK}
${FILE_COUNT} file(s) selected"
else
DIALOG_TEXT="Original: ${IMG_W}×${IMG_H}
${FILE_COUNT} file(s) selected"
fi
else
FILE_COUNT=$(($(wc -l <<< "${IMG_PATH}")))
COMBO_VALUES="640x480|1280x720|1920x1080|2560x1440|3840x2160"
DIALOG_TEXT="${FILE_COUNT} file(s) selected"
fi
}
# Debug: print current dialog state (for test mode).
resize_debug_print() {
echo "--- resize_debug ---"
echo "RESIZE_DEBUG_FIRST_IMG=${RESIZE_DEBUG_FIRST_IMG:-}"
echo "RESIZE_DEBUG_FIRST_W=${RESIZE_DEBUG_FIRST_W:-}"
echo "RESIZE_DEBUG_FIRST_H=${RESIZE_DEBUG_FIRST_H:-}"
echo "RESIZE_DEBUG_ALL_SAME=${RESIZE_DEBUG_ALL_SAME:-}"
echo "RESIZE_DEBUG_UNIQUE_COUNT=${RESIZE_DEBUG_UNIQUE_COUNT:-}"
echo "FILE_COUNT=${FILE_COUNT:-}"
echo "IMG_DSIZE=${IMG_DSIZE:-}"
echo "COMBO_VALUES=${COMBO_VALUES:-}"
echo "DIALOG_TEXT<<EOF"
echo "${DIALOG_TEXT:-}"
echo "EOF"
echo "--- end ---"
}
# --- Main / test entry ---
if [ -n "${TEST_RESIZE:-}" ]; then
# Test mode: paths from arguments (best), or RESIZE_TEST_PATHS_FILE, or TEST_PATHS.
if [ $# -gt 0 ]; then
IMG_PATH="$(printf '%s\n' "$@")"
elif [ -n "${RESIZE_TEST_PATHS_FILE:-}" ] && [ -f "${RESIZE_TEST_PATHS_FILE}" ]; then
IMG_PATH=$(< "${RESIZE_TEST_PATHS_FILE}")
else
IMG_PATH="${TEST_PATHS:-}"
fi
resize_generate_dialog_state "$IMG_PATH"
resize_debug_print
exit 0
fi
IMG_PATH="${NAUTILUS_SCRIPT_SELECTED_FILE_PATHS/%$'\n'/}"
resize_generate_dialog_state "$IMG_PATH"
if script_init
then script_exec
fi
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment