93 lines
3.0 KiB
Python
93 lines
3.0 KiB
Python
"""Thumbnail generation utilities using Pillow.
|
|
|
|
Generates 200x200 thumbnails for image files (jpg, png, webp).
|
|
Non-image files return None (no thumbnail).
|
|
"""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from PIL import Image, ImageOps
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
THUMBNAIL_SIZE = (200, 200)
|
|
IMAGE_MIME_TYPES = {
|
|
"image/jpeg",
|
|
"image/png",
|
|
"image/webp",
|
|
}
|
|
IMAGE_EXTENSIONS = {"jpg", "jpeg", "png", "webp"}
|
|
|
|
|
|
def is_image_mime_type(mime_type: str) -> bool:
|
|
"""Check if the given MIME type is a supported image type."""
|
|
return mime_type.lower() in IMAGE_MIME_TYPES
|
|
|
|
|
|
def generate_thumbnail(
|
|
source_path: str | Path,
|
|
thumbnail_dir: str | Path,
|
|
stored_filename: str,
|
|
) -> Optional[str]:
|
|
"""Generate a 200x200 thumbnail for an image file.
|
|
|
|
Args:
|
|
source_path: Path to the original image file.
|
|
thumbnail_dir: Directory where thumbnails are stored.
|
|
stored_filename: The stored filename of the original file.
|
|
|
|
Returns:
|
|
Relative path to the thumbnail file, or None if the file is not
|
|
a supported image or thumbnail generation fails.
|
|
"""
|
|
source_path = Path(source_path)
|
|
thumbnail_dir = Path(thumbnail_dir)
|
|
|
|
# Check if the file extension is a supported image type
|
|
ext = source_path.suffix.lower().lstrip(".")
|
|
if ext not in IMAGE_EXTENSIONS:
|
|
return None
|
|
|
|
try:
|
|
thumbnail_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
thumbnail_name = f"thumb_{stored_filename}"
|
|
# Use .jpg for thumbnails to save space
|
|
if ext in ("jpg", "jpeg"):
|
|
thumbnail_name = f"thumb_{Path(stored_filename).stem}.jpg"
|
|
elif ext == "png":
|
|
thumbnail_name = f"thumb_{Path(stored_filename).stem}.png"
|
|
elif ext == "webp":
|
|
thumbnail_name = f"thumb_{Path(stored_filename).stem}.webp"
|
|
|
|
thumbnail_path = thumbnail_dir / thumbnail_name
|
|
|
|
with Image.open(source_path) as img:
|
|
# Convert to RGB if necessary (e.g., for RGBA/P mode images)
|
|
if img.mode in ("RGBA", "P", "LA"):
|
|
# Create a white background for transparency
|
|
background = Image.new("RGB", img.size, (255, 255, 255))
|
|
if img.mode == "P":
|
|
img = img.convert("RGBA")
|
|
background.paste(
|
|
img, mask=img.split()[-1] if img.mode in ("RGBA", "LA") else None
|
|
)
|
|
img = background
|
|
elif img.mode != "RGB":
|
|
img = img.convert("RGB")
|
|
|
|
# Use ImageOps.fit for a centered crop to exact thumbnail size
|
|
thumbnail = ImageOps.fit(
|
|
img, THUMBNAIL_SIZE, method=Image.Resampling.LANCZOS
|
|
)
|
|
thumbnail.save(thumbnail_path, quality=85, optimize=True)
|
|
|
|
logger.info("Thumbnail generated: %s", thumbnail_path)
|
|
return str(thumbnail_path)
|
|
|
|
except Exception as exc:
|
|
logger.error("Failed to generate thumbnail for %s: %s", source_path, exc)
|
|
return None
|