script to reduce the image size

This commit is contained in:
Ludger Sicking 2026-08-18 12:33:36 +02:00
parent 163f3585a9
commit c2139730ae
1 changed files with 213 additions and 0 deletions

View File

@ -0,0 +1,213 @@
#!/usr/bin/env bash
# Resize/convert all png|PNG|jpeg|jpg|JPG images under content/ to .jpg,
# long edge <= MAX_DIM px, file size <= MAX_BYTES, in place.
# Exception: PNGs already under PNG_KEEP_MAX_DIM px and PNG_KEEP_MAX_BYTES
# bytes are left as PNG, untouched.
# Updates filename references in content/**/*.md when a file gets renamed.
#
# Usage: scripts/resize-and-convert-images.sh [content-dir] --apply
# scripts/resize-and-convert-images.sh [content-dir] (dry run, default)
#
# SAFETY: this script defaults to a dry run and changes NOTHING unless you
# pass --apply explicitly. Unknown arguments are a hard error, not silently
# ignored, so a typo can never turn a dry run into a real run.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONTENT_DIR="$SCRIPT_DIR/../content"
MAX_DIM=1600
MAX_BYTES=307200 # 300 KiB
PNG_KEEP_MAX_DIM=1600
PNG_KEEP_MAX_BYTES=204800 # 200 KiB
DRY_RUN=1
CONTENT_DIR_SET=0
usage() {
echo "Usage: $0 [content-dir] --apply" >&2
echo " $0 [content-dir] # dry run (default, no changes written)" >&2
}
for arg in "$@"; do
case "$arg" in
--apply) DRY_RUN=0 ;;
--dry-run) DRY_RUN=1 ;;
--help|-h) usage; exit 0 ;;
-*)
echo "Error: unknown option '$arg'" >&2
usage
exit 1
;;
*)
if [ "$CONTENT_DIR_SET" -eq 1 ]; then
echo "Error: unexpected extra argument '$arg'" >&2
usage
exit 1
fi
CONTENT_DIR="$arg"
CONTENT_DIR_SET=1
;;
esac
done
if ! command -v convert >/dev/null 2>&1; then
echo "Error: ImageMagick 'convert' not found." >&2
exit 1
fi
if ! command -v identify >/dev/null 2>&1; then
echo "Error: ImageMagick 'identify' not found." >&2
exit 1
fi
if [ ! -d "$CONTENT_DIR" ]; then
echo "Error: content dir '$CONTENT_DIR' not found." >&2
exit 1
fi
converted=0
renamed_only=0
skipped_ok=0
skipped_png_small=0
skipped_collision=0
still_too_big=0
errors=0
mapfile -d '' files < <(find "$CONTENT_DIR" -type f \
\( -name '*.png' -o -name '*.PNG' -o -name '*.jpeg' -o -name '*.jpg' -o -name '*.JPG' \) -print0 | sort -z)
total=${#files[@]}
echo "Found $total image(s) under $CONTENT_DIR"
[ "$DRY_RUN" -eq 1 ] && echo "(dry run — no files will be changed)"
echo
update_refs() {
# $1 old basename, $2 new basename, $3 directory to search (content root)
local old="$1" new="$2" root="$3"
grep -rlF -- "$old" "$root" --include='*.md' 2>/dev/null | while IFS= read -r mdfile; do
OLD="$old" NEW="$new" perl -i -pe 's/\Q$ENV{OLD}\E/$ENV{NEW}/g' "$mdfile"
echo " updated reference in $mdfile"
done
}
i=0
for src in "${files[@]}"; do
i=$((i+1))
dir=$(dirname "$src")
base=$(basename "$src")
stem="${base%.*}"
ext="${base##*.}"
target="$dir/$stem.jpg"
printf '[%d/%d] %s\n' "$i" "$total" "$src"
fmt=""
if [ "$ext" = "png" ] || [ "$ext" = "PNG" ]; then
w=$(identify -format '%w' "$src" 2>/dev/null)
h=$(identify -format '%h' "$src" 2>/dev/null)
size=$(stat -c%s "$src")
longedge=$(( w > h ? w : h ))
if [ "$longedge" -lt "$PNG_KEEP_MAX_DIM" ] && [ "$size" -lt "$PNG_KEEP_MAX_BYTES" ]; then
echo " small PNG (${w}x${h}, ${size} bytes) below keep-as-is threshold, leaving untouched"
skipped_png_small=$((skipped_png_small+1))
continue
fi
else
fmt=$(identify -format '%m' "$src" 2>/dev/null | head -c4)
fi
if [ "$fmt" = "JPEG" ]; then
w=$(identify -format '%w' "$src" 2>/dev/null)
h=$(identify -format '%h' "$src" 2>/dev/null)
size=$(stat -c%s "$src")
longedge=$(( w > h ? w : h ))
if [ "$longedge" -le "$MAX_DIM" ] && [ "$size" -le "$MAX_BYTES" ]; then
if [ "$target" = "$src" ]; then
echo " already compliant, skipping"
skipped_ok=$((skipped_ok+1))
else
if [ -e "$target" ]; then
echo " COLLISION: target '$target' already exists — skipping, needs manual review" >&2
skipped_collision=$((skipped_collision+1))
elif [ "$DRY_RUN" -eq 1 ]; then
echo " would rename (no re-encode needed) -> $target"
else
mv -f "$src" "$target"
update_refs "$base" "$stem.jpg" "$CONTENT_DIR"
renamed_only=$((renamed_only+1))
echo " renamed (already compliant) -> $target"
fi
fi
continue
fi
fi
if [ "$target" != "$src" ] && [ -e "$target" ]; then
echo " COLLISION: target '$target' already exists — skipping, needs manual review" >&2
skipped_collision=$((skipped_collision+1))
continue
fi
if [ "$DRY_RUN" -eq 1 ]; then
echo " would convert -> $target"
continue
fi
tmp=$(mktemp --suffix=.jpg "${dir}/.rsz-XXXXXX")
quality=85
dim=$MAX_DIM
status=0
while :; do
if ! err=$(convert "$src" -auto-orient -background white -alpha remove -alpha off \
-resize "${dim}x${dim}>" -strip -sampling-factor 4:2:0 -quality "$quality" "$tmp" 2>&1); then
echo " ERROR converting $src: $err" >&2
rm -f "$tmp"
status=-1
break
fi
size=$(stat -c%s "$tmp")
if [ "$size" -le "$MAX_BYTES" ]; then
status=1
break
fi
if [ "$quality" -gt 40 ]; then
quality=$((quality-5))
continue
fi
if [ "$dim" -gt 800 ]; then
dim=$((dim-160))
quality=70
continue
fi
status=2
break
done
if [ "$status" -eq -1 ]; then
errors=$((errors+1))
continue
fi
if [ "$status" -eq 2 ]; then
finalsize=$(stat -c%s "$tmp")
echo " WARNING: could not get under $MAX_BYTES bytes (final ${finalsize} bytes, quality=$quality, dim=$dim)" >&2
still_too_big=$((still_too_big+1))
fi
mv -f "$tmp" "$target"
converted=$((converted+1))
echo " converted -> $target ($(stat -c%s "$target") bytes)"
if [ "$target" != "$src" ]; then
rm -f "$src"
update_refs "$base" "$stem.jpg" "$CONTENT_DIR"
fi
done
echo
echo "Done."
echo " converted (re-encoded): $converted"
echo " renamed only (already compliant): $renamed_only"
echo " already compliant, untouched: $skipped_ok"
echo " small PNGs kept as-is: $skipped_png_small"
echo " skipped due to name collision: $skipped_collision"
echo " still over size limit after max compression: $still_too_big"
echo " errors: $errors"