Initial commit: Paperwall, a standalone e-ink dashboard
Navidrome now-playing, clock, and weather for a Waveshare epd4in0e panel, with a local web UI and install/uninstall scripts for the Pi.
This commit is contained in:
commit
66464291e1
33 files changed
+2215
No files matched your search
@@ -0,0 +1,79 @@
|
||||
"""Shared PIL rendering helpers: font loading and text layout.
|
||||
|
||||
The panel driver dithers whatever RGB image we hand it down to its 6-color
|
||||
palette (see epd4in0e.getbuffer, which quantizes with Floyd-Steinberg
|
||||
dithering by default) -- so screens can draw with normal anti-aliased RGB
|
||||
text and full-color cover art without worrying about the palette themselves.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import ImageFont
|
||||
|
||||
FONTS_DIR = Path(__file__).resolve().parent.parent / "fonts"
|
||||
|
||||
_font_cache: dict[tuple[str, int], ImageFont.FreeTypeFont] = {}
|
||||
|
||||
|
||||
def load_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont:
|
||||
key = ("bold" if bold else "regular", size)
|
||||
if key not in _font_cache:
|
||||
filename = "DejaVuSans-Bold.ttf" if bold else "DejaVuSans.ttf"
|
||||
_font_cache[key] = ImageFont.truetype(str(FONTS_DIR / filename), size)
|
||||
return _font_cache[key]
|
||||
|
||||
|
||||
def text_width(draw, text: str, font: ImageFont.FreeTypeFont) -> int:
|
||||
left, _, right, _ = draw.textbbox((0, 0), text, font=font)
|
||||
return right - left
|
||||
|
||||
|
||||
def truncate_to_width(draw, text: str, font: ImageFont.FreeTypeFont, max_width: int) -> str:
|
||||
"""Single-line ellipsis truncation, mirroring the CSS text-overflow: ellipsis look."""
|
||||
if text_width(draw, text, font) <= max_width:
|
||||
return text
|
||||
|
||||
ellipsis = "…"
|
||||
lo, hi = 0, len(text)
|
||||
while lo < hi:
|
||||
mid = (lo + hi + 1) // 2
|
||||
candidate = text[:mid].rstrip() + ellipsis
|
||||
if text_width(draw, candidate, font) <= max_width:
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid - 1
|
||||
return text[:lo].rstrip() + ellipsis if lo else ellipsis
|
||||
|
||||
|
||||
def wrap_to_lines(draw, text: str, font: ImageFont.FreeTypeFont, max_width: int, max_lines: int) -> list[str]:
|
||||
"""Word-wrap up to `max_lines`, ellipsis-truncating the last line if it still
|
||||
overflows -- mirrors the CSS -webkit-line-clamp behavior used in the old design."""
|
||||
words = text.split()
|
||||
if not words:
|
||||
return []
|
||||
|
||||
lines: list[str] = []
|
||||
current = words[0]
|
||||
next_index = 1
|
||||
while next_index < len(words):
|
||||
word = words[next_index]
|
||||
candidate = f"{current} {word}"
|
||||
if text_width(draw, candidate, font) <= max_width:
|
||||
current = candidate
|
||||
next_index += 1
|
||||
continue
|
||||
|
||||
lines.append(current)
|
||||
current = word
|
||||
next_index += 1
|
||||
if len(lines) == max_lines - 1:
|
||||
break
|
||||
|
||||
if next_index < len(words):
|
||||
# Ran out of lines with words still remaining -- fold the rest into
|
||||
# the last line and ellipsis-truncate it.
|
||||
current = current + " " + " ".join(words[next_index:])
|
||||
current = truncate_to_width(draw, current, font, max_width)
|
||||
|
||||
lines.append(current)
|
||||
return lines[:max_lines]
|
||||
Reference in new issue
Block a user