"""Thumbnail generation for DOM.РФ photos. Strategy: original PNG/JPG ~1-3 MB downloaded by scraper to data/raw/domrf_photos//.{png,jpg}. We additionally write a WebP cover-thumbnail at 320×240 (~10-30 KB) next to it, suffix `_thumb.webp`. The frontend grid uses /api/v1/photos//?size=thumb. Lightbox opens the original DOM.РФ URL (we don't need to mirror originals). """ from __future__ import annotations import logging from pathlib import Path from PIL import Image, ImageOps logger = logging.getLogger(__name__) THUMB_SIZE: tuple[int, int] = (320, 240) THUMB_QUALITY: int = 70 THUMB_SUFFIX: str = "_thumb.webp" def thumb_path_for(src: Path) -> Path: """Return canonical thumbnail path next to the source file.""" return src.with_name(src.stem + THUMB_SUFFIX) def make_thumbnail( src: Path, *, size: tuple[int, int] = THUMB_SIZE, quality: int = THUMB_QUALITY, overwrite: bool = False, ) -> Path | None: """Generate a WebP cover-thumbnail next to src. Returns thumb path on success. Skips if thumb already exists and overwrite=False. Returns None on any error (logged) so callers can keep going with the next file. """ if not src.exists(): return None dst = thumb_path_for(src) if dst.exists() and not overwrite: return dst try: with Image.open(src) as im: im = ImageOps.exif_transpose(im) im = im.convert("RGB") im = ImageOps.fit(im, size, method=Image.Resampling.LANCZOS) dst.parent.mkdir(parents=True, exist_ok=True) im.save(dst, format="WEBP", quality=quality, method=4) return dst except Exception as e: logger.warning("thumbnail %s failed: %s", src, e) return None