Skip to content

Commit 0245101

Browse files
committed
Add script to compress images to WebP format and update references
1 parent 02e75d4 commit 0245101

1 file changed

Lines changed: 168 additions & 0 deletions

File tree

scripts/optimize_all_images.sh

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Compress every supported image under docs/ to WebP, rewrite references to the
4+
# new filename across docs/ and mkdocs.yml, and remove the original file.
5+
#
6+
# Safe to re-run: images whose .webp sibling already exists are skipped.
7+
#
8+
# Usage:
9+
# scripts/optimize_all_images.sh # convert everything under docs/
10+
# scripts/optimize_all_images.sh --dry-run # show what would happen, no changes
11+
12+
set -euo pipefail
13+
14+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
15+
cd "$REPO_ROOT"
16+
17+
SCAN_DIR="docs"
18+
REFERENCE_ROOTS=("docs" "mkdocs.yml")
19+
20+
# Portable byte size lookup (BSD stat on macOS, GNU stat on Linux, wc -c fallback).
21+
# Always emits only digits so it is safe to use in arithmetic contexts.
22+
filesize() {
23+
local sz
24+
# Try BSD stat (macOS), then GNU stat (Linux), then wc -c. Each is tried
25+
# independently so stdout from a partially-failing earlier call cannot be
26+
# concatenated with the next. Only a clean digits-only result is returned.
27+
if sz=$(stat -f %z "$1" 2>/dev/null) && [[ $sz =~ ^[0-9]+$ ]]; then
28+
printf '%s' "$sz"; return
29+
fi
30+
if sz=$(stat -c %s "$1" 2>/dev/null) && [[ $sz =~ ^[0-9]+$ ]]; then
31+
printf '%s' "$sz"; return
32+
fi
33+
sz=$({ wc -c <"$1" | tr -dc '0-9'; } 2>/dev/null)
34+
[[ -n $sz ]] && printf '%s' "$sz" || printf '0'
35+
}
36+
37+
# Format a byte count as a human-readable string (e.g. "1.23 MB").
38+
human_bytes() {
39+
awk -v b="$1" 'BEGIN {
40+
split("B KB MB GB TB", u)
41+
i = 1
42+
while (b >= 1024 && i < 5) { b /= 1024; i++ }
43+
if (i == 1) printf "%d %s", b, u[i]
44+
else printf "%.2f %s", b, u[i]
45+
}'
46+
}
47+
48+
DRY_RUN=0
49+
if [[ "${1:-}" == "--dry-run" ]]; then
50+
DRY_RUN=1
51+
echo "DRY RUN — no files will be changed"
52+
echo
53+
fi
54+
55+
# Collect every candidate image (null-delimited so names with spaces survive).
56+
IMAGES=()
57+
while IFS= read -r -d '' path; do
58+
IMAGES+=("$path")
59+
done < <(find "$SCAN_DIR" -type f \( \
60+
-iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \
61+
-o -iname '*.gif' -o -iname '*.bmp' -o -iname '*.tiff' -o -iname '*.tif' \
62+
\) -print0)
63+
64+
total=${#IMAGES[@]}
65+
if [[ $total -eq 0 ]]; then
66+
echo "No images found under $SCAN_DIR"
67+
exit 0
68+
fi
69+
70+
echo "Found $total image(s) under $SCAN_DIR"
71+
echo
72+
73+
converted=0
74+
skipped=0
75+
failed=0
76+
index=0
77+
total_original_bytes=0
78+
total_webp_bytes=0
79+
80+
for image in "${IMAGES[@]}"; do
81+
index=$((index + 1))
82+
dir="$(dirname "$image")"
83+
base="$(basename "$image")"
84+
stem="${base%.*}"
85+
webp_base="$stem.webp"
86+
webp_path="$dir/$webp_base"
87+
88+
echo "[$index/$total] $image"
89+
90+
if [[ -e "$webp_path" ]]; then
91+
echo " skip: $webp_path already exists"
92+
skipped=$((skipped + 1))
93+
echo
94+
continue
95+
fi
96+
97+
if [[ $DRY_RUN -eq 1 ]]; then
98+
echo " would convert → $webp_path"
99+
echo " would rewrite references: $base$webp_base"
100+
echo " would remove: $image"
101+
converted=$((converted + 1))
102+
echo
103+
continue
104+
fi
105+
106+
if ! python3 scripts/optimize_image.py "$image"; then
107+
echo " FAILED: optimize_image.py exited non-zero, leaving original in place"
108+
failed=$((failed + 1))
109+
echo
110+
continue
111+
fi
112+
113+
if [[ ! -f "$webp_path" ]]; then
114+
echo " FAILED: expected $webp_path was not produced, leaving original in place"
115+
failed=$((failed + 1))
116+
echo
117+
continue
118+
fi
119+
120+
orig_bytes=$(filesize "$image")
121+
new_bytes=$(filesize "$webp_path")
122+
orig_bytes=${orig_bytes:-0}
123+
new_bytes=${new_bytes:-0}
124+
# 10# forces base-10 so a leading zero is never parsed as octal.
125+
total_original_bytes=$((total_original_bytes + 10#$orig_bytes))
126+
total_webp_bytes=$((total_webp_bytes + 10#$new_bytes))
127+
pct=$(awk -v o="$orig_bytes" -v n="$new_bytes" 'BEGIN {
128+
if (o == 0) { printf "0.0"; exit }
129+
printf "%+.1f", (n/o - 1) * 100
130+
}')
131+
echo " size: $(human_bytes "$orig_bytes")$(human_bytes "$new_bytes") (${pct}%)"
132+
133+
# Rewrite references by basename across docs/ and mkdocs.yml.
134+
# Filename is passed via env so shell/regex specials in the name are safe;
135+
# \Q...\E makes perl treat the old name as a literal string, and the
136+
# lookbehind/lookahead prevents matches inside other filenames (e.g. we
137+
# must not rewrite "a.png" when it appears inside "aa.png" or "a.png.bak").
138+
ref_count=0
139+
while IFS= read -r -d '' refpath; do
140+
OLD_NAME="$base" NEW_NAME="$webp_base" perl -i -pe '
141+
s/(?<![A-Za-z0-9_.\-])\Q$ENV{OLD_NAME}\E(?![A-Za-z0-9_.\-])/$ENV{NEW_NAME}/g
142+
' "$refpath"
143+
ref_count=$((ref_count + 1))
144+
done < <(grep -rlI --null --fixed-strings -- "$base" "${REFERENCE_ROOTS[@]}" 2>/dev/null || true)
145+
146+
rm -- "$image"
147+
148+
if [[ $ref_count -gt 0 ]]; then
149+
echo " updated $ref_count file(s) referencing $base"
150+
else
151+
echo " no references to $base found"
152+
fi
153+
echo " removed original"
154+
converted=$((converted + 1))
155+
echo
156+
done
157+
158+
echo "Summary: $converted converted, $skipped skipped, $failed failed"
159+
if [[ $converted -gt 0 && $DRY_RUN -eq 0 ]]; then
160+
saved_bytes=$((total_original_bytes - total_webp_bytes))
161+
total_pct=$(awk -v o="$total_original_bytes" -v n="$total_webp_bytes" 'BEGIN {
162+
if (o == 0) { printf "0.0"; exit }
163+
printf "%+.1f", (n/o - 1) * 100
164+
}')
165+
echo "Total size: $(human_bytes "$total_original_bytes")$(human_bytes "$total_webp_bytes") (${total_pct}%)"
166+
echo "Saved: $(human_bytes "$saved_bytes")"
167+
fi
168+
[[ $failed -eq 0 ]]

0 commit comments

Comments
 (0)