import colorsys from dataclasses import dataclass from typing import Self, TypeAlias, cast import numpy as np from PIL import Image, ImageDraw, ImageFont from .config import Config _RGB: TypeAlias = tuple[int, int, int] _XY: TypeAlias = tuple[float, float] @dataclass(slots=True, frozen=True) class AdventImage: img: Image.Image @classmethod async def from_img(cls, img: Image.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=Image.LANCZOS, ) ) async def get_text_box( self, xy: _XY, text: str | bytes, font: "ImageFont._Font", anchor: str | None = "mm", **text_kwargs, ) -> "Image._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 = 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: "Image._Box", ) -> tuple[int, int, int]: """ Durchschnittsfarbe eines rechteckigen Ausschnitts in einem Bild berechnen """ pixel_data = self.img.crop(box).getdata() 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: "ImageFont._Font", 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, )