All checks were successful
Build Release / create-release (push) Successful in 1s
Build Release / build-docker (push) Successful in 42s
Lint & Test / test (push) Successful in 2m50s
Build Release / build-windows (push) Successful in 3m27s
Build Release / build-linux (push) Successful in 1m59s
- Create utils/image_codec.py with cv2-based image helpers - Replace PIL usage across all routes, filters, and engines with cv2 - Move Pillow from core deps to [tray] optional in pyproject.toml - Extract shared build logic into build-common.sh (detect_version, cleanup, etc.) - Strip unused NumPy/PIL/zeroconf/debug files in build scripts
92 lines
2.9 KiB
Python
92 lines
2.9 KiB
Python
"""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]
|