advent22/api/advent22_api/core/depends.py

215 lines
5.4 KiB
Python
Raw Permalink Normal View History

2023-10-31 21:23:34 +00:00
import re
2023-11-01 00:30:33 +00:00
from dataclasses import dataclass
from datetime import date
from io import BytesIO
2023-09-08 18:17:18 +00:00
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
2023-10-29 16:08:16 +00:00
from .dav.webdav import WebDAV
from .helpers import (
2023-11-01 00:30:33 +00:00
RE_TTF,
EventDates,
Random,
2023-11-01 00:30:33 +00:00
list_fonts,
list_images_auto,
list_images_manual,
load_image,
set_len,
)
2023-09-08 18:17:18 +00:00
2023-09-18 21:12:32 +00:00
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))
2023-09-12 17:22:52 +00:00
async def get_all_parts(
cfg: Config = Depends(get_config),
2023-09-18 21:12:32 +00:00
days: list[int] = Depends(get_all_sorted_days),
2023-09-12 07:27:59 +00:00
) -> dict[int, str]:
"""
Lösung auf vorhandene Tage aufteilen
"""
2023-11-24 00:40:54 +00:00
# noch keine Buchstaben verteilt
result = {day: "" for day in days}
# extra-Tage ausfiltern
days = [day for day in days if day not in cfg.puzzle.extra_days]
2023-10-31 19:18:18 +00:00
solution_length = len(cfg.solution.clean)
num_days = len(days)
rnd = await Random.get()
solution_days = [
2023-09-14 23:38:35 +00:00
# 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)),
2023-09-14 23:38:35 +00:00
# wie viele Buchstaben bleiben übrig?
# zB 26 % 10 == 6 Buchstaben => an 6 Tagen ein Zeichen mehr
*rnd.sample(days, solution_length % num_days),
]
2023-10-31 19:18:18 +00:00
for day, letter in zip(solution_days, cfg.solution.clean):
2023-09-12 07:27:59 +00:00
result[day] += letter
return result
2023-11-24 00:01:09 +00:00
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:
2023-11-24 00:40:54 +00:00
days = [day for day in days if parts[day] != "" or day in cfg.puzzle.extra_days]
2023-11-24 00:01:09 +00:00
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,
)
2023-09-12 17:22:52 +00:00
async def get_all_auto_image_names(
2023-09-18 21:12:32 +00:00
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)
2023-10-31 21:23:34 +00:00
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
2023-11-01 00:30:33 +00:00
@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],
2023-11-01 00:30:33 +00:00
ttfonts: list[TTFont],
) -> Image.Image:
"""
Automatisch generiertes Bild erstellen
"""
# Datei existiert garantiert!
img = await load_image(auto_image_names[day])
2023-10-31 20:40:07 +00:00
image = await AdventImage.from_img(img, cfg)
rnd = await Random.get(day)
2023-11-01 01:34:04 +00:00
xy_range = range(cfg.image.border, (cfg.image.size - cfg.image.border))
# Buchstaben verstecken
2023-09-14 23:38:35 +00:00
for letter in day_parts[day]:
await image.hide_text(
2023-10-31 20:40:07 +00:00
xy=cast(_XY, tuple(rnd.choices(xy_range, k=2))),
text=letter,
2023-11-01 00:30:33 +00:00
font=await rnd.choice(ttfonts).font,
2023-09-08 18:17:18 +00:00
)
return image.img
async def get_day_image(
day: int,
2023-09-21 11:26:02 +00:00
days: list[int] = Depends(get_all_sorted_days),
cfg: Config = Depends(get_config),
2023-09-21 11:22:27 +00:00
auto_image_names: dict[int, str] = Depends(get_all_auto_image_names),
2023-09-14 23:38:35 +00:00
day_parts: dict[int, str] = Depends(get_all_parts),
2023-11-01 00:30:33 +00:00
ttfonts: list[TTFont] = Depends(get_all_ttfonts),
2023-09-21 11:26:02 +00:00
) -> Image.Image | None:
"""
Bild für einen Tag abrufen
"""
2023-09-21 11:26:02 +00:00
if day not in days:
return None
try:
# Versuche, aus "manual"-Ordner zu laden
2023-09-10 15:22:45 +00:00
img = await load_image(f"images_manual/{day}.jpg")
# Als AdventImage verarbeiten
2023-10-31 20:40:07 +00:00
image = await AdventImage.from_img(img, cfg)
2023-09-10 15:22:45 +00:00
return image.img
except RuntimeError:
# Erstelle automatisch generiertes Bild
return await gen_day_auto_image(
2023-11-01 00:30:33 +00:00
day=day,
cfg=cfg,
auto_image_names=auto_image_names,
day_parts=day_parts,
ttfonts=ttfonts,
)