From faa30912ebb86e35fa8f528e9a87d4e18263d2bd Mon Sep 17 00:00:00 2001 From: moonwhaler Date: Thu, 13 Aug 2026 21:51:31 +0200 Subject: [PATCH] fix: use a real bold system font instead of Pillow's default face Pillow's load_default() font is a thin geometric face that renders mushy at small pixel sizes even with a stroke faking boldness. Look up a proper hinted bold font (Liberation Sans / DejaVu Sans Bold, common Linux/Windows/macOS paths) and use it directly; fall back to load_default() with the old stroke trick only if none is found. --- _label_ops.py | 59 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/_label_ops.py b/_label_ops.py index a1540b5..0564198 100644 --- a/_label_ops.py +++ b/_label_ops.py @@ -5,6 +5,8 @@ Shared by the node's own execution (`labeled_load_image_node.py`) and the to what actually gets queued - one implementation, not two. """ +import os + from PIL import Image as PILImage from PIL import ImageDraw, ImageFont @@ -14,6 +16,47 @@ ARROW_ZONE_RATIO = 0.4 MIN_FONT_PX = 16 TEXT_WIDTH_RATIO = 0.92 +# Pillow's own load_default() font is a thin geometric face that reads mushy +# at small pixel sizes even with a stroke faking boldness. A real hinted bold +# font looks crisp at the same size, so prefer whichever of these the host +# actually has installed - common Linux/Windows/macOS bold sans paths, most +# widely available first. Falls back to load_default() if none exist. +_BOLD_FONT_CANDIDATES = ( + "/usr/share/fonts/liberation/LiberationSans-Bold.ttf", + "/usr/share/fonts/liberation-fonts/LiberationSans-Bold.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", + "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf", + "/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf", + "C:\\Windows\\Fonts\\arialbd.ttf", + "C:\\Windows\\Fonts\\seguisb.ttf", + "/System/Library/Fonts/Supplemental/Arial Bold.ttf", + "/Library/Fonts/Arial Bold.ttf", +) + +_bold_font_path = None +_bold_font_path_checked = False + + +def _find_bold_font_path(): + global _bold_font_path, _bold_font_path_checked + if not _bold_font_path_checked: + _bold_font_path_checked = True + for path in _BOLD_FONT_CANDIDATES: + if os.path.isfile(path): + _bold_font_path = path + break + return _bold_font_path + + +def _load_font(size): + """Returns (font, is_real_bold_face). Caller uses the flag to decide + whether a stroke is still needed to fake boldness.""" + path = _find_bold_font_path() + if path: + return ImageFont.truetype(path, size), True + return ImageFont.load_default(size=size), False + def _hex_to_rgb(hex_str, fallback=(0, 0, 0)): if not isinstance(hex_str, str): @@ -29,15 +72,15 @@ def _hex_to_rgb(hex_str, fallback=(0, 0, 0)): def _fit_text(draw, text, max_width, max_font_px): """Shrinks font size to fit max_width, then truncates with an ellipsis if - even the floor size overflows. Returns (font, drawn_text).""" + even the floor size overflows. Returns (font, is_bold_face, drawn_text).""" size = max(MIN_FONT_PX, max_font_px) - font = ImageFont.load_default(size=size) + font, is_bold = _load_font(size) while size > MIN_FONT_PX and draw.textlength(text, font=font) > max_width: size -= 1 - font = ImageFont.load_default(size=size) + font, is_bold = _load_font(size) if draw.textlength(text, font=font) <= max_width: - return font, text + return font, is_bold, text lo, hi = 0, len(text) while lo < hi: @@ -47,7 +90,7 @@ def _fit_text(draw, text, max_width, max_font_px): lo = mid else: hi = mid - 1 - return font, (text[:lo] + "…" if lo > 0 else "…") + return font, is_bold, (text[:lo] + "…" if lo > 0 else "…") def _draw_arrow(draw, center_x, zone_top, zone_h, point_down, color_rgb): @@ -109,8 +152,10 @@ def compose_labeled_image(img, text, position, show_label, show_arrow, if show_label and text: max_font_px = max(MIN_FONT_PX, round(text_zone_h * 0.62)) - font, shown = _fit_text(draw, text, width * TEXT_WIDTH_RATIO, max_font_px) - stroke_width = max(1, round(font.size / 18)) + font, is_bold, shown = _fit_text(draw, text, width * TEXT_WIDTH_RATIO, max_font_px) + # Already a bold face: no stroke, it would just clog up small glyphs. + # Fallback face has no bold weight: fake it with a thin stroke instead. + stroke_width = 0 if is_bold else max(1, round(font.size / 18)) draw.text( (width / 2, text_zone_top + text_zone_h / 2), shown, font=font, fill=text_rgb, stroke_width=stroke_width,