feat(T05): Dateiablage pro Fahrzeug + File UI with thumbnails and gallery

This commit is contained in:
2026-07-17 01:38:48 +02:00
parent 149ac04dc1
commit a38d340ddc
18 changed files with 2649 additions and 69 deletions
+88
View File
@@ -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