Batch-optimize JPEGs with jpegtran without hiding failures
Use jpegtran to change how existing JPEGs are encoded without another lossy compression pass.
For a local batch, write to a new directory and check every conversion’s exit status. The Bash script
below keeps your originals and metadata, creates progressive JPEGs, and returns a failure status if
any selected image fails.
Choose what to preserve
jpegtran works with compressed DCT coefficients instead of decoding pixels and recompressing them.
That preserves the existing JPEG’s image data; it cannot restore detail lost when the JPEG was first
encoded. The libjpeg-turbo usage guide
explains this distinction.
This workflow uses -optimize for Huffman coding and -progressive for multiple image scans.
A compatible viewer can display an initial preview before receiving the complete progressive JPEG.
Neither flag guarantees a smaller file, so compare byte counts before replacing assets.
The metadata policy is -copy all: retain extra markers, including EXIF and ICC profiles.
This matters because EXIF orientation and color profiles affect how viewers display an image.
It also retains potentially private information such as GPS coordinates. These outputs are for
local review, not automatic public upload.
Using -copy none would discard those markers, potentially changing displayed orientation or color
even though the coefficients survive. -copy icc retains the color profile but still drops EXIF
orientation. Choose a separate metadata-removal workflow if you need one; this batch does not rotate
pixels or normalize orientation. See the marker-copy options.
Check the tools
Use Bash and libjpeg-turbo’s jpegtran on Linux, with the usual mkdir, mv, rm, and wc
utilities. The example was tested with Bash 5.3.15 and libjpeg-turbo 3.2.0. Install libjpeg-turbo’s
command-line tools through your distribution or the project’s
release packages, then check which
implementation you have:
bash --version
jpegtran -version
The second command should identify libjpeg-turbo. Other projects also distribute a program named
jpegtran; their options and output can differ. The input scope here is ordinary 8-bit, DCT-based
JPEGs, including sequential and progressive files.
Run a batch into a new directory
Save this as optimize_images.sh in the directory containing your JPEGs. Run it with Bash, not
sh. It selects regular files ending in .jpg or .jpeg, including uppercase extensions and
hidden files, without descending into subdirectories or following symlink entries. Filenames with
spaces, leading hyphens, and literal % characters are supported.
The output directory must not already exist. Each successful output keeps its original filename
under optimized/. A temporary file holds each conversion until jpegtran succeeds, so a failed
conversion does not become a finished output. Run this in a directory you control, without other
processes changing its files while the batch runs.
#!/usr/bin/env bash
set -u
if (( $# != 0 )); then
printf 'Run this script without arguments in your JPEG directory.\n' >&2
exit 2
fi
if ! command -v jpegtran >/dev/null; then
printf 'jpegtran is not installed or is missing from PATH.\n' >&2
exit 2
fi
shopt -s nullglob nocaseglob dotglob
images=()
for img in ./*.jpg ./*.jpeg; do
if [[ -f "$img" && ! -L "$img" ]]; then
images+=("$img")
fi
done
if (( ${#images[@]} == 0 )); then
printf 'No JPEG files selected.\n' >&2
exit 1
fi
if ! mkdir -- ./optimized; then
printf 'Cannot create optimized; move any previous results aside before rerunning.\n' >&2
exit 1
fi
temporary=./optimized/.jpegtran.tmp
trap 'rm -f -- "$temporary"' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
status=0
for img in "${images[@]}"; do
output="./optimized/${img#./}"
if jpegtran -strict -copy all -optimize -progressive -outfile "$temporary" "$img" &&
mv -- "$temporary" "$output"; then
printf 'Wrote %q\n' "$output"
else
printf 'Failed: %q\n' "$img" >&2
status=1
fi
done
exit "$status"
From that same directory, run:
bash optimize_images.sh
An exit status of zero means every selected file was converted and moved to its destination.
Invalid images and conversion warnings fail because of -strict. Diagnostics stay visible, the
batch continues with the remaining images, and its final status is nonzero. Successful outputs from
a mixed batch remain available for inspection. An empty selection also returns nonzero and creates
no output directory.
A rerun refuses an existing optimized directory, file, or symlink and leaves it untouched. Review
and move previous results aside yourself before trying again. In a build job, use the invocation
above as a step whose failure stops deployment; adding || echo ... would hide that failure again.
This is a local asset workflow, not a sandbox for untrusted uploads.
Measure the result
For an input named photo.jpg, compare the two files after a successful batch:
wc -c -- ./photo.jpg ./optimized/photo.jpg
The first two rows are the original and output byte counts; the last row is their sum. There is no fixed percentage saving. A small JPEG or an already optimized file can grow when encoded progressively. This script keeps that result for review rather than silently substituting the original. Open representative pairs in the viewer your audience uses, checking orientation and color as well as size. Matching decoded image data does not by itself test a viewer’s metadata handling.
Understand failures and the limits of -perfect
For a corrupt or truncated JPEG, inspect the diagnostic and recover the source image before rerunning. Do not discard stderr or accept a partial file just because something was written. If the output directory cannot be created, check whether it already exists and whether you can write to its parent.
-perfect is a check for geometric transformations, not an extra quality setting or a file-integrity
check. It is unnecessary for the batch’s entropy optimization and progressive conversion.
For rotation or flipping, it rejects transformations whose edge blocks cannot be transformed
completely. With common 4:2:0 sampling, the iMCU is 16 × 16 pixels: a 90-degree clockwise rotation
requires the height to be divisible by 16, a 270-degree rotation requires the width, and a 180-degree
rotation requires both. Other sampling factors change those boundaries. These conditions are
explicit in libjpeg-turbo’s transformation code.
-trim discards untransformable edge pixels, so it is a cropping decision. It is not a way to
preserve the complete image after -perfect refuses a rotation. Rotating pixels also requires a
deliberate EXIF orientation and thumbnail policy; copying existing metadata is not automatic
orientation repair.
Decide when to re-encode instead
If the measured saving is too small, changing dimensions, lowering JPEG quality, or converting to
WebP or AVIF is a different task. Those choices require decoding and encoding with an appropriate
image tool and evaluating the resulting appearance. Use this jpegtran batch when keeping the
existing JPEG image data is the constraint; use a separate re-encoding workflow when you can accept
changes to it.
