diff --git a/api/advent22_api/routers/_image.py b/api/advent22_api/routers/_image.py new file mode 100644 index 0000000..99498e3 --- /dev/null +++ b/api/advent22_api/routers/_image.py @@ -0,0 +1,83 @@ +from dataclasses import dataclass + +import numpy as np +from PIL import Image, ImageDraw, ImageFont + + +@dataclass +class AdventImage: + img: Image.Image + + @classmethod + async def load_standard(cls, fp) -> "AdventImage": + """ + Bild laden und einen quadratischen Ausschnitt + aus der Mitte nehmen + """ + + # Bild laden + img = Image.open(fp=fp) + + # 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=(500, 500), + resample=Image.ANTIALIAS, + ) + + # Farbmodell festlegen + return cls(img=img.convert("RGB")) + + async def get_text_box( + self, + 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=self.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( + self, + box: tuple[int, int, int, int], + ) -> tuple[int, int, int]: + """ + Durchschnittsfarbe eines rechteckigen Ausschnitts in + einem Bild `img` berechnen + """ + + pixel_data = self.img.crop(box).getdata() + mean_color: np.ndarray = np.mean(pixel_data, axis=0) + + return tuple(mean_color.astype(int).tolist()) diff --git a/api/advent22_api/routers/days.py b/api/advent22_api/routers/days.py index e806ebd..52d186f 100644 --- a/api/advent22_api/routers/days.py +++ b/api/advent22_api/routers/days.py @@ -4,12 +4,12 @@ import re # 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 +from PIL import ImageDraw, ImageFont from ..dav_common import get_file, list_files +from ._image import AdventImage router = APIRouter(prefix="/days", tags=["days"]) @@ -65,9 +65,9 @@ _RE_IMAGE_FILE = re.compile( ) -async def load_picture_standard( +async def load_image( index: int, -) -> Image.Image: +) -> AdventImage: """ Bild laden und einen quadratischen Ausschnitt aus der Mitte nehmen @@ -78,73 +78,7 @@ async def load_picture_standard( dat = rnd.choice(await list_files(_RE_IMAGE_FILE)) img_buffer = await get_file(dat) - img = Image.open(img_buffer) - - # 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=(500, 500), - 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()) + return await AdventImage.load_standard(img_buffer) @router.get( @@ -154,7 +88,7 @@ async def get_average_color( async def get_picture_for_day( index: int, letter: str = Depends(get_letter), - img: Image.Image = Depends(load_picture_standard), + adv_img: AdventImage = Depends(load_image), ) -> StreamingResponse: """ Bild für einen Tag erstellen @@ -168,8 +102,7 @@ async def get_picture_for_day( xy = tuple(rnd.choices(range(30, 470), k=2)) # betroffenen Bildbereich bestimmen - text_box = await get_text_box( - img=img, + text_box = await adv_img.get_text_box( xy=xy, text=letter, font=font, @@ -177,8 +110,7 @@ async def get_picture_for_day( if text_box is not None: # Durchschnittsfarbe bestimmen - text_color = await get_average_color( - img=img, + text_color = await adv_img.get_average_color( box=text_box, ) @@ -196,7 +128,7 @@ async def get_picture_for_day( text_color = tuple(int(val) for val in text_color) # Buchstaben verstecken - ImageDraw.Draw(img).text( + ImageDraw.Draw(adv_img.img).text( xy=xy, text=letter, font=font, @@ -206,7 +138,7 @@ async def get_picture_for_day( # Bilddaten in Puffer laden img_buffer = BytesIO() - img.save(img_buffer, format="JPEG", quality=85) + adv_img.img.save(img_buffer, format="JPEG", quality=85) img_buffer.seek(0) return StreamingResponse(