Source code for todays_paper_web.widgets.core.image

import logging
from typing import Optional

from PIL import Image
from pydantic import Field

from todays_paper_web.widgets import registerView, View
from todays_paper_web.widgets.view import ViewParams

_LOGGER = logging.getLogger(__name__)


class ImageParams(ViewParams):
    """Shared by every image-producing widget (`image`, `gocomics`, `weather.icon`) - sizing/
    fitting only. Each concrete widget's own `Params` adds whatever it needs on top (e.g.
    `LocalImageParams.path` below) to say where the source bitmap actually comes from."""

    width: Optional[int] = Field(
        None, description="Target width; unset keeps the source image's own."
    )
    height: Optional[int] = Field(
        None, description="Target height; unset keeps the source image's own."
    )
    fillmode: str = Field(
        "preserveaspectfit",
        description="How the source image fills the width/height box: `preserveaspectfit` shrinks "
        "to fit within the box without enlarging; `preserveaspectstretch` scales to touch at least "
        "one edge of the box. Both preserve aspect ratio. An unrecognized value is logged and "
        "leaves the image unresized, rather than raising.",
    )


class ImageBaseView(View):
    """Shared resize/fit logic for any image-producing widget - not registered as a tag itself.
    A subclass (`ImageView` below, `GoComics`, `WeatherIconView`) implements `fetch_img()` to say
    where its source bitmap comes from; this class doesn't know or care."""

    Params = ImageParams

    def render(self):
        img = self.fetch_img()
        if img is None:
            return None
        if img.mode in ("LA"):
            img = img.convert("RGBA")

        new_width, new_height = img.size
        if self.params.width:
            new_width = self.params.width
        if self.params.height:
            new_height = self.params.height
        if new_width != img.size[0] or new_height != img.size[1]:
            img = self.resize(img, new_width, new_height)
        return img

    def fetch_img(self) -> Image:
        raise NotImplementedError

    def resize(self, img: Image, box_width: int, box_height: int) -> Image:
        orig_width, orig_height = img.size
        aspect = orig_width / orig_height
        fillmode = self.params.fillmode.lower()
        new_width, new_height = img.size
        if fillmode in ("preserveaspectfit", "preserve_aspect_fit"):
            if orig_width > box_width:
                new_width = box_width
                new_height = round(box_width / orig_width * orig_height)
            if orig_height > box_height:
                new_width = round(box_height / orig_height * orig_width)
                new_height = box_height
        elif fillmode in ("preserveaspectstretch", "preserve_aspect_stretch"):
            if box_height * aspect <= box_width:
                new_width = round(box_height * aspect)
                new_height = box_height
            else:
                new_width = box_width
                new_height = round(box_width / aspect)

        else:
            _LOGGER.warning("Unknown fillmode %s", fillmode)
            return img
        return img.resize((new_width, new_height))


[docs] class LocalImageParams(ImageParams): path: str = Field( ..., description="File path, resolved like any other file the config references." )
@registerView("image") class ImageView(ImageBaseView): Params = LocalImageParams def fetch_img(self) -> Image: return Image.open(self.openFile(self.params.path))