image-processor-pro / optimizer.py
divakar-rajodiya
Image Processor Pro web app
6d8fa62
Raw
History Blame Contribute Delete
2.49 kB
from __future__ import annotations
import logging
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from PIL import Image
from config import Config
logger = logging.getLogger("image_processor")
class OptimizationError(Exception):
"""Raised when saving an optimized image fails."""
@dataclass
class OptimizationResult:
output_path: Path
final_width: int
final_height: int
file_size_bytes: int
elapsed_seconds: float
class Optimizer:
"""Resize (optional) and write JPEG with metadata stripped."""
def __init__(self, config: Config) -> None:
self.config = config
def optimize_and_save(self, image: Image.Image, output_path: Path) -> OptimizationResult:
start = time.perf_counter()
resized = self._maybe_resize(image)
save_kwargs: dict[str, object] = {
"format": "JPEG",
"quality": int(self.config.jpeg_quality),
"optimize": True,
"progressive": True,
}
if not self.config.strip_metadata:
exif = image.info.get("exif")
if exif:
save_kwargs["exif"] = exif
try:
output_path.parent.mkdir(parents=True, exist_ok=True)
resized.save(output_path, **save_kwargs)
except PermissionError as exc:
raise OptimizationError(f"Permission denied writing {output_path}: {exc}") from exc
except OSError as exc:
raise OptimizationError(f"Failed to save {output_path}: {exc}") from exc
elapsed = time.perf_counter() - start
return OptimizationResult(
output_path=output_path,
final_width=resized.width,
final_height=resized.height,
file_size_bytes=output_path.stat().st_size,
elapsed_seconds=elapsed,
)
def _maybe_resize(self, image: Image.Image) -> Image.Image:
max_w = self.config.max_width
max_h = self.config.max_height
if not max_w and not max_h:
return image
width, height = image.size
scale = 1.0
if max_w and width > max_w:
scale = min(scale, max_w / width)
if max_h and height > max_h:
scale = min(scale, max_h / height)
if scale >= 1.0:
return image
new_size = (max(1, int(width * scale)), max(1, int(height * scale)))
return image.resize(new_size, Image.Resampling.LANCZOS)