#!/usr/bin/env python3 """Bild-Optimierer für GeoGraSim. Bewegt Originale in `_original/`-Unterordner, generiert daneben WebP-Variante in der jeweiligen Zielbreite. Höhe wird proportional skaliert. Konvention: vorher: foo/bar.png (groß, PNG) nachher: foo/_original/bar.png (Backup, NICHT im Deploy) foo/bar.webp (deploy-ready, klein) Aufruf: python App/scripts/optimize-images.py cards python App/scripts/optimize-images.py busfahrt python App/scripts/optimize-images.py glossar python App/scripts/optimize-images.py splash python App/scripts/optimize-images.py avatars python App/scripts/optimize-images.py all Dry-run (nur listen, nichts machen): DRY=1 python App/scripts/optimize-images.py cards Skip already done (default: skip wenn .webp neuer als Original): FORCE=1 python ... cards # erzwingt Neu-Erstellung """ import os, sys, shutil, glob from PIL import Image REPO = 'C:/xampp/htdocs/geograsim/App' DRY = os.environ.get('DRY') == '1' FORCE = os.environ.get('FORCE') == '1' # Job-Definitionen: (label, glob-pattern, target-width-px, webp-quality) JOBS = { 'cards': [(f'{REPO}/assets/img/card-*.png', 800, 85)], 'busfahrt': [(f'{REPO}/sims/busfahrt/assets/cities/*.png', 600, 82)], 'logistik': [(f'{REPO}/sims/logistik/assets/cities/*.png', 600, 82)], 'glossar': [(f'{REPO}/assets/img/glossar/*.png', 1600, 85)], 'splash': [(f'{REPO}/sims/*/assets/splash-*.png', 1920, 85), (f'{REPO}/sims/*/assets/splash/*.png', 1920, 85)], 'avatars': [(f'{REPO}/assets/img/avatars/avatar-*.png', 400, 85)], 'heli': [(f'{REPO}/sims/heli/assets/cities/*.png', 800, 82), (f'{REPO}/sims/heli/assets/cities-panorama/*.png', 1920, 82), (f'{REPO}/sims/heli/assets/missions/*.png', 1024, 82), (f'{REPO}/sims/heli/assets/helis-new/*.png', 600, 85), (f'{REPO}/sims/heli/assets/heli-*.png', 600, 85)], } def process_file(src, target_w, quality): src_dir = os.path.dirname(src) name = os.path.basename(src) name_noext = os.path.splitext(name)[0] orig_dir = os.path.join(src_dir, '_original') orig_dest = os.path.join(orig_dir, name) webp_dest = os.path.join(src_dir, name_noext + '.webp') # Wenn schon migriert (Original liegt schon im _original/) -> Skip if os.path.exists(orig_dest) and os.path.exists(webp_dest) and not FORCE: return 'skip', os.path.getsize(webp_dest) if DRY: print(f' [DRY] would convert: {os.path.relpath(src, REPO)} -> .webp ({target_w}px)') return 'dry', 0 # Original in _original/ verschieben os.makedirs(orig_dir, exist_ok=True) if not os.path.exists(orig_dest): shutil.move(src, orig_dest) elif os.path.exists(src): os.remove(src) # Duplikat — Original ist schon gesichert # WebP generieren img = Image.open(orig_dest) if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info): # Alpha-Kanal behalten (WebP unterstützt das) pass elif img.mode != 'RGB': img = img.convert('RGB') w, h = img.size if w > target_w: new_h = int(h * target_w / w) img = img.resize((target_w, new_h), Image.LANCZOS) img.save(webp_dest, 'WebP', quality=quality, method=6) return 'ok', os.path.getsize(webp_dest) def run_job(label, jobs): print(f'\n=== {label.upper()} ===') total_files = 0 total_saved = 0 total_webp_size = 0 for pattern, target_w, quality in jobs: files = sorted(glob.glob(pattern)) if not files: print(f' (kein Match für {pattern})') continue for src in files: try: orig_size = os.path.getsize(src) except OSError: # Original schon verschoben -> check _original/ orig_path = os.path.join(os.path.dirname(src), '_original', os.path.basename(src)) orig_size = os.path.getsize(orig_path) if os.path.exists(orig_path) else 0 try: status, webp_size = process_file(src, target_w, quality) except Exception as e: print(f' FAIL {os.path.relpath(src, REPO).replace(chr(92),"/"):60s} {type(e).__name__}: {str(e)[:80]}') continue saved = orig_size - webp_size if status == 'ok' else 0 total_files += 1 total_saved += saved total_webp_size += webp_size short = os.path.relpath(src, REPO).replace('\\', '/') if status in ('ok', 'skip'): print(f' {status:4s} {short:60s} {webp_size//1024:5d}K') print(f' --- {len(files)} files | total WebP: {total_webp_size//1024//1024} MB | freed: {total_saved//1024//1024} MB ---') def main(): targets = sys.argv[1:] or ['cards'] if 'all' in targets: targets = list(JOBS.keys()) for t in targets: if t not in JOBS: print(f'? unbekannt: {t}. Optionen: {list(JOBS.keys())} oder all') continue run_job(t, JOBS[t]) if __name__ == '__main__': main()