mirror of
https://code.lenaisten.de/Lenaisten/advent22.git
synced 2024-12-25 05:53:00 +00:00
227 lines
5.7 KiB
Python
227 lines
5.7 KiB
Python
import re
|
|
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
|
|
"""
|
|
|
|
# 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]
|
|
|
|
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),
|
|
]
|
|
|
|
for day, letter in zip(solution_days, cfg.solution.clean):
|
|
result[day] += letter
|
|
|
|
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] != "" or day in cfg.puzzle.extra_days]
|
|
|
|
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_manual_image_names(
|
|
manual_image_names: list[str] = Depends(list_images_manual),
|
|
) -> dict[int, str]:
|
|
"""
|
|
Bilder: "manual" zuordnen
|
|
"""
|
|
|
|
num_re = re.compile(r"/(\d+)\.", flags=re.IGNORECASE)
|
|
return {
|
|
int(num_match.group(1)): name
|
|
for name in manual_image_names
|
|
if (num_match := num_re.search(name)) is not None
|
|
}
|
|
|
|
|
|
async def get_all_image_names(
|
|
auto_image_names: dict[int, str] = Depends(get_all_auto_image_names),
|
|
manual_image_names: dict[int, str] = Depends(get_all_manual_image_names),
|
|
) -> dict[int, str]:
|
|
"""
|
|
Bilder "auto" und "manual" zu Tagen zuordnen
|
|
"""
|
|
|
|
result = auto_image_names.copy()
|
|
result.update(manual_image_names)
|
|
|
|
return result
|
|
|
|
|
|
@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),
|
|
manual_image_names: dict[int, str] = Depends(get_all_manual_image_names),
|
|
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 "manual"-Bild zu laden
|
|
img = await load_image(manual_image_names[day])
|
|
|
|
# Als AdventImage verarbeiten
|
|
image = await AdventImage.from_img(img, cfg)
|
|
return image.img
|
|
|
|
except (KeyError, 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,
|
|
)
|