feat(T05): Dateiablage pro Fahrzeug + File UI with thumbnails and gallery
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user