import base64 import io import logging from typing import Optional, Tuple from PIL import Image, ImageOps logger = logging.getLogger(__name__) MAX_SIDE = 768 # sisi terpanjang setelah resize JPEG_QUALITIES = (72, 60, 50) MAX_OUTPUT_BYTES = 200_000 MAX_PIXELS = 40_000_000 # tolak gambar raksasa (decompression bomb) Image.MAX_IMAGE_PIXELS = MAX_PIXELS def compress_image_to_data_url(path: Optional[str]) -> Tuple[Optional[str], str]: """Return (data_url, error). Semua di memori; tidak ada file baru ditulis.""" if not path: return None, "" try: with Image.open(path) as src: if src.width * src.height > MAX_PIXELS: return None, "Resolusi foto terlalu besar." src.draft("RGB", (MAX_SIDE * 2, MAX_SIDE * 2)) # percepat decode JPEG img = ImageOps.exif_transpose(src) # perbaiki rotasi HP img = img.convert("RGB") img.thumbnail((MAX_SIDE, MAX_SIDE), Image.Resampling.LANCZOS) data = b"" for q in JPEG_QUALITIES: buf = io.BytesIO() img.save(buf, "JPEG", quality=q, optimize=True) # EXIF/GPS tidak ikut data = buf.getvalue() if len(data) <= MAX_OUTPUT_BYTES: break b64 = base64.b64encode(data).decode("ascii") return f"data:image/jpeg;base64,{b64}", "" except Exception as e: logger.warning(f"Image compress failed: {e}") return None, "Foto tidak dapat diproses."