"""Shows a picture -- either uploaded through the web UI or fetched from a URL on a timer. Same contrast-boost/grayscale treatment as the Navidrome cover art, since the same e-ink legibility concerns apply. """ import logging from io import BytesIO from pathlib import Path import requests 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__) HTTP_TIMEOUT = 15 UPLOAD_DIR = Path(__file__).resolve().parent.parent / "web" / "static" / "uploads" class PictureScreen(Screen): id = "picture" display_name = "Picture" poll_interval = 1800 # 30 minutes -- only matters for url mode def default_settings(self) -> dict: return { "mode": "url", # "url" or "upload" "image_url": "", "uploaded_filename": "", "fit": "cover", # "cover" or "contain" "boost_contrast": True, "grayscale": False, } def check(self, settings: dict): mode = settings.get("mode", "url") if mode == "upload": filename = settings.get("uploaded_filename") if not filename: return None path = UPLOAD_DIR / filename if not path.exists(): logger.warning("Uploaded picture %s is missing", path) return None try: return Image.open(path).convert("RGB") except OSError as exc: logger.warning("Could not open uploaded picture %s: %s", path, exc) return None url = (settings.get("image_url") or "").strip() if not url: return None try: response = requests.get(url, timeout=HTTP_TIMEOUT) response.raise_for_status() return Image.open(BytesIO(response.content)).convert("RGB") except (requests.exceptions.RequestException, OSError) as exc: logger.warning("Could not load picture from %s: %s", url, 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=(140, 140, 140)) 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