Skip to content

Commit 147b5d4

Browse files
authored
Merge pull request matplotlib#24059 from meeseeksmachine/auto-backport-of-pr-23638-on-v3.6.x
Backport PR matplotlib#23638 on branch v3.6.x (FIX: correctly handle generic font families in svg text-as-text mode)
2 parents c73f7b1 + bf16cb6 commit 147b5d4

File tree

3 files changed

+114
-6
lines changed

3 files changed

+114
-6
lines changed

lib/matplotlib/backends/backend_svg.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,10 +1151,48 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None):
11511151
weight = fm.weight_dict[prop.get_weight()]
11521152
if weight != 400:
11531153
font_parts.append(f'{weight}')
1154+
1155+
def _format_font_name(fn):
1156+
normalize_names = {
1157+
'sans': 'sans-serif',
1158+
'sans serif': 'sans-serif'
1159+
}
1160+
# A generic font family. We need to do two things:
1161+
# 1. list all of the configured fonts with quoted names
1162+
# 2. append the generic name unquoted
1163+
if fn in fm.font_family_aliases:
1164+
# fix spelling of sans-serif
1165+
# we accept 3 ways CSS only supports 1
1166+
fn = normalize_names.get(fn, fn)
1167+
# get all of the font names and fix spelling of sans-serif
1168+
# if it comes back
1169+
aliases = [
1170+
normalize_names.get(_, _) for _ in
1171+
fm.FontManager._expand_aliases(fn)
1172+
]
1173+
# make sure the generic name appears at least once
1174+
# duplicate is OK, next layer will deduplicate
1175+
aliases.append(fn)
1176+
1177+
for a in aliases:
1178+
# generic font families must not be quoted
1179+
if a in fm.font_family_aliases:
1180+
yield a
1181+
# specific font families must be quoted
1182+
else:
1183+
yield repr(a)
1184+
# specific font families must be quoted
1185+
else:
1186+
yield repr(fn)
1187+
1188+
def _get_all_names(prop):
1189+
for f in prop.get_family():
1190+
yield from _format_font_name(f)
1191+
11541192
font_parts.extend([
11551193
f'{_short_float_fmt(prop.get_size())}px',
1156-
# ensure quoting
1157-
f'{", ".join(repr(f) for f in prop.get_family())}',
1194+
# ensure quoting and expansion of font names
1195+
", ".join(dict.fromkeys(_get_all_names(prop)))
11581196
])
11591197
style['font'] = ' '.join(font_parts)
11601198
if prop.get_stretch() != 'normal':

lib/matplotlib/font_manager.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,9 +1345,12 @@ def findfont(self, prop, fontext='ttf', directory=None,
13451345
rc_params = tuple(tuple(mpl.rcParams[key]) for key in [
13461346
"font.serif", "font.sans-serif", "font.cursive", "font.fantasy",
13471347
"font.monospace"])
1348-
return self._findfont_cached(
1348+
ret = self._findfont_cached(
13491349
prop, fontext, directory, fallback_to_default, rebuild_if_missing,
13501350
rc_params)
1351+
if isinstance(ret, Exception):
1352+
raise ret
1353+
return ret
13511354

13521355
def get_font_names(self):
13531356
"""Return the list of available fonts."""
@@ -1496,8 +1499,11 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
14961499
return self.findfont(default_prop, fontext, directory,
14971500
fallback_to_default=False)
14981501
else:
1499-
raise ValueError(f"Failed to find font {prop}, and fallback "
1500-
f"to the default font was disabled")
1502+
# This return instead of raise is intentional, as we wish to
1503+
# cache the resulting exception, which will not occur if it was
1504+
# actually raised.
1505+
return ValueError(f"Failed to find font {prop}, and fallback "
1506+
f"to the default font was disabled")
15011507
else:
15021508
_log.debug('findfont: Matching %s to %s (%r) with score of %f.',
15031509
prop, best_font.name, best_font.fname, best_score)
@@ -1516,7 +1522,10 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
15161522
return self.findfont(
15171523
prop, fontext, directory, rebuild_if_missing=False)
15181524
else:
1519-
raise ValueError("No valid font could be found")
1525+
# This return instead of raise is intentional, as we wish to
1526+
# cache the resulting exception, which will not occur if it was
1527+
# actually raised.
1528+
return ValueError("No valid font could be found")
15201529

15211530
return _cached_realpath(result)
15221531

lib/matplotlib/tests/test_backend_svg.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,3 +527,64 @@ def test_svg_escape():
527527
fig.savefig(fd, format='svg')
528528
buf = fd.getvalue().decode()
529529
assert '<'"&>"' in buf
530+
531+
532+
@pytest.mark.parametrize("font_str", [
533+
"'DejaVu Sans', 'WenQuanYi Zen Hei', 'Arial', sans-serif",
534+
"'DejaVu Serif', 'WenQuanYi Zen Hei', 'Times New Roman', serif",
535+
"'Arial', 'WenQuanYi Zen Hei', cursive",
536+
"'Impact', 'WenQuanYi Zen Hei', fantasy",
537+
"'DejaVu Sans Mono', 'WenQuanYi Zen Hei', 'Courier New', monospace",
538+
# These do not work because the logic to get the font metrics will not find
539+
# WenQuanYi as the fallback logic stops with the first fallback font:
540+
# "'DejaVu Sans Mono', 'Courier New', 'WenQuanYi Zen Hei', monospace",
541+
# "'DejaVu Sans', 'Arial', 'WenQuanYi Zen Hei', sans-serif",
542+
# "'DejaVu Serif', 'Times New Roman', 'WenQuanYi Zen Hei', serif",
543+
])
544+
@pytest.mark.parametrize("include_generic", [True, False])
545+
def test_svg_font_string(font_str, include_generic):
546+
fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
547+
if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
548+
pytest.skip("Font may be missing")
549+
550+
explicit, *rest, generic = map(
551+
lambda x: x.strip("'"), font_str.split(", ")
552+
)
553+
size = len(generic)
554+
if include_generic:
555+
rest = rest + [generic]
556+
plt.rcParams[f"font.{generic}"] = rest
557+
plt.rcParams["font.size"] = size
558+
plt.rcParams["svg.fonttype"] = "none"
559+
560+
fig, ax = plt.subplots()
561+
if generic == "sans-serif":
562+
generic_options = ["sans", "sans-serif", "sans serif"]
563+
else:
564+
generic_options = [generic]
565+
566+
for generic_name in generic_options:
567+
# test that fallback works
568+
ax.text(0.5, 0.5, "There are 几个汉字 in between!",
569+
family=[explicit, generic_name], ha="center")
570+
# test deduplication works
571+
ax.text(0.5, 0.1, "There are 几个汉字 in between!",
572+
family=[explicit, *rest, generic_name], ha="center")
573+
ax.axis("off")
574+
575+
with BytesIO() as fd:
576+
fig.savefig(fd, format="svg")
577+
buf = fd.getvalue()
578+
579+
tree = xml.etree.ElementTree.fromstring(buf)
580+
ns = "http://www.w3.org/2000/svg"
581+
text_count = 0
582+
for text_element in tree.findall(f".//{{{ns}}}text"):
583+
text_count += 1
584+
font_info = dict(
585+
map(lambda x: x.strip(), _.strip().split(":"))
586+
for _ in dict(text_element.items())["style"].split(";")
587+
)["font"]
588+
589+
assert font_info == f"{size}px {font_str}"
590+
assert text_count == len(ax.texts)

0 commit comments

Comments
 (0)