advent22/api/advent22_api/core/depends.py

213 lines
5.4 KiB
Python

import re
from collections import defaultdict
from dataclasses import dataclass
from datetime import date
from io import BytesIO
from typing import cast
from fastapi import Depends
from PIL import Image, ImageFont
from .advent_image import _XY, AdventImage
from .calendar_config import CalendarConfig, get_calendar_config
from .config import Config, get_config
from .dav.webdav import WebDAV
from .helpers import (
RE_TTF,
EventDates,
Random,
list_fonts,
list_images_auto,
list_images_manual,
load_image,
set_len,
)
async def get_all_sorted_days(
cal_cfg: CalendarConfig = Depends(get_calendar_config),
) -> list[int]:
"""
Alle Tage, für die es ein Türchen gibt
"""
return sorted(set(door.day for door in cal_cfg.doors))
async def get_all_parts(
cfg: Config = Depends(get_config),
days: list[int] = Depends(get_all_sorted_days),
) -> dict[int, str]:
"""
Lösung auf vorhandene Tage aufteilen
"""
solution_length = len(cfg.solution.clean)
num_days = len(days)
rnd = await Random.get()
solution_days = [
# wie oft passen die Tage "ganz" in die Länge der Lösung?
# zB 26 Buchstaben // 10 Tage == 2 mal => 2 Zeichen pro Tag
*rnd.shuffled(days * (solution_length // num_days)),
# wie viele Buchstaben bleiben übrig?
# zB 26 % 10 == 6 Buchstaben => an 6 Tagen ein Zeichen mehr
*rnd.sample(days, solution_length % num_days),
]
result: defaultdict[int, str] = defaultdict(str)
for day, letter in zip(solution_days, cfg.solution.clean):
result[day] += letter
result |= {missed_day: "" for missed_day in set(days) - set(result.keys())}
return result
async def get_all_event_dates(
cfg: Config = Depends(get_config),
days: list[int] = Depends(get_all_sorted_days),
parts: dict[int, str] = Depends(get_all_parts),
) -> EventDates:
"""
Aktueller Kalender-Zeitraum
"""
if cfg.puzzle.skip_empty:
days = [day for day in days if parts[day] != ""]
return EventDates(
today=date.today(),
begin_month=cfg.puzzle.begin_month,
begin_day=cfg.puzzle.begin_day,
events=days,
close_after=cfg.puzzle.close_after,
)
async def get_all_auto_image_names(
days: list[int] = Depends(get_all_sorted_days),
images: list[str] = Depends(list_images_auto),
) -> dict[int, str]:
"""
Bilder: Reihenfolge zufällig bestimmen
"""
rnd = await Random.get()
ls = set_len(images, len(days))
return dict(zip(days, rnd.shuffled(ls)))
async def get_all_image_names(
auto_image_names: dict[int, str] = Depends(get_all_auto_image_names),
manual_image_names: list[str] = Depends(list_images_manual),
) -> dict[int, str]:
"""
Bilder "auto" und "manual" zu Tagen zuordnen
"""
num_re = re.compile(r"/(\d+)\.", flags=re.IGNORECASE)
for name in manual_image_names:
assert (num_match := num_re.search(name)) is not None
auto_image_names[int(num_match.group(1))] = name
return auto_image_names
@dataclass(slots=True, frozen=True)
class TTFont:
# Dateiname
file_name: str
# Schriftgröße für den Font
size: int = 50
@property
async def font(self) -> "ImageFont._Font":
return ImageFont.truetype(
font=BytesIO(await WebDAV.read_bytes(self.file_name)),
size=100,
)
async def get_all_ttfonts(
font_names: list[str] = Depends(list_fonts),
) -> list[TTFont]:
result = []
for name in font_names:
assert (size_match := RE_TTF.search(name)) is not None
result.append(
TTFont(
file_name=name,
size=int(size_match.group(1)),
)
)
return result
async def gen_day_auto_image(
day: int,
cfg: Config,
auto_image_names: dict[int, str],
day_parts: dict[int, str],
ttfonts: list[TTFont],
) -> Image.Image:
"""
Automatisch generiertes Bild erstellen
"""
# Datei existiert garantiert!
img = await load_image(auto_image_names[day])
image = await AdventImage.from_img(img, cfg)
rnd = await Random.get(day)
xy_range = range(cfg.image.border, (cfg.image.size - cfg.image.border))
# Buchstaben verstecken
for letter in day_parts[day]:
await image.hide_text(
xy=cast(_XY, tuple(rnd.choices(xy_range, k=2))),
text=letter,
font=await rnd.choice(ttfonts).font,
)
return image.img
async def get_day_image(
day: int,
days: list[int] = Depends(get_all_sorted_days),
cfg: Config = Depends(get_config),
auto_image_names: dict[int, str] = Depends(get_all_auto_image_names),
day_parts: dict[int, str] = Depends(get_all_parts),
ttfonts: list[TTFont] = Depends(get_all_ttfonts),
) -> Image.Image | None:
"""
Bild für einen Tag abrufen
"""
if day not in days:
return None
try:
# Versuche, aus "manual"-Ordner zu laden
img = await load_image(f"images_manual/{day}.jpg")
# Als AdventImage verarbeiten
image = await AdventImage.from_img(img, cfg)
return image.img
except RuntimeError:
# Erstelle automatisch generiertes Bild
return await gen_day_auto_image(
day=day,
cfg=cfg,
auto_image_names=auto_image_names,
day_parts=day_parts,
ttfonts=ttfonts,
)