Two files named photo.jpg and photo.png in the same directory would both become photo.webp. A batch converter that notices the collision only after it starts encoding may leave one image unconverted or replace an earlier result. It is better to check the entire input list first.
The Bash script here uses cwebp for encoding and keeps a separate output tree. Its default auto mode encodes JPEG with lossy WebP and PNG with lossless WebP. The source images stay in place; existing destination files are skipped.
Map source files to distinct destinations
Relative directories are preserved: images/products/front.jpg becomes webp/products/front.webp. The script rejects an output directory inside the source tree so a later run cannot discover its own products. It also checks for pairs such as front.jpg and front.png before converting anything. A collision stops the run rather than silently choosing a file.
The file list is NUL-delimited. find -print0 and read -d '' keep spaces and newlines inside filenames intact. Writing that list to a manifest also makes a failed directory scan stop the job before encoding starts, instead of hiding a find error in a pipeline. See GNU find’s filename-handling documentation.
The Bash script
Save this as batch-to-webp.sh. It uses Bash 4 or later, cwebp, and GNU find, realpath, mktemp, and ln.
#!/usr/bin/env bash
set -euo pipefail
if (( $# < 2 || $# > 4 )); then
printf 'Usage: %s SOURCE_DIR OUTPUT_DIR [QUALITY] [auto|lossy|lossless]\n' "$0" >&2
exit 2
fi
quality=${3:-80}
mode=${4:-auto}
if [[ ! -d $1 || ! $quality =~ ^(100|[1-9]?[0-9])$ ]]; then
printf 'Use an existing source directory and a quality from 0 to 100.\n' >&2
exit 2
fi
case "$mode" in
auto|lossy|lossless) ;;
*) printf 'Mode must be auto, lossy, or lossless.\n' >&2; exit 2 ;;
esac
for tool in cwebp find realpath mktemp ln; do
if ! command -v "$tool" >/dev/null 2>&1; then
printf 'Missing command: %s\n' "$tool" >&2
exit 1
fi
done
source=$(realpath -e -- "$1")
output=$(realpath -m -- "$2")
if [[ $output == "$source" || $output == "$source/"* ]]; then
printf 'Place the output directory outside the source tree.\n' >&2
exit 2
fi
mkdir -p -- "$output"
output=$(realpath -e -- "$output")
manifest=$(mktemp -- "$output/.webp-inputs.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' \) -print0 > "$manifest"
declare -A claimed=()
while IFS= read -r -d '' input; do
relative=${input#"$source"/}
target="$output/${relative%.*}.webp"
if [[ -n ${claimed[$target]+present} ]]; then
printf 'Conflicting names: %q and %q both map to %q\n' \
"${claimed[$target]}" "$relative" "$target" >&2
exit 1
fi
claimed["$target"]=$relative
done < "$manifest"
converted=0
skipped=0
failed=0
while IFS= read -r -d '' input; do
relative=${input#"$source"/}
target="$output/${relative%.*}.webp"
if [[ -e $target || -L $target ]]; then
((skipped += 1))
continue
fi
mkdir -p -- "${target%/*}"
tmp=$(mktemp -- "${target%/*}/.webp.XXXXXXXX")
options=(-q "$quality")
if [[ $mode == lossless || ( $mode == auto && ${input,,} == *.png ) ]]; then
options=(-lossless -q "$quality")
fi
if cwebp -quiet "${options[@]}" "$input" -o "$tmp" && ln -- "$tmp" "$target"; then
((converted += 1))
elif [[ -e $target || -L $target ]]; then
((skipped += 1))
else
((failed += 1))
printf 'Failed: %q\n' "$relative" >&2
fi
rm -f -- "$tmp"
tmp=''
done < "$manifest"
printf 'Converted: %d | skipped: %d | failed: %d\n' \
"$converted" "$skipped" "$failed"
(( failed == 0 ))On Debian or Ubuntu, install the webp package and run the script with a source directory, a separate destination, and an optional quality and mode:
sudo apt install webp
chmod +x batch-to-webp.sh
./batch-to-webp.sh ./images ./webp 80 auto
./batch-to-webp.sh ./images ./webp-lossy 75 lossyThe quality defaults to 80 and the mode defaults to auto. The other modes are lossy, which encodes every input with lossy WebP, and lossless, which uses lossless WebP for every input. Use a different destination when comparing settings; an existing result is intentionally not regenerated.
Encoding and metadata are separate decisions
For lossy encoding, cwebp -q 80 trades some image detail for size. The same -q argument has a different meaning with -lossless: higher values spend more effort on compression and can produce smaller files, without deliberately discarding decoded pixel detail. Turning a JPEG into lossless WebP does not recover information already lost when that JPEG was created.
Lossless PNG conversion preserves visible pixel detail and transparency, but cwebp does not preserve the RGB values of fully transparent pixels unless -exact is used. That distinction matters when an image-processing pipeline later uses those hidden colour values. Lossless output is not guaranteed to be smaller than the source PNG.
By default, cwebp does not copy EXIF, ICC, or XMP metadata. This removes unwanted location metadata from many web assets, but it can also discard a colour profile or an EXIF orientation tag that a photo needs for the intended appearance. Normalize orientation before converting when necessary, or choose -metadata exif,icc deliberately after checking what the source contains. The cwebp reference documents these options and their format-dependent limits.
Publishing results without overwriting anything
Each image is encoded into a temporary file beside its destination. After cwebp succeeds, ln creates the final filename only if it does not already exist. Removing the temporary name leaves the completed WebP in place. This avoids exposing a partially encoded output and also prevents a concurrent run from overwriting a result that appeared after the initial check.
A damaged or mislabeled image is counted as a failure while the remaining inputs continue. The final exit status is nonzero if any conversion failed. Repeating a run skips existing outputs, which helps after an interruption, but it does not detect sources modified since the previous run. A new output directory is the safer choice after changing source images, quality, or encoding mode.
The script processes one file at a time rather than launching many decoders together. An exceptionally large image can still consume substantial memory. For collections where a missing colour profile, a rotated photo, or an unexpectedly large WebP matters, inspect representative outputs before replacing any references to the original files.