Batch image resizing and conversion with ImageMagick
Generate 480-, 960-, and 1,440-pixel versions of each image, in both WebP and JPEG, for a responsive website. This Bash script writes a fresh batch without changing the originals. A small HTML page then lets the browser choose among the generated files.
The task here is preparing several sizes and a format fallback for the same website image. For a single PNG copy per JPEG, including nested folders, use the JPEG-to-PNG walkthrough.
Check the tools and input scope
Use Bash 5.3.15 and ImageMagick 7.1.2-31 on Linux with a case-sensitive filesystem for this recipe.
Run the command blocks in Bash. Installation options for ImageMagick 7 are listed in the
official downloads. A package named imagemagick may
provide version 6; this recipe uses the version 7 magick executable. It does not cover native
Windows shells or macOS’s older system Bash.
bash --version && magick -version && magick -list format
Check for rw in the format list for JPEG, PNG, and WEBP. The script reads still JPEG
(.jpg or .jpeg), PNG, and WebP files directly inside one input folder, including hidden files
and mixed-case extensions. It skips subdirectories and symbolic links. Each image must be at least
1,440 pixels wide after applying orientation metadata. Smaller images are rejected rather than
enlarged. The script rejects sequences the decoder exposes, including animated WebP. Use still
PNGs: as the format notes explain, the PNG decoder
reads only the first frame of an animated PNG (APNG).
This script does not preserve animation.
The outputs are opaque, 8-bit images. Transparent pixels are composited onto white in both formats.
ImageMagick’s -colorspace sRGB converts
the decoded colors before compositing. This is a local recipe for ordinary web photographs, not an
ICC proofing workflow: profile-sensitive or wide-gamut assets need an appropriate
-profile conversion before stripping
profiles. Keep your originals for that work.
Create a reproducible input
In a working directory, create a sample image split into blue and red halves. The && stops the
write if input-images already exists. You can use this directory inside an existing project;
the commands do not install packages or change that project’s configuration.
mkdir input-images &&
magick -size 2400x1600 xc:steelblue -fill tomato \
-draw 'rectangle 1200,0 2399,1599' -depth 8 jpeg:- > input-images/hero.jpg
For your own batch, put supported images into the input folder and keep it unchanged while the
script runs. Use another working directory if you already have an input-images folder.
Save the batch script
Save the following as a new batch-images.sh beside input-images. Each source gets a directory
named after its complete filename: hero.jpg and hero.png remain distinct. Shell redirection
feeds ImageMagick’s standard streams, so
spaces, leading hyphens, brackets, and % patterns in filenames stay literal.
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
shopt -s nullglob dotglob
if (( $# != 2 )); then
printf 'Usage: bash batch-images.sh INPUT_DIR NEW_OUTPUT_DIR\n' >&2
exit 1
fi
# A final slash preserves directory names ending in newlines during command substitution.
input_argument=$1
[[ "$input_argument" == /* ]] || input_argument="./$input_argument"
input=$(cd -P -- "$input_argument" && printf '%s/' "$PWD")
input=${input%/}
output=$2
[[ "$output" == /* ]] || output="./$output"
files=()
for file in "$input"/*; do
[[ -f "$file" && ! -L "$file" ]] || continue
case ${file,,} in
*.jpg|*.jpeg|*.png|*.webp) files+=("$file") ;;
esac
done
if (( ${#files[@]} == 0 )); then
printf 'No supported images in %s\n' "$input" >&2
exit 1
fi
# mkdir refuses existing destinations, including existing symbolic links.
mkdir -- "$output"
stage="$output/.source.miff"
diagnostics="$output/.diagnostics"
trap 'rm -f -- "$stage" "$diagnostics"' EXIT
fail_image() {
printf 'Could not process: %s\n' "$file" >&2
cat -- "$diagnostics" >&2
exit 1
}
for file in "${files[@]}"; do
name=${file##*/}
case ${file,,} in
*.jpg|*.jpeg) coder=jpeg ;;
*.png) coder=png ;;
*.webp) coder=webp ;;
esac
# Normalize once, before resizing or discarding the source metadata.
if ! magick -regard-warnings "$coder:-" -auto-orient -colorspace sRGB \
-background white -alpha remove -alpha off -strip -depth 8 miff:- \
< "$file" 2> "$diagnostics" > "$stage" || [[ -s "$diagnostics" ]]; then
fail_image
fi
if ! dimensions=$(magick -regard-warnings miff:- -format '%n %w %h\n' info: \
< "$stage" 2> "$diagnostics") || [[ -s "$diagnostics" ]]; then
fail_image
fi
if [[ ! "$dimensions" =~ ^1\ ([0-9]+)\ ([0-9]+)$ ]]; then
printf 'Expected one still image: %s\n' "$file" >&2
exit 1
fi
if (( BASH_REMATCH[1] < 1440 )); then
printf 'Image must be at least 1440 pixels wide after orientation: %s\n' "$file" >&2
exit 1
fi
mkdir -- "$output/$name"
for width in 480 960 1440; do
for format in webp jpg; do
target="$output/$name/$width.$format"
if ! magick -regard-warnings miff:- -resize "${width}x>" -quality 82 "$format:-" \
< "$stage" 2> "$diagnostics" > "$target" || [[ -s "$diagnostics" ]]; then
rm -f -- "$target"
fail_image
fi
done
done
printf 'Created six images for %s\n' "$name"
done
printf 'Finished processing %s source image(s) in %s\n' "${#files[@]}" "$output"
The script applies -auto-orient
before checking width. -resize '480x>'
limits width while preserving aspect ratio; the other two sizes work the same way. The minimum
input width ensures that all three outputs have exactly the advertised widths.
-quality 82 is an encoder setting,
not 82% visual quality or a promised file-size reduction. Both outputs use lossy encoding. Compare
the files’ appearance and byte sizes before choosing a quality setting for real photographs.
Run and inspect the batch
bash batch-images.sh input-images web-images
For the sample, success prints Created six images for hero.jpg followed by
Finished processing 1 source image(s) in ./web-images. The directory contains hero.jpg/480.webp,
hero.jpg/960.webp, and hero.jpg/1440.webp, plus the corresponding .jpg files.
The original input-images/hero.jpg is unchanged. A rerun refuses the existing web-images
directory; choose a new destination to create another batch.
The script processes names in C locale order, stops at the first error, and returns a nonzero
status identifying the source file.
Earlier successful derivatives remain, and a failed derivative is removed. This is a partial batch,
not a published release: inspect it and correct the source before rerunning into a new directory.
It treats diagnostics as failures as well as checking status, because
-regard-warnings only turns some
decoder warnings into errors. A decoder can still recover damaged content without a diagnostic;
successful conversion does not prove that an input is intact.
Check all six sample derivatives by reading their bytes, avoiding filename-pattern interpretation:
(
set -e
for width in 480 960 1440; do
for format in webp jpg; do
file="web-images/hero.jpg/$width.$format"
printf '%s: ' "$file"
magick "$format:-" -format '%m %wx%h\n' info: < "$file"
done
done
)
Expect WEBP 480x320 and JPG 480x320, then the same formats at 960x640 and 1440x960.
Open the outputs too: confirm that both colored halves and the far edges are present. With your
own images, check orientation, subjects near the edges, and the white background where pixels
were transparent. The dimensions alone cannot establish those properties.
Use the generated files in a responsive page
Save this complete document as web-images/preview.html. It refers to the actual sample outputs;
open that file directly in your browser. No server is needed for this local image preview.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Responsive image preview</title>
<style>
body { margin: 0; }
img { display: block; width: 100%; max-width: 960px; height: auto; }
</style>
<picture>
<source type="image/webp"
srcset="hero.jpg/480.webp 480w, hero.jpg/960.webp 960w, hero.jpg/1440.webp 1440w"
sizes="(max-width: 960px) 100vw, 960px">
<img src="hero.jpg/960.jpg"
srcset="hero.jpg/480.jpg 480w, hero.jpg/960.jpg 960w, hero.jpg/1440.jpg 1440w"
sizes="(max-width: 960px) 100vw, 960px"
width="1440" height="960" alt="Blue and red halves of a sample image">
</picture>
</html>
The picture and srcset rules
let a browser that supports WebP select from the first source; the img supplies JPEG candidates
and a default src. Each w descriptor is the file’s actual pixel width. sizes describes the
displayed width, matching the CSS limit of 960 pixels. Change the paths, alternative text,
width/height ratio, and sizes when adapting this to your own page. URL-encode filenames with
special characters: a directory named photo%03d.jpg needs photo%2503d.jpg/480.webp in a URL.
To inspect the selected file, open the browser’s developer console and run:
document.querySelector('img').currentSrc
Try a fresh page at a narrow viewport, then a wider one. Device pixel ratio and the browser’s
selection policy also affect the choice; resizing an already loaded page may retain a larger
cached image. Removing the source element in a temporary copy lets you inspect JPEG selection
in the same browser. A broken WebP URL does not automatically select the JPEG fallback.
Keep the complete set of derivatives together when moving the markup into your website. The local preview demonstrates file generation and browser selection; measure your actual page if you need to establish a loading-time or bandwidth improvement.
