Strip emoji from calendar event titles before rendering
Build and push server image / build-and-push (push) Successful in 49s

ImageFont.load_default() has no emoji glyphs, and PIL/FreeType don't
skip an unsupported codepoint -- they substitute a visible ".notdef"
tofu box, which read as a rendering glitch rather than "not supported."
Stripped instead (covers the standard emoji Unicode blocks, skin-tone
modifiers, and the ZWJ used to combine them into one glyph), with
surrounding whitespace collapsed.
This commit is contained in:
2026-07-22 22:58:06 -04:00
parent 27cd6b3703
commit 7fc262f9c3
+38 -2
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import calendar as calendar_module
import io
import re
from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo
@@ -102,6 +103,40 @@ def _fmt_time(dt: datetime) -> str:
return text if text else "12:00 AM"
# ImageFont.load_default() (used everywhere in this module -- see the
# module docstring) has no emoji glyphs. PIL/FreeType don't just skip an
# unsupported codepoint, they substitute a ".notdef" tofu box (a visible
# filled rectangle), which reads as a rendering glitch rather than "emoji
# not supported" on an actual panel. Stripped before drawing rather than
# left to render as tofu. Covers the standard Unicode emoji blocks (plus
# skin-tone modifiers, the zero-width joiner used to combine them into
# one glyph, and the variation selector that forces emoji presentation)
# -- these block ranges are stable even as new individual emoji get
# added within them, so this doesn't need updating as emoji sets grow.
_EMOJI_PATTERN = re.compile(
"["
"\U0001F1E6-\U0001F1FF" # regional indicator symbols (flag emoji)
"\U0001F300-\U0001F5FF" # misc symbols & pictographs
"\U0001F600-\U0001F64F" # emoticons
"\U0001F680-\U0001F6FF" # transport & map symbols
"\U0001F900-\U0001F9FF" # supplemental symbols & pictographs
"\U0001FA70-\U0001FAFF" # symbols & pictographs extended-A
"\U0001F3FB-\U0001F3FF" # skin-tone modifiers
"\U00002600-\U000026FF" # misc symbols (☀☂☕ etc.)
"\U00002700-\U000027BF" # dingbats (✂✈✉ etc.)
"\U0000FE0F" # variation selector-16 (emoji presentation)
"\U0000200D" # zero-width joiner
"]+"
)
def _strip_emoji(text: str) -> str:
"""Removes emoji (see _EMOJI_PATTERN above) and collapses whatever
whitespace that leaves behind -- "\U0001F389 Birthday party" becomes
"Birthday party", not " Birthday party"."""
return re.sub(r"\s+", " ", _EMOJI_PATTERN.sub("", text)).strip()
def _truncate_to_width(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont, max_width: int) -> str:
"""Pixel-width-aware truncation (unlike device.py's char-count
_truncate, tuned for a fixed firmware font at a fixed size) -- this
@@ -313,7 +348,7 @@ def _draw_agenda_day(img: Image.Image, draw: ImageDraw.ImageDraw, day: date, eve
color = _event_color(event, owners_seen, palette_rgb)
draw.rounded_rectangle([text_x0, y + 2, text_x0 + 10, y + row_h - 7], radius=3, fill=color)
time_str = "All day" if event["all_day"] else _fmt_time(_event_start(event, tz))
line = f"{time_str} {event['summary']}"
line = f"{time_str} {_strip_emoji(event['summary'])}"
draw_text(img, (text_x0 + 18, y), _truncate_to_width(draw, line, body_font, text_w - 18), body_font)
y += row_h
@@ -428,7 +463,8 @@ def _build_week(events: list[dict], browse_offset: int, orientation: str, tz: Zo
break
color = _event_color(event, owners_seen, palette_rgb)
draw.rounded_rectangle([x0 + 4, y + 1, x0 + 11, y + row_h - 5], radius=2, fill=color)
text = event["summary"] if event["all_day"] else f"{_fmt_time(_event_start(event, tz))[:-3]} {event['summary']}"
summary = _strip_emoji(event["summary"])
text = summary if event["all_day"] else f"{_fmt_time(_event_start(event, tz))[:-3]} {summary}"
draw_text(img, (x0 + 16, y), _truncate_to_width(draw, text, chip_font, col_w - 20), chip_font)
y += row_h