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_event_dates( cfg: Config = Depends(get_config), days: list[int] = Depends(get_all_sorted_days), ) -> EventDates: """ Aktueller Kalender-Zeitraum """ 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_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_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, )