advent22/api/advent22_api/routers/_misc.py

150 lines
3.3 KiB
Python

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",
)