Created
February 11, 2026 00:14
-
-
Save modellking/bff8044235129ac87373cd67d3bd88a5 to your computer and use it in GitHub Desktop.
Nautilus image resize
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
| #!/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