Spaces:
Sleeping
Sleeping
| """Image resize, format conversion, and save operations.""" | |
| from __future__ import annotations | |
| import os | |
| from PIL import Image, ImageFile, ImageSequence | |
| from image_resizer.config import ( | |
| SAVE_QUALITY, | |
| WHITE_BG_BORDER_PX, | |
| WHITE_BG_MIN_ALPHA, | |
| WHITE_BG_MIN_RATIO, | |
| WHITE_BG_THRESHOLD, | |
| ) | |
| ImageFile.LOAD_TRUNCATED_IMAGES = True | |
| def resize_with_padding( | |
| img: Image.Image, | |
| target_w: int, | |
| target_h: int, | |
| fill: tuple[int, int, int] = (255, 255, 255), | |
| ) -> Image.Image: | |
| """Resize image to fit within target dimensions with letterboxing.""" | |
| img.thumbnail((target_w, target_h), Image.LANCZOS) | |
| bg = Image.new("RGB", (target_w, target_h), fill) | |
| x = (target_w - img.width) // 2 | |
| y = (target_h - img.height) // 2 | |
| bg.paste(img, (x, y)) | |
| return bg | |
| def pad_without_resize( | |
| img: Image.Image, | |
| target_w: int, | |
| target_h: int, | |
| fill: tuple[int, int, int] = (255, 255, 255), | |
| ) -> Image.Image: | |
| """Expand canvas to at least target size; never scale or stretch the image.""" | |
| canvas_w = max(img.width, target_w) | |
| canvas_h = max(img.height, target_h) | |
| if canvas_w == img.width and canvas_h == img.height: | |
| return img | |
| mode = "RGBA" if "A" in img.getbands() else "RGB" | |
| fill_color: tuple = (*fill, 255) if mode == "RGBA" else fill | |
| bg = Image.new(mode, (canvas_w, canvas_h), fill_color) | |
| x = (canvas_w - img.width) // 2 | |
| y = (canvas_h - img.height) // 2 | |
| if img.mode != mode: | |
| img = img.convert(mode) | |
| bg.paste(img, (x, y), img if mode == "RGBA" and "A" in img.getbands() else None) | |
| return bg | |
| def _format_and_extension(fmt: str) -> tuple[str, str]: | |
| fmt_upper = fmt.upper() | |
| save_fmt = "JPEG" if fmt_upper in ("JPEG", "JPG") else fmt_upper | |
| ext = "jpg" if fmt_upper in ("JPEG", "JPG") else fmt.lower() | |
| return save_fmt, ext | |
| def has_white_background(img: Image.Image) -> bool: | |
| """Return True if the border is mostly white or transparent (no background).""" | |
| img = img.convert("RGBA") | |
| width, height = img.size | |
| if width == 0 or height == 0: | |
| return False | |
| border = min(WHITE_BG_BORDER_PX, width // 2, height // 2) | |
| pixels: list[tuple[int, int, int, int]] = [] | |
| if border == 0: | |
| pixels = list(img.getdata()) | |
| else: | |
| for y in range(height): | |
| for x in range(width): | |
| if x < border or x >= width - border or y < border or y >= height - border: | |
| pixels.append(img.getpixel((x, y))) | |
| if not pixels: | |
| return False | |
| ok_count = 0 | |
| for r, g, b, a in pixels: | |
| # Transparent / cutout ("no background") counts as OK for MAIN checks. | |
| if a < WHITE_BG_MIN_ALPHA: | |
| ok_count += 1 | |
| continue | |
| if ( | |
| r >= WHITE_BG_THRESHOLD | |
| and g >= WHITE_BG_THRESHOLD | |
| and b >= WHITE_BG_THRESHOLD | |
| ): | |
| ok_count += 1 | |
| return ok_count / len(pixels) >= WHITE_BG_MIN_RATIO | |
| def open_image_frames(path: str) -> list[Image.Image]: | |
| """Open an image file and return a list of RGBA frames (one per GIF frame).""" | |
| img = Image.open(path) | |
| if getattr(img, "is_animated", False): | |
| img.seek(0) | |
| return [frame.convert("RGBA").copy() for frame in ImageSequence.Iterator(img)] | |
| return [img.convert("RGBA")] | |
| def save_processed_image( | |
| img: Image.Image, | |
| name: str, | |
| fmt: str, | |
| out_dir: str, | |
| width: int = None, | |
| height: int = None, | |
| frame_index: int = None, | |
| total_frames: int = 1, | |
| pad_only: bool = False, | |
| ) -> str: | |
| """Composite, optionally resize or pad, format-convert, and save a frame.""" | |
| img = img.convert("RGBA") | |
| bg = Image.new("RGBA", img.size, (255, 255, 255, 255)) | |
| img = Image.alpha_composite(bg, img) | |
| save_fmt, ext = _format_and_extension(fmt) | |
| if save_fmt == "JPEG": | |
| img = img.convert("RGB") | |
| if width is not None and height is not None: | |
| if pad_only: | |
| img = pad_without_resize(img, width, height) | |
| else: | |
| img = resize_with_padding(img, width, height) | |
| if total_frames > 1 and frame_index is not None: | |
| fname = f"{name}_frame{frame_index + 1}.{ext}" | |
| else: | |
| fname = f"{name}.{ext}" | |
| out_path = os.path.join(out_dir, fname) | |
| save_kwargs = {"format": save_fmt} | |
| if save_fmt == "JPEG": | |
| save_kwargs.update(quality=SAVE_QUALITY, subsampling=0, optimize=True) | |
| elif save_fmt in ("WEBP", "AVIF"): | |
| save_kwargs["quality"] = SAVE_QUALITY | |
| img.save(out_path, **save_kwargs) | |
| return out_path | |
| # Backward-compatible alias | |
| def save_resized_image( | |
| img: Image.Image, | |
| name: str, | |
| fmt: str, | |
| width: int, | |
| height: int, | |
| out_dir: str, | |
| frame_index: int = None, | |
| total_frames: int = 1, | |
| ) -> str: | |
| return save_processed_image( | |
| img, name, fmt, out_dir, | |
| width=width, height=height, | |
| frame_index=frame_index, total_frames=total_frames, | |
| ) | |