"""Image utilities for Indic Heritage Studio v2.""" from __future__ import annotations from pathlib import Path from typing import Tuple, Union import numpy as np from PIL import Image def load_image(path: Union[str, Path]) -> Image.Image: """Load an image as RGB PIL.""" img = Image.open(path) if img.mode != "RGB": img = img.convert("RGB") return img def resize_to_sdxl(image: Image.Image, target: int = 1024) -> Image.Image: """Resize so the longer edge = target, preserve aspect ratio.""" w, h = image.size if w >= h: new_w, new_h = target, int(h * target / w) else: new_w, new_h = int(w * target / h), target return image.resize((new_w, new_h), Image.LANCZOS) def center_crop_to(image: Image.Image, size: Tuple[int, int]) -> Image.Image: """Center-crop an image to exact dimensions.""" w, h = image.size tw, th = size left = (w - tw) // 2 top = (h - th) // 2 return image.crop((left, top, left + tw, top + th)) def image_to_numpy(image: Image.Image) -> np.ndarray: return np.array(image.convert("RGB")) def numpy_to_image(arr: np.ndarray) -> Image.Image: return Image.fromarray(arr.astype(np.uint8)) def save_grid(images, path: Union[str, Path], cols: int = 4, cell_size: int = 256) -> None: """Save a list of images as a grid PNG.""" from PIL import Image as PILImage n = len(images) rows = (n + cols - 1) // cols grid = PILImage.new("RGB", (cols * cell_size, rows * cell_size), color="white") for i, img in enumerate(images): r, c = i // cols, i % cols img_resized = img.resize((cell_size, cell_size), PILImage.LANCZOS) grid.paste(img_resized, (c * cell_size, r * cell_size)) Path(path).parent.mkdir(parents=True, exist_ok=True) grid.save(path)