from __future__ import annotations import io from dataclasses import dataclass from PIL import Image, ImageOps from app.config import get_settings @dataclass class PreparedImage: jpeg_bytes: bytes width: int height: int original_bytes: int compressed_bytes: int mime: str = "image/jpeg" def to_meta(self) -> dict[str, int | str]: return { "mime": self.mime, "width": self.width, "height": self.height, "original_bytes": self.original_bytes, "compressed_bytes": self.compressed_bytes, } def prepare_image(raw_bytes: bytes) -> PreparedImage: settings = get_settings() max_edge = max(256, int(settings.vision_max_edge_px)) quality = max(40, min(95, int(settings.vision_jpeg_quality))) with Image.open(io.BytesIO(raw_bytes)) as img: img = ImageOps.exif_transpose(img) img = img.convert("RGB") width, height = img.size if max(width, height) > max_edge: img.thumbnail((max_edge, max_edge), Image.Resampling.LANCZOS) width, height = img.size buffer = io.BytesIO() img.save(buffer, format="JPEG", quality=quality, optimize=True) jpeg_bytes = buffer.getvalue() return PreparedImage( jpeg_bytes=jpeg_bytes, width=width, height=height, original_bytes=len(raw_bytes), compressed_bytes=len(jpeg_bytes), )