"""Image encoding/decoding/resizing utilities using OpenCV. Replaces PIL/Pillow for JPEG encoding, image loading, and resizing operations. All functions work with numpy RGB arrays (H, W, 3) uint8. """ import base64 from pathlib import Path from typing import Tuple, Union import cv2 import numpy as np def encode_jpeg(image: np.ndarray, quality: int = 85) -> bytes: """Encode an RGB numpy array as JPEG bytes.""" bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) ok, buf = cv2.imencode(".jpg", bgr, [cv2.IMWRITE_JPEG_QUALITY, quality]) if not ok: raise RuntimeError("JPEG encoding failed") return buf.tobytes() def encode_jpeg_data_uri(image: np.ndarray, quality: int = 85) -> str: """Encode an RGB numpy array as a JPEG base64 data URI.""" raw = encode_jpeg(image, quality) b64 = base64.b64encode(raw).decode("utf-8") return f"data:image/jpeg;base64,{b64}" def resize_image(image: np.ndarray, width: int, height: int) -> np.ndarray: """Resize an image to exact dimensions. Uses INTER_AREA for downscaling (better quality, faster) and INTER_LANCZOS4 for upscaling. """ h, w = image.shape[:2] shrinking = (width * height) < (w * h) interp = cv2.INTER_AREA if shrinking else cv2.INTER_LANCZOS4 return cv2.resize(image, (width, height), interpolation=interp) def thumbnail(image: np.ndarray, max_width: int) -> np.ndarray: """Create a thumbnail that fits within max_width, preserving aspect ratio. Uses INTER_AREA (optimal for downscaling). """ h, w = image.shape[:2] if w <= max_width: return image.copy() scale = max_width / w new_w = max_width new_h = max(1, int(h * scale)) return cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA) def resize_down(image: np.ndarray, max_width: int) -> np.ndarray: """Downscale if wider than max_width; return as-is otherwise. Uses INTER_AREA (optimal for downscaling). """ h, w = image.shape[:2] if w <= max_width: return image scale = max_width / w new_w = max_width new_h = max(1, int(h * scale)) return cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA) def load_image_file(path: Union[str, Path]) -> np.ndarray: """Load an image file and return as RGB numpy array.""" path = str(path) bgr = cv2.imread(path, cv2.IMREAD_COLOR) if bgr is None: raise FileNotFoundError(f"Cannot load image: {path}") return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) def load_image_bytes(data: bytes) -> np.ndarray: """Decode image bytes (JPEG, PNG, etc.) and return as RGB numpy array.""" arr = np.frombuffer(data, dtype=np.uint8) bgr = cv2.imdecode(arr, cv2.IMREAD_COLOR) if bgr is None: raise ValueError("Cannot decode image data") return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) def image_size(image: np.ndarray) -> Tuple[int, int]: """Return (width, height) of an image array.""" return image.shape[1], image.shape[0]