diff --git a/Website/scripts/resize-and-convert-images.sh b/Website/scripts/resize-and-convert-images.sh new file mode 100755 index 0000000..d5efb23 --- /dev/null +++ b/Website/scripts/resize-and-convert-images.sh @@ -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"