From 7fc262f9c359af25ab77355a821e57d0e594d489 Mon Sep 17 00:00:00 2001 From: Thomas Faour Date: Wed, 22 Jul 2026 22:58:06 -0400 Subject: [PATCH] Strip emoji from calendar event titles before rendering 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. --- server/app/calendar_render.py | 40 +++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/server/app/calendar_render.py b/server/app/calendar_render.py index 876e7e0..e3b4eaa 100644 --- a/server/app/calendar_render.py +++ b/server/app/calendar_render.py @@ -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