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:
+52
-7
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user