release nouri 0.6.0 polish backup and pwa
This commit is contained in:
+163
@@ -0,0 +1,163 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageOps, UnidentifiedImageError
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
|
||||
ALLOWED_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp"}
|
||||
IMAGE_VARIANTS = {
|
||||
"sm": {"width": 320, "quality": 76},
|
||||
"md": {"width": 720, "quality": 82},
|
||||
"lg": {"width": 1280, "quality": 86},
|
||||
}
|
||||
DEFAULT_RENDERED_FORMAT = "webp"
|
||||
ORIGINAL_MAX_WIDTH = 1600
|
||||
|
||||
|
||||
def allowed_image_file(filename: str) -> bool:
|
||||
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_IMAGE_EXTENSIONS
|
||||
|
||||
|
||||
def ensure_upload_structure(upload_folder: str | Path) -> None:
|
||||
upload_root = Path(upload_folder)
|
||||
upload_root.mkdir(parents=True, exist_ok=True)
|
||||
(upload_root / "variants").mkdir(parents=True, exist_ok=True)
|
||||
for variant_name in IMAGE_VARIANTS:
|
||||
(upload_root / "variants" / variant_name).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def build_variant_filename(filename: str, variant_name: str) -> str:
|
||||
source = Path(filename)
|
||||
return f"{source.stem}__{variant_name}.webp"
|
||||
|
||||
|
||||
def build_variant_relative_path(filename: str, variant_name: str) -> str:
|
||||
return f"variants/{variant_name}/{build_variant_filename(filename, variant_name)}"
|
||||
|
||||
|
||||
def remove_photo_assets(upload_folder: str | Path, filename: str | None) -> None:
|
||||
if not filename:
|
||||
return
|
||||
upload_root = Path(upload_folder)
|
||||
original_path = upload_root / filename
|
||||
if original_path.exists():
|
||||
original_path.unlink()
|
||||
for variant_name in IMAGE_VARIANTS:
|
||||
variant_path = upload_root / build_variant_relative_path(filename, variant_name)
|
||||
if variant_path.exists():
|
||||
variant_path.unlink()
|
||||
|
||||
|
||||
def _open_image(upload: FileStorage) -> Image.Image:
|
||||
upload.stream.seek(0)
|
||||
image = Image.open(upload.stream)
|
||||
image.load()
|
||||
return ImageOps.exif_transpose(image)
|
||||
|
||||
|
||||
def _prepare_image(image: Image.Image) -> Image.Image:
|
||||
if image.mode not in {"RGB", "RGBA"}:
|
||||
image = image.convert("RGBA" if "A" in image.getbands() else "RGB")
|
||||
return image
|
||||
|
||||
|
||||
def _resize_copy(image: Image.Image, width: int) -> Image.Image:
|
||||
resized = image.copy()
|
||||
resized.thumbnail((width, width * 3), Image.Resampling.LANCZOS)
|
||||
return resized
|
||||
|
||||
|
||||
def _save_image(image: Image.Image, destination: Path, quality: int) -> None:
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
image.save(
|
||||
destination,
|
||||
format=DEFAULT_RENDERED_FORMAT.upper(),
|
||||
quality=quality,
|
||||
method=6,
|
||||
)
|
||||
|
||||
|
||||
def save_photo_with_variants(
|
||||
upload: FileStorage | None,
|
||||
upload_folder: str | Path,
|
||||
current_filename: str | None = None,
|
||||
) -> str | None:
|
||||
if not upload or not upload.filename:
|
||||
return current_filename
|
||||
|
||||
if not allowed_image_file(upload.filename):
|
||||
raise ValueError("Bitte ein Bild als PNG, JPG, GIF oder WEBP hochladen.")
|
||||
|
||||
ensure_upload_structure(upload_folder)
|
||||
|
||||
original_name = secure_filename(upload.filename)
|
||||
extension = original_name.rsplit(".", 1)[1].lower()
|
||||
|
||||
try:
|
||||
image = _prepare_image(_open_image(upload))
|
||||
filename = f"{uuid.uuid4().hex}.webp"
|
||||
original_path = Path(upload_folder) / filename
|
||||
optimized = _resize_copy(image, ORIGINAL_MAX_WIDTH)
|
||||
_save_image(optimized, original_path, quality=88)
|
||||
for variant_name, config in IMAGE_VARIANTS.items():
|
||||
variant_image = _resize_copy(image, int(config["width"]))
|
||||
variant_path = Path(upload_folder) / build_variant_relative_path(filename, variant_name)
|
||||
_save_image(variant_image, variant_path, quality=int(config["quality"]))
|
||||
except (UnidentifiedImageError, OSError, ValueError):
|
||||
filename = f"{uuid.uuid4().hex}.{extension}"
|
||||
original_path = Path(upload_folder) / filename
|
||||
upload.stream.seek(0)
|
||||
upload.save(original_path)
|
||||
|
||||
if current_filename and current_filename != filename:
|
||||
remove_photo_assets(upload_folder, current_filename)
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def image_url(filename: str | None, url_builder, variant: str = "md", upload_folder: str | Path | None = None) -> str | None:
|
||||
if not filename:
|
||||
return None
|
||||
if variant in IMAGE_VARIANTS:
|
||||
if upload_folder is not None:
|
||||
variant_path = Path(upload_folder) / build_variant_relative_path(filename, variant)
|
||||
if variant_path.exists():
|
||||
return url_builder("uploaded_file", filename=build_variant_relative_path(filename, variant))
|
||||
else:
|
||||
return url_builder("uploaded_file", filename=build_variant_relative_path(filename, variant))
|
||||
return url_builder("uploaded_file", filename=filename)
|
||||
|
||||
|
||||
def image_srcset(filename: str | None, url_builder, upload_folder: str | Path | None = None) -> str:
|
||||
if not filename:
|
||||
return ""
|
||||
parts = []
|
||||
for variant_name, config in IMAGE_VARIANTS.items():
|
||||
variant_url = image_url(filename, url_builder, variant_name, upload_folder=upload_folder)
|
||||
if variant_url and (not upload_folder or variant_url != url_builder("uploaded_file", filename=filename)):
|
||||
parts.append(f"{variant_url} {config['width']}w")
|
||||
original_width = max(config["width"] for config in IMAGE_VARIANTS.values()) + 320
|
||||
parts.append(f"{url_builder('uploaded_file', filename=filename)} {original_width}w")
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
def image_sizes(card: str = "grid") -> str:
|
||||
if card == "detail":
|
||||
return "(max-width: 720px) 100vw, 720px"
|
||||
return "(max-width: 720px) 42vw, (max-width: 1080px) 28vw, 180px"
|
||||
|
||||
|
||||
def upload_file_size_ok(upload: FileStorage | None, max_bytes: int) -> bool:
|
||||
if not upload or not upload.filename:
|
||||
return True
|
||||
stream = upload.stream
|
||||
current_position = stream.tell()
|
||||
stream.seek(0, os.SEEK_END)
|
||||
size = stream.tell()
|
||||
stream.seek(current_position)
|
||||
return size <= max_bytes
|
||||
Reference in New Issue
Block a user