Thanks to visit codestin.com
Credit goes to github.com

Skip to content

FIX: correctly handle generic font families in svg text-as-text mode #23638

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions lib/matplotlib/backends/backend_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1153,10 +1153,48 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None):
weight = fm.weight_dict[prop.get_weight()]
if weight != 400:
font_parts.append(f'{weight}')

def _format_font_name(fn):
normalize_names = {
'sans': 'sans-serif',
'sans serif': 'sans-serif'
}
# A generic font family. We need to do two things:
# 1. list all of the configured fonts with quoted names
# 2. append the generic name unquoted
if fn in fm.font_family_aliases:
# fix spelling of sans-serif
# we accept 3 ways CSS only supports 1
fn = normalize_names.get(fn, fn)
# get all of the font names and fix spelling of sans-serif
# if it comes back
aliases = [
normalize_names.get(_, _) for _ in
fm.FontManager._expand_aliases(fn)
]
# make sure the generic name appears at least once
# duplicate is OK, next layer will deduplicate
aliases.append(fn)

for a in aliases:
# generic font families must not be quoted
if a in fm.font_family_aliases:
yield a
# specific font families must be quoted
else:
yield repr(a)
# specific font families must be quoted
else:
yield repr(fn)

def _get_all_names(prop):
for f in prop.get_family():
yield from _format_font_name(f)

font_parts.extend([
f'{_short_float_fmt(prop.get_size())}px',
# ensure quoting
f'{", ".join(repr(f) for f in prop.get_family())}',
# ensure quoting and expansion of font names
", ".join(dict.fromkeys(_get_all_names(prop)))
])
style['font'] = ' '.join(font_parts)
if prop.get_stretch() != 'normal':
Expand Down
17 changes: 13 additions & 4 deletions lib/matplotlib/font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1345,9 +1345,12 @@ def findfont(self, prop, fontext='ttf', directory=None,
rc_params = tuple(tuple(mpl.rcParams[key]) for key in [
"font.serif", "font.sans-serif", "font.cursive", "font.fantasy",
"font.monospace"])
return self._findfont_cached(
ret = self._findfont_cached(
prop, fontext, directory, fallback_to_default, rebuild_if_missing,
rc_params)
if isinstance(ret, Exception):
raise ret
return ret

def get_font_names(self):
"""Return the list of available fonts."""
Expand Down Expand Up @@ -1496,8 +1499,11 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
return self.findfont(default_prop, fontext, directory,
fallback_to_default=False)
else:
raise ValueError(f"Failed to find font {prop}, and fallback "
f"to the default font was disabled")
# This return instead of raise is intentional, as we wish to
# cache the resulting exception, which will not occur if it was
# actually raised.
return ValueError(f"Failed to find font {prop}, and fallback "
f"to the default font was disabled")
else:
_log.debug('findfont: Matching %s to %s (%r) with score of %f.',
prop, best_font.name, best_font.fname, best_score)
Expand All @@ -1516,7 +1522,10 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default,
return self.findfont(
prop, fontext, directory, rebuild_if_missing=False)
else:
raise ValueError("No valid font could be found")
# This return instead of raise is intentional, as we wish to
# cache the resulting exception, which will not occur if it was
# actually raised.
return ValueError("No valid font could be found")

return _cached_realpath(result)

Expand Down
61 changes: 61 additions & 0 deletions lib/matplotlib/tests/test_backend_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,64 @@ def test_svg_escape():
fig.savefig(fd, format='svg')
buf = fd.getvalue().decode()
assert '<'"&>"' in buf


@pytest.mark.parametrize("font_str", [
"'DejaVu Sans', 'WenQuanYi Zen Hei', 'Arial', sans-serif",
"'DejaVu Serif', 'WenQuanYi Zen Hei', 'Times New Roman', serif",
"'Arial', 'WenQuanYi Zen Hei', cursive",
"'Impact', 'WenQuanYi Zen Hei', fantasy",
"'DejaVu Sans Mono', 'WenQuanYi Zen Hei', 'Courier New', monospace",
# These do not work because the logic to get the font metrics will not find
# WenQuanYi as the fallback logic stops with the first fallback font:
# "'DejaVu Sans Mono', 'Courier New', 'WenQuanYi Zen Hei', monospace",
# "'DejaVu Sans', 'Arial', 'WenQuanYi Zen Hei', sans-serif",
# "'DejaVu Serif', 'Times New Roman', 'WenQuanYi Zen Hei', serif",
])
@pytest.mark.parametrize("include_generic", [True, False])
def test_svg_font_string(font_str, include_generic):
fp = fm.FontProperties(family=["WenQuanYi Zen Hei"])
if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc":
pytest.skip("Font may be missing")

explicit, *rest, generic = map(
lambda x: x.strip("'"), font_str.split(", ")
)
size = len(generic)
if include_generic:
rest = rest + [generic]
plt.rcParams[f"font.{generic}"] = rest
plt.rcParams["font.size"] = size
plt.rcParams["svg.fonttype"] = "none"

fig, ax = plt.subplots()
if generic == "sans-serif":
generic_options = ["sans", "sans-serif", "sans serif"]
else:
generic_options = [generic]

for generic_name in generic_options:
# test that fallback works
ax.text(0.5, 0.5, "There are 几个汉字 in between!",
family=[explicit, generic_name], ha="center")
# test deduplication works
ax.text(0.5, 0.1, "There are 几个汉字 in between!",
family=[explicit, *rest, generic_name], ha="center")
ax.axis("off")

with BytesIO() as fd:
fig.savefig(fd, format="svg")
buf = fd.getvalue()

tree = xml.etree.ElementTree.fromstring(buf)
ns = "http://www.w3.org/2000/svg"
text_count = 0
for text_element in tree.findall(f".//{{{ns}}}text"):
text_count += 1
font_info = dict(
map(lambda x: x.strip(), _.strip().split(":"))
for _ in dict(text_element.items())["style"].split(";")
)["font"]

assert font_info == f"{size}px {font_str}"
assert text_count == len(ax.texts)