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.
This commit is contained in:
moonwhaler
2026-08-13 21:51:31 +02:00
parent fa2eee46a2
commit faa30912eb
+52 -7
View File
@@ -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,