"""Whiteboard frame mode: fetches a Nextcloud Whiteboard (or any other WebDAV server's) .whiteboard file and renders it via the local render-service sidecar (server/render-service/, own README there) -- Excalidraw's real export code, not a hand-rolled reimplementation of its element types/styling/fonts. Deliberately renders at a fixed generous width, not the panel's exact target size -- the resulting PNG then runs through image_pipeline.compose_into exactly like a photo would (crop/letterbox per the frame's own display_mode setting), so this module doesn't need to know anything about panel dimensions/orientation, and whiteboard mode reuses the same fit logic photos mode already has instead of a second parallel implementation of it. Pure functions -- no ORM, no FastAPI Depends -- same testability philosophy as calendar_feed.py/weather.py. """ from __future__ import annotations import json import httpx from . import webdav_client RENDER_SERVICE_URL = "http://127.0.0.1:3001/render" # Rendering (not just fetching) can take a moment for a busy board -- # more generous than a typical fetch timeout. HTTP_TIMEOUT_S = 30.0 # Fixed render width regardless of the target frame's orientation/size -- # see module docstring. Comfortably above this panel's 800px long edge # so downstream cropping isn't working from an upscaled source. RENDER_WIDTH = 1600 CHECK_INTERVAL_S = 20 * 60 # same cadence as calendar_feed's merge-fetch throttle class WhiteboardRenderError(Exception): """Fetch or render failed -- network, auth, an invalid/non-JSON file, or the render sidecar itself erroring. Raised loudly; callers decide what to do.""" def fetch_and_render(url: str, username: str, password: str) -> bytes: """Fetches the .whiteboard file at `url` and renders it to a PNG via the local render-service sidecar. Returns raw PNG bytes at RENDER_WIDTH wide, natural aspect ratio.""" try: raw = webdav_client.fetch_file(url, username, password) except webdav_client.WebDavError as e: raise WhiteboardRenderError(f"Could not fetch whiteboard file: {e}") from e try: scene = json.loads(raw) except ValueError as e: raise WhiteboardRenderError(f"Not a valid whiteboard file (not JSON): {e}") from e if not isinstance(scene.get("elements"), list): raise WhiteboardRenderError('Not a valid whiteboard file (no "elements" array)') try: resp = httpx.post( RENDER_SERVICE_URL, json={ "elements": scene.get("elements", []), "appState": scene.get("appState", {}), "files": scene.get("files", {}), "width": RENDER_WIDTH, }, timeout=HTTP_TIMEOUT_S, ) resp.raise_for_status() return resp.content except httpx.HTTPError as e: raise WhiteboardRenderError(f"Render sidecar failed: {e}") from e