Add Picture screen: upload a file or point at a URL

New screen alongside clock/weather/navidrome. Upload goes through the web
UI (validated with PIL before accepting, stored under web/static/uploads/,
gitignored), or point it at a URL re-fetched every 30 minutes. Cover/contain
fit modes (new fit_cover/fit_contain helpers in render_utils.py, generalized
from the square-only cover-art cropping already used by the Navidrome
screen), plus the same contrast-boost/grayscale options that screen has.
This commit is contained in:
Honey committed 2026-07-21 22:56:32 +02:00
1 parent 61348be674
commit bfe4616f20
8 files changed
+232 -4

No files matched your search

+24 -1
View File
@@ -8,7 +8,7 @@ text and full-color cover art without worrying about the palette themselves.
from pathlib import Path
from PIL import ImageFont
from PIL import Image, ImageFont
FONTS_DIR = Path(__file__).resolve().parent.parent / "fonts"
@@ -77,3 +77,26 @@ def wrap_to_lines(draw, text: str, font: ImageFont.FreeTypeFont, max_width: int,
lines.append(current)
return lines[:max_lines]
def fit_cover(image: Image.Image, width: int, height: int) -> Image.Image:
"""Resize+crop to fill exactly width x height, mirroring CSS's
object-fit: cover -- no letterboxing, but edges may be cropped off."""
src_w, src_h = image.size
scale = max(width / src_w, height / src_h)
scaled = image.resize((round(src_w * scale), round(src_h * scale)), Image.LANCZOS)
left = (scaled.width - width) // 2
top = (scaled.height - height) // 2
return scaled.crop((left, top, left + width, top + height))
def fit_contain(image: Image.Image, width: int, height: int, background=(255, 255, 255)) -> Image.Image:
"""Resize to fit entirely within width x height, centered on a solid
background -- mirrors CSS's object-fit: contain, full image always
visible, letterboxed instead of cropped."""
src_w, src_h = image.size
scale = min(width / src_w, height / src_h)
scaled = image.resize((round(src_w * scale), round(src_h * scale)), Image.LANCZOS)
canvas = Image.new("RGB", (width, height), background)
canvas.paste(scaled, ((width - scaled.width) // 2, (height - scaled.height) // 2))
return canvas