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_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: dict[int, str] = {} for day, letter in zip(solution_days, cfg.solution.clean): result[day] = result.get(day, "") 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, )