import itertools import random import re from io import BytesIO from typing import Any, Self, Sequence, cast from fastapi import Depends from fastapi.responses import StreamingResponse from PIL import Image, ImageFont from ..config import Config, get_config from ..dav_common import dav_file_exists, dav_get_file, dav_list_files from ._image import _XY, AdventImage ########## # RANDOM # ########## class Random(random.Random): @classmethod async def get(cls, bonus_salt: Any = "") -> Self: cfg = await get_config() return cls(f"{cfg.puzzle.solution}{bonus_salt}{cfg.puzzle.random_pepper}") async def set_length(seq: Sequence, length: int) -> list: # `seq` unendlich wiederholen infinite = itertools.cycle(seq) # Die ersten `length` einträge nehmen return list(itertools.islice(infinite, length)) async def shuffle(seq: Sequence, rnd: random.Random | None = None) -> list: # Zufallsgenerator rnd = rnd or await Random.get() # Elemente mischen return rnd.sample(seq, len(seq)) ######### # IMAGE # ######### async def get_letter( index: int, cfg: Config = Depends(get_config), ) -> str: return (await shuffle(cfg.puzzle.solution))[index] async def list_images_auto() -> list[str]: """ Finde alle Bilder im "automatisch"-Verzeichnis """ ls = await dav_list_files( re.compile(r"\.(gif|jpe?g|tiff?|png|bmp)$", flags=re.IGNORECASE), "/images_auto", ) ls = await set_length(ls, 24) return await shuffle(ls) async def load_image( file_name: str, ) -> AdventImage: """ Versuche, Bild aus Datei zu laden """ if not await dav_file_exists(file_name): raise RuntimeError(f"DAV-File {file_name} does not exist!") img_buffer = await dav_get_file(file_name) img_buffer.seek(0) return await AdventImage.load_standard(img_buffer) async def get_auto_image( index: int, letter: str, images: list[str], cfg: Config, ) -> AdventImage: """ Erstelle automatisch generiertes Bild """ # hier niemals RuntimeError! image = await load_image(images[index]) rnd = await Random.get(index) font = await dav_get_file(f"files/{cfg.server.font}") font.seek(0) # Buchstabe verstecken await image.hide_text( xy=cast(_XY, tuple(rnd.choices(range(30, 470), k=2))), text=letter, font=ImageFont.truetype(font, 50), ) return image async def get_image( index: int, letter: str = Depends(get_letter), images: list[str] = Depends(list_images_auto), cfg: Config = Depends(get_config), ) -> AdventImage: """ Bild für einen Tag erstellen """ try: # Versuche, aus "manual"-Ordner zu laden return await load_image(f"images_manual/{index}.jpg") except RuntimeError: # Erstelle automatisch generiertes Bild return await get_auto_image( index=index, letter=letter, images=images, cfg=cfg, ) async def api_return_image( img: Image.Image, ) -> StreamingResponse: """ Bild mit API zurückgeben """ # Bilddaten in Puffer laden img_buffer = BytesIO() img.save(img_buffer, format="JPEG", quality=85) img_buffer.seek(0) # zurückgeben return StreamingResponse( content=img_buffer, media_type="image/jpeg", )