A resize command is straightforward. Keeping a batch recoverable takes more care. Writing results over the source images can leave a collection only partly processed if a conversion fails. Flattening every result into one folder also creates collisions: events/photo.jpg and products/photo.jpg would have the same destination name.

A separate output tree avoids both problems. The script below accepts a source directory, a destination directory, and a maximum edge length. It processes JPEG, PNG, and WebP files recursively, retains their relative paths and formats, and leaves the originals alone.

Keep the output outside the source tree

Given photos/events/photo.jpg, an output directory named resized receives resized/events/photo.jpg. The script rejects an output directory inside the source tree; otherwise, a later run could discover images produced by an earlier run. Existing destination files are skipped instead of overwritten.

ImageMagick 7 supplies the magick command. This example also uses Bash and GNU utilities (find, realpath, mktemp, and mv). Check magick -version before running it; some systems still install ImageMagick 6, whose command names differ.

The batch script

Save the following as batch-resize.sh:

#!/usr/bin/env bash
set -euo pipefail

if (( $# != 3 )); then
  printf 'Usage: %s SOURCE_DIR OUTPUT_DIR MAX_EDGE\n' "$0" >&2
  exit 2
fi

for tool in magick find realpath mktemp; do
  if ! command -v "$tool" >/dev/null 2>&1; then
    printf 'Missing command: %s\n' "$tool" >&2
    exit 1
  fi
done

if [[ ! -d "$1" || ! "$3" =~ ^[1-9][0-9]*$ ]]; then
  printf 'Provide an existing source directory and a positive MAX_EDGE.\n' >&2
  exit 2
fi

source=$(realpath -e -- "$1")
output=$(realpath -m -- "$2")

if [[ "$output" == "$source" || "$output" == "$source/"* ]]; then
  printf 'The output directory must be outside the source directory.\n' >&2
  exit 2
fi

mkdir -p -- "$output"
output=$(realpath -e -- "$output")

manifest=$(mktemp -- "$output/.resize-list.XXXXXXXX")
tmp=''
cleanup() {
  if [[ -n "$tmp" ]]; then rm -f -- "$tmp"; fi
  rm -f -- "$manifest"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

find "$source" -type f \( \
  -iname '*.jpg' -o -iname '*.jpeg' -o \
  -iname '*.png' -o -iname '*.webp' \
\) -print0 > "$manifest"

resized=0
skipped=0
failed=0

while IFS= read -r -d '' input; do
  relative=${input#"$source"/}
  target="$output/$relative"

  if [[ -e "$target" || -L "$target" ]]; then
    ((skipped += 1))
    continue
  fi

  parent=${target%/*}
  mkdir -p -- "$parent"
  extension=${target##*.}
  tmp=$(mktemp -- "$parent/.resize.XXXXXXXX.$extension")

  if magick "${input}[0]" -auto-orient -resize "${3}x${3}>" "$tmp"; then
    mv -n -- "$tmp" "$target"
    if [[ -e "$tmp" ]]; then
      rm -f -- "$tmp"
      ((skipped += 1))
    else
      ((resized += 1))
    fi
  else
    rm -f -- "$tmp"
    ((failed += 1))
    printf 'Failed: %q\n' "$relative" >&2
  fi
  tmp=''
done < "$manifest"

printf 'Resized: %d | skipped: %d | failed: %d\n' \
  "$resized" "$skipped" "$failed"
(( failed == 0 ))

For a collection whose longest side should be at most 1600 pixels:

chmod +x batch-resize.sh
./batch-resize.sh ./photos ./resized 1600

The third argument is a limit, not a fixed width and height. A 4000 × 3000 image becomes 1600 × 1200; a 900 × 600 image is not enlarged.

Why the file handling matters

The ImageMagick resize geometry 1600x1600> preserves the aspect ratio and shrinks images only when necessary. The > must be quoted in the shell so it is not treated as output redirection. -auto-orient applies an image’s EXIF orientation before resizing when that information is available.

find -print0 and read -d '' pass filenames through a NUL-delimited stream. A photo named holiday trip.jpg, or even one containing a newline, remains one path. The manifest also allows a failed find operation to stop the job before any conversion begins, rather than hiding a traversal error inside process substitution. See the GNU findutils filename-handling documentation.

Each result is encoded to a temporary file in its destination directory and moved into place only after ImageMagick succeeds. The suffix keeps the output format recognizable to ImageMagick. mv -n avoids replacing a destination that appeared during processing; the script checks whether the temporary file still exists because GNU mv -n can also report success when it skips a collision. A failed conversion removes its temporary file, increments the failure count, and lets the remaining images continue. The script exits unsuccessfully if any conversion failed.

Boundaries worth checking before a large run

The [0] input selector deliberately takes the first frame. Animated WebP files will therefore produce a still image, not a resized animation. Filenames select candidates; their extensions do not prove their content is a valid image, so ImageMagick may reject mislabeled or corrupt files.

Even when > prevents enlargement, ImageMagick still decodes and re-encodes an image that is already small enough. JPEG re-encoding can change quality, and metadata or color-profile handling should be checked if those properties matter to the archive. This sequential script avoids launching a large number of decoders at once, but an unusually large image can still use substantial memory.

A rerun skips files already present in the destination. That makes interrupted batches practical to resume, but changing the size limit does not regenerate older results. Use a fresh output directory when the target dimensions or encoding requirements change.