import colorsys from dataclasses import dataclass from typing import Self, TypeAlias, cast import numpy as np from PIL import Image as PILImage from PIL import ImageDraw from PIL.Image import Image, Resampling from PIL.ImageFont import FreeTypeFont from .config import Config _RGB: TypeAlias = tuple[int, int, int] _XY: TypeAlias = tuple[float, float] _Box: TypeAlias = tuple[int, int, int, int] @dataclass(slots=True, frozen=True) class AdventImage: img: Image @classmethod async def from_img(cls, img: Image, cfg: Config) -> Self: """ Einen quadratischen Ausschnitt aus der Mitte des Bilds nehmen """ # Farbmodell festlegen img = img.convert(mode="RGB") # Größen bestimmen width, height = img.size square = min(width, height) # zuschneiden img = img.crop( box=( int((width - square) / 2), int((height - square) / 2), int((width + square) / 2), int((height + square) / 2), ) ) # skalieren return cls( img.resize( size=(cfg.image.size, cfg.image.size), resample=Resampling.LANCZOS, ) ) async def get_text_box( self, xy: _XY, text: str | bytes, font: FreeTypeFont, anchor: str | None = "mm", **text_kwargs, ) -> _Box | None: """ Koordinaten (links, oben, rechts, unten) des betroffenen Rechtecks bestimmen, wenn das Bild mit einem Text versehen wird """ # Neues 1-Bit Bild, gleiche Größe mask = PILImage.new(mode="1", size=self.img.size) # 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: _Box, ) -> tuple[int, int, int]: """ Durchschnittsfarbe eines rechteckigen Ausschnitts in einem Bild berechnen """ pixel_data = np.asarray(self.img.crop(box)) print(pixel_data) mean_color: np.ndarray = np.mean(pixel_data, axis=0) return cast(_RGB, tuple(mean_color.astype(int))) async def hide_text( self, xy: _XY, text: str | bytes, font: FreeTypeFont, anchor: str | None = "mm", **text_kwargs, ) -> None: """ Text `text` in Bild an Position `xy` verstecken. Weitere Parameter wie bei `ImageDraw.text()`. """ # betroffenen Bildbereich bestimmen text_box = await self.get_text_box( xy=xy, text=text, font=font, anchor=anchor, **text_kwargs ) if text_box is not None: # Durchschnittsfarbe bestimmen text_color = await self.get_average_color( 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(self.img).text( xy=xy, text=text, font=font, fill=cast(_RGB, text_color), anchor=anchor, **text_kwargs, )