import colorsys import random from datetime import date from io import BytesIO import numpy as np from fastapi import APIRouter, Depends from fastapi.responses import StreamingResponse from PIL import Image, ImageDraw, ImageFont router = APIRouter(prefix="/days", tags=["days"]) loesungswort = "ABCDEFGHIJKLMNOPQRSTUVWX" async def shuffle(string: str) -> str: rnd = random.Random(loesungswort) return "".join(rnd.sample(string, len(string))) @router.on_event("startup") async def narf() -> None: print(loesungswort) print(await shuffle(loesungswort)) @router.get("/letter/{index}") async def get_letter( index: int, ) -> str: return (await shuffle(loesungswort))[index] @router.get("/date") def get_date() -> int: return date.today().day @router.get( "/picture", response_class=StreamingResponse, ) async def get_picture(): img = Image.open("hand.png").convert("RGBA") d1 = ImageDraw.Draw(img) font = ImageFont.truetype("Lena.ttf", 50) d1.text((260, 155), "W", font=font, fill=(0, 0, 255)) # d1.text(xy=(400, 210), text="Deine Hände auch?", # font=Font, fill=(255, 0, 0)) img_buffer = BytesIO() img.save(img_buffer, format="PNG", quality=85) img_buffer.seek(0) return StreamingResponse( content=img_buffer, media_type="image/png", ) async def load_picture_standard() -> Image.Image: """ Bild laden und einen quadratischen Ausschnitt aus der Mitte nehmen """ # Bild laden img = Image.open("lena2.jpg") # Größen bestimmen width, height = img.size square = min(width, height) # Bild zuschneiden und skalieren img = img.crop(box=( int((width - square)/2), int((height - square)/2), int((width + square)/2), int((height + square)/2), )) img = img.resize( size=(400, 400), resample=Image.ANTIALIAS, ) # Farbmodell festlegen return img.convert("RGB") async def get_text_box( img: Image.Image, xy: tuple[float, float], text: str | bytes, font: "ImageFont._Font", anchor: str | None = "mm", **text_kwargs, ) -> tuple[int, int, int, int] | None: """ Koordinaten (links, oben, rechts, unten) des betroffenen Rechtecks bestimmen, wenn das Bild `img` mit einem Text versehen wird """ # Neues 1-Bit Bild, gleiche Größe mask = Image.new(mode="1", size=img.size, color=0) # Text auf Maske auftragen ImageDraw.Draw(mask).text( xy=xy, text=text, font=font, anchor=anchor, fill=1, **text_kwargs, ) # betroffenen Pixelbereich bestimmen return mask.getbbox() async def get_average_color( img: Image.Image, box: tuple[int, int, int, int], ) -> tuple[int, int, int]: """ Durchschnittsfarbe eines rechteckigen Ausschnitts in einem Bild `img` berechnen """ pixel_data = img.crop(box).getdata() mean_color: np.ndarray = np.mean(pixel_data, axis=0) return tuple(mean_color.astype(int).tolist()) @router.get( "/picture/{index}", response_class=StreamingResponse, ) async def get_picture_for_day( index: int, letter: str = Depends(get_letter), img: Image.Image = Depends(load_picture_standard), ) -> StreamingResponse: """ Bild für einen Tag erstellen """ # Font laden font = ImageFont.truetype("Lena.ttf", 50) # Position des Buchstaben bestimmen rnd = random.Random(f"{loesungswort}{index}") xy = tuple(rnd.choices(range(30, 370), k=2)) # betroffenen Bildbereich bestimmen text_box = await get_text_box( img=img, xy=xy, text=letter, font=font, ) if text_box is not None: # Durchschnittsfarbe bestimmen text_color = await get_average_color( img=img, box=text_box, ) # etwas heller/dunkler machen tc_h, tc_s, tc_v = colorsys.rgb_to_hsv(*text_color) tc_v = int((tc_v - 127) * 0.97) + 127 if tc_v < 127: tc_v += 3 else: tc_v -= 3 text_color = colorsys.hsv_to_rgb(tc_h, tc_s, tc_v) text_color = tuple(int(val) for val in text_color) # Buchstaben verstecken ImageDraw.Draw(img).text( xy=xy, text=letter, font=font, anchor="mm", fill=text_color, ) # Bilddaten in Puffer laden img_buffer = BytesIO() img.save(img_buffer, format="JPEG", quality=85) img_buffer.seek(0) return StreamingResponse( content=img_buffer, media_type="image/jpeg", )