From 60a417dc643df753d20de6bd4b0745672fc80c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= Date: Sun, 30 Nov 2025 14:19:05 +0100 Subject: [PATCH] API: use `ImageData` type --- api/advent22_api/core/helpers.py | 44 ++++++++++++++++++++++++++------ api/advent22_api/routers/user.py | 36 +++++++++++++------------- 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/api/advent22_api/core/helpers.py b/api/advent22_api/core/helpers.py index b75e31c..bbf7260 100644 --- a/api/advent22_api/core/helpers.py +++ b/api/advent22_api/core/helpers.py @@ -1,3 +1,4 @@ +import base64 import itertools import random import re @@ -5,9 +6,9 @@ from datetime import date, datetime, timedelta from io import BytesIO from typing import Any, Awaitable, Callable, Iterable, Self, Sequence, TypeVar -from fastapi.responses import StreamingResponse from PIL import Image as PILImage from PIL.Image import Image, Resampling +from pydantic import BaseModel from .config import get_config from .dav.webdav import WebDAV @@ -115,25 +116,51 @@ async def load_image(file_name: str) -> Image: return PILImage.open(BytesIO(await WebDAV.read_bytes(file_name))) -async def api_return_ico(img: Image) -> StreamingResponse: +class ImageData(BaseModel): + width: int + height: int + aspect_ratio: float + data_url: str + + @classmethod + def create( + cls, + *, + media_type: str, + content: BytesIO, + width: int, + height: int, + ) -> Self: + img_data = base64.b64encode(content.getvalue()).decode("utf-8") + + return cls( + width=width, + height=height, + aspect_ratio=width / height, + data_url=f"data:{media_type};base64,{img_data}", + ) + + +async def api_return_ico(img: Image) -> ImageData: """ ICO-Bild mit API zurückgeben """ - # JPEG-Daten in Puffer speichern + # ICO-Daten in Puffer speichern (256px) img_buffer = BytesIO() img.resize(size=(256, 256), resample=Resampling.LANCZOS) img.save(img_buffer, format="ICO") - img_buffer.seek(0) # zurückgeben - return StreamingResponse( + return ImageData.create( media_type="image/x-icon", content=img_buffer, + width=img.width, + height=img.height, ) -async def api_return_jpeg(img: Image) -> StreamingResponse: +async def api_return_jpeg(img: Image) -> ImageData: """ JPEG-Bild mit API zurückgeben """ @@ -141,12 +168,13 @@ async def api_return_jpeg(img: Image) -> StreamingResponse: # JPEG-Daten in Puffer speichern img_buffer = BytesIO() img.save(img_buffer, format="JPEG", quality=85) - img_buffer.seek(0) # zurückgeben - return StreamingResponse( + return ImageData.create( media_type="image/jpeg", content=img_buffer, + width=img.width, + height=img.height, ) diff --git a/api/advent22_api/routers/user.py b/api/advent22_api/routers/user.py index 48c0e54..20ee228 100644 --- a/api/advent22_api/routers/user.py +++ b/api/advent22_api/routers/user.py @@ -1,25 +1,31 @@ from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.responses import StreamingResponse from PIL.Image import Image -from ..core.calendar_config import CalendarConfig, DoorsSaved, get_calendar_config +from ..core.calendar_config import ( + CalendarConfig, + DoorsSaved, + get_calendar_config, +) from ..core.config import Config, Site, get_config from ..core.depends import get_all_event_dates, get_day_image -from ..core.helpers import EventDates, api_return_ico, api_return_jpeg, load_image +from ..core.helpers import ( + EventDates, + ImageData, + api_return_ico, + api_return_jpeg, + load_image, +) from ._security import user_can_view_day, user_is_admin, user_visible_days router = APIRouter(prefix="/user", tags=["user"]) -@router.get( - "/background_image", - response_class=StreamingResponse, -) +@router.get("/background_image") async def get_background_image( cal_cfg: CalendarConfig = Depends(get_calendar_config), -) -> StreamingResponse: +) -> ImageData: """ Hintergrundbild laden """ @@ -27,13 +33,10 @@ async def get_background_image( return await api_return_jpeg(await load_image(f"files/{cal_cfg.background}")) -@router.get( - "/favicon", - response_class=StreamingResponse, -) +@router.get("/favicon") async def get_favicon( cal_cfg: CalendarConfig = Depends(get_calendar_config), -) -> StreamingResponse: +) -> ImageData: """ Favicon laden """ @@ -68,15 +71,12 @@ async def get_doors( return [door for door in cal_cfg.doors if door.day in visible_days] -@router.get( - "/image_{day}", - response_class=StreamingResponse, -) +@router.get("/image_{day}") async def get_image_for_day( user_can_view: bool = Depends(user_can_view_day), is_admin: bool = Depends(user_is_admin), image: Image | None = Depends(get_day_image), -) -> StreamingResponse: +) -> ImageData: """ Bild für einen Tag erstellen """