Indian-Sign-Language-KairoAI / optimize_sign_assets.py
meghmodi's picture
Upload 13 files
07d2eba verified
Raw
History Blame Contribute Delete
3.7 kB
"""
Optimize bundled sign images in-place to reduce Flutter app size.
Default behavior:
- Scans assets/signs/**/image.*
- Resizes images to max 960px on the longest side (if larger)
- Converts to RGB palette PNG (256 colors) and saves optimized
- Keeps existing file names and paths unchanged
Usage:
e:/KairoAI/.venv/Scripts/python.exe optimize_sign_assets.py
e:/KairoAI/.venv/Scripts/python.exe optimize_sign_assets.py --max-size 768 --quality 80
"""
from __future__ import annotations
import argparse
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable
from PIL import Image
SUPPORTED = {".png", ".jpg", ".jpeg", ".webp"}
ASSET_ROOT = Path(__file__).parent / "assets" / "signs"
@dataclass
class FileStat:
path: Path
before: int
after: int
def iter_image_files(root: Path) -> Iterable[Path]:
if not root.exists():
return []
return sorted(
[p for p in root.rglob("image.*") if p.is_file() and p.suffix.lower() in SUPPORTED]
)
def optimize_file(path: Path, max_size: int, quality: int) -> FileStat:
before = path.stat().st_size
with Image.open(path) as img:
img = img.convert("RGB")
# Resize only when needed, preserving aspect ratio.
longest = max(img.width, img.height)
if longest > max_size:
scale = max_size / float(longest)
new_size = (max(1, int(img.width * scale)), max(1, int(img.height * scale)))
img = img.resize(new_size, Image.Resampling.LANCZOS)
# Keep original extension so existing asset paths remain valid.
ext = path.suffix.lower()
if ext == ".png":
pal = img.quantize(colors=256, method=Image.Quantize.MEDIANCUT)
pal.save(path, format="PNG", optimize=True)
elif ext in {".jpg", ".jpeg"}:
img.save(path, format="JPEG", optimize=True, quality=quality)
elif ext == ".webp":
img.save(path, format="WEBP", quality=quality, method=6)
after = path.stat().st_size
return FileStat(path=path, before=before, after=after)
def fmt_size(num_bytes: int) -> str:
mb = num_bytes / (1024 * 1024)
return f"{mb:.2f} MB"
def main() -> int:
parser = argparse.ArgumentParser(description="Optimize local sign assets in-place")
parser.add_argument("--max-size", type=int, default=960, help="Longest image side after resize")
parser.add_argument("--quality", type=int, default=82, help="JPEG/WEBP quality")
args = parser.parse_args()
files = list(iter_image_files(ASSET_ROOT))
if not files:
print(f"No supported image files found under: {ASSET_ROOT}")
return 1
stats: list[FileStat] = []
for file_path in files:
stat = optimize_file(file_path, max_size=args.max_size, quality=args.quality)
stats.append(stat)
total_before = sum(s.before for s in stats)
total_after = sum(s.after for s in stats)
saved = total_before - total_after
pct = (saved / total_before * 100.0) if total_before else 0.0
print(f"Processed: {len(stats)} file(s)")
print(f"Before: {fmt_size(total_before)}")
print(f"After: {fmt_size(total_after)}")
print(f"Saved: {fmt_size(saved)} ({pct:.1f}%)")
# Print top 5 biggest savings for quick visibility.
top = sorted(stats, key=lambda s: (s.before - s.after), reverse=True)[:5]
print("Top savings:")
for s in top:
delta = s.before - s.after
print(f"- {s.path.as_posix()}: {fmt_size(delta)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())