Simplify and caption
This commit is contained in:
parent
cd70c4cdca
commit
2c8d78b397
1 changed files with 551 additions and 596 deletions
106
src/lib/overlay.py
Normal file
106
src/lib/overlay.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
"""Text overlay rendering for the e-ink frame.
|
||||
|
||||
Paints aliased white-on-black-stroke text into the dithered palette index
|
||||
array; black/white survive Atkinson dithering so edges stay crisp on e-ink.
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
FONT_CANDIDATES = (
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/freefont/FreeSansBold.ttf",
|
||||
"/System/Library/Fonts/Helvetica.ttc",
|
||||
)
|
||||
|
||||
PALETTE_BLACK = 0
|
||||
PALETTE_WHITE = 1
|
||||
|
||||
|
||||
def _load_font(size: int) -> ImageFont.ImageFont:
|
||||
for path in FONT_CANDIDATES:
|
||||
if os.path.exists(path):
|
||||
return ImageFont.truetype(path, size)
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def format_age(asset: dict) -> str | None:
|
||||
"""Photo capture age as 'N days/weeks/months/years ago'."""
|
||||
exif = asset.get("exifInfo") or {}
|
||||
date_str = exif.get("dateTimeOriginal") or asset.get("fileCreatedAt")
|
||||
if not date_str:
|
||||
return None
|
||||
try:
|
||||
dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
days = (datetime.now(timezone.utc) - dt).days
|
||||
if days < 0:
|
||||
return None
|
||||
if days == 0:
|
||||
return "Today"
|
||||
if days == 1:
|
||||
return "Yesterday"
|
||||
if days < 7:
|
||||
return f"{days} days ago"
|
||||
for n, unit in ((365, "year"), (30, "month"), (7, "week")):
|
||||
if days >= n:
|
||||
count = max(1, days // n)
|
||||
return f"{count} {unit}{'s' if count > 1 else ''} ago"
|
||||
|
||||
|
||||
def format_location(asset: dict) -> str | None:
|
||||
"""Most specific location available from EXIF."""
|
||||
exif = asset.get("exifInfo") or {}
|
||||
return exif.get("city") or exif.get("state") or exif.get("country") or None
|
||||
|
||||
|
||||
def render_text_into_indices(indices: np.ndarray,
|
||||
left_text: str | None,
|
||||
right_text: str | None,
|
||||
orientation: int = 0) -> None:
|
||||
"""Paint white-on-black-stroke text into a (height, width) palette-index array.
|
||||
|
||||
Text is laid out viewer-bottom-left/right, then rotated by `orientation`
|
||||
so labels land at the viewer's bottom regardless of frame mounting.
|
||||
"""
|
||||
font_size, margin, stroke_width = 20, 18, 2
|
||||
buffer_h, buffer_w = indices.shape
|
||||
if orientation in (90, 270):
|
||||
view_w, view_h = buffer_h, buffer_w
|
||||
else:
|
||||
view_w, view_h = buffer_w, buffer_h
|
||||
|
||||
fill_layer = Image.new("L", (view_w, view_h), 0)
|
||||
full_layer = Image.new("L", (view_w, view_h), 0)
|
||||
fill_draw = ImageDraw.Draw(fill_layer)
|
||||
full_draw = ImageDraw.Draw(full_layer)
|
||||
font = _load_font(font_size)
|
||||
baseline = view_h - margin
|
||||
|
||||
if left_text:
|
||||
pos = (margin, baseline)
|
||||
fill_draw.text(pos, left_text, font=font, fill=255, anchor="lb")
|
||||
full_draw.text(pos, left_text, font=font, fill=255, anchor="lb",
|
||||
stroke_width=stroke_width, stroke_fill=255)
|
||||
|
||||
if right_text:
|
||||
pos = (view_w - margin, baseline)
|
||||
fill_draw.text(pos, right_text, font=font, fill=255, anchor="rb")
|
||||
full_draw.text(pos, right_text, font=font, fill=255, anchor="rb",
|
||||
stroke_width=stroke_width, stroke_fill=255)
|
||||
|
||||
if orientation:
|
||||
fill_layer = fill_layer.rotate(orientation, expand=True)
|
||||
full_layer = full_layer.rotate(orientation, expand=True)
|
||||
|
||||
fill_mask = np.asarray(fill_layer) >= 128
|
||||
stroke_mask = (np.asarray(full_layer) >= 128) & ~fill_mask
|
||||
indices[stroke_mask] = PALETTE_BLACK
|
||||
indices[fill_mask] = PALETTE_WHITE
|
||||
Loading…
Reference in a new issue