"""Shows a picture -- either uploaded through the web UI or downloaded from a URL. Either way it ends up as a local file in UPLOAD_DIR and check() just reads that file; a URL is downloaded once, at settings-save time (in web/app.py), not re-fetched on every poll -- this screen has no partial refresh any more than the rest of the panel does, so re-fetching on a timer would risk a pointless flash every poll_interval if the remote image ever came back byte-different despite looking the same. Same contrast-boost/ grayscale treatment as the Navidrome cover art, since the same e-ink legibility concerns apply. """ import logging from pathlib import Path from PIL import Image, ImageDraw, ImageEnhance from screens.base import Screen from screens.render_utils import fit_contain, fit_cover, load_font, text_width logger = logging.getLogger(__name__) UPLOAD_DIR = Path(__file__).resolve().parent.parent / "web" / "static" / "uploads" class PictureScreen(Screen): id = "picture" display_name = "Picture" poll_interval = 1800 # just a cheap local file re-read, doesn't need to be frequent def default_settings(self) -> dict: return { "mode": "url", # "url" or "upload" -- purely informational (which source the current cached file came from), check() treats both the same "image_url": "", "uploaded_filename": "", "fit": "cover", # "cover" or "contain" "boost_contrast": True, "grayscale": False, } def check(self, settings: dict): filename = settings.get("uploaded_filename") if not filename: return None path = UPLOAD_DIR / filename if not path.exists(): logger.warning("Picture %s is missing", path) return None try: return Image.open(path).convert("RGB") except OSError as exc: logger.warning("Could not open picture %s: %s", path, exc) return None def render(self, state, settings: dict, resolution: tuple[int, int]) -> Image.Image: width, height = resolution if state is None: image = Image.new("RGB", (width, height), "white") draw = ImageDraw.Draw(image) font = load_font(size=int(min(width, height) * 0.06)) msg = "Kein Bild konfiguriert" mw = text_width(draw, msg, font) draw.text(((width - mw) / 2, height / 2), msg, font=font, fill="black") return image if settings.get("fit", "cover") == "contain": image = fit_contain(state, width, height) else: image = fit_cover(state, width, height) if settings.get("boost_contrast", True): image = ImageEnhance.Contrast(image).enhance(1.35) image = ImageEnhance.Brightness(image).enhance(1.05) if settings.get("grayscale", False): image = image.convert("L").convert("RGB") return image