Update to v1.1 website code from kottgard-production-v1.1.zip
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compress site images to <= 100 KB while preserving original colors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SITE = ROOT / "public" / "images" / "site"
|
||||
BACKUP = SITE / "_originals"
|
||||
SKIP = {"logo.jpeg", "README.txt"}
|
||||
SKIP_PATTERNS = (" copy", "-1.jpg")
|
||||
|
||||
|
||||
def should_skip(path: Path) -> bool:
|
||||
if path.name in SKIP or path.name.startswith("_"):
|
||||
return True
|
||||
return any(p in path.name for p in SKIP_PATTERNS)
|
||||
|
||||
|
||||
def encode_jpeg(img: Image.Image, quality: int) -> bytes:
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=quality, optimize=True, subsampling=2)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def compress_to_target(img: Image.Image, max_bytes: int) -> tuple[Image.Image, int]:
|
||||
working = img.convert("RGB")
|
||||
scale = 1.0
|
||||
|
||||
while scale >= 0.35:
|
||||
if scale < 1.0:
|
||||
w, h = working.size
|
||||
nw, nh = max(1, int(w * scale)), max(1, int(h * scale))
|
||||
candidate = working.resize((nw, nh), Image.Resampling.LANCZOS)
|
||||
else:
|
||||
candidate = working
|
||||
|
||||
lo, hi = 20, 92
|
||||
best_q = lo
|
||||
best_data = encode_jpeg(candidate, lo)
|
||||
|
||||
while lo <= hi:
|
||||
mid = (lo + hi) // 2
|
||||
data = encode_jpeg(candidate, mid)
|
||||
if len(data) <= max_bytes:
|
||||
best_q = mid
|
||||
best_data = data
|
||||
lo = mid + 1
|
||||
else:
|
||||
hi = mid - 1
|
||||
|
||||
if len(best_data) <= max_bytes:
|
||||
return Image.open(io.BytesIO(best_data)).convert("RGB"), best_q
|
||||
|
||||
scale *= 0.88
|
||||
|
||||
data = encode_jpeg(candidate, 20)
|
||||
return Image.open(io.BytesIO(data)).convert("RGB"), 20
|
||||
|
||||
|
||||
def optimize_file(path: Path, max_bytes: int, dry_run: bool) -> tuple[int, int]:
|
||||
before = path.stat().st_size
|
||||
if before <= max_bytes:
|
||||
return before, before
|
||||
|
||||
with Image.open(path) as img:
|
||||
out, quality = compress_to_target(img, max_bytes)
|
||||
|
||||
if dry_run:
|
||||
buf = io.BytesIO()
|
||||
out.save(buf, format="JPEG", quality=quality, optimize=True, subsampling=2)
|
||||
return before, len(buf.getvalue())
|
||||
|
||||
out.save(path, format="JPEG", quality=quality, optimize=True, subsampling=2)
|
||||
return before, path.stat().st_size
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Optimize site images to <= 100 KB")
|
||||
parser.add_argument("--max-kb", type=int, default=100)
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
max_bytes = args.max_kb * 1024
|
||||
BACKUP.mkdir(exist_ok=True)
|
||||
|
||||
files = sorted(SITE.glob("*.jpg")) + sorted(SITE.glob("*.jpeg"))
|
||||
changed = 0
|
||||
|
||||
for path in files:
|
||||
if should_skip(path):
|
||||
continue
|
||||
|
||||
backup = BACKUP / path.name
|
||||
if not backup.exists() and not args.dry_run:
|
||||
shutil.copy2(path, backup)
|
||||
|
||||
before, after = optimize_file(path, max_bytes, args.dry_run)
|
||||
if after < before or before > max_bytes:
|
||||
changed += 1
|
||||
print(
|
||||
f"{'~' if args.dry_run else '✓'} {path.name}: "
|
||||
f"{before // 1024} KB → {after // 1024} KB"
|
||||
)
|
||||
else:
|
||||
print(f"○ {path.name}: {before // 1024} KB (ok)")
|
||||
|
||||
print(f"\nDone — {changed} optimized, originals in {BACKUP}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user