From a6f15f26dd6c1e2c6a385846aaac48aa30574988 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 16 Aug 2022 22:28:48 -0400 Subject: [PATCH 1/3] TST: add a test of font families in svg text-as-text mode This tests: 1. all of the fonts from the generic lists Matplotlib maintains ends up in the svg 2. the generic family is included and un-escaped 3. the specific fonts are escaped 4. glyph fallback will happen in fonts found via generic family names --- lib/matplotlib/tests/test_backend_svg.py | 61 ++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index 7ea81730f20d..680efd67379b 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -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) From a0fcf79db9306352e084b734519e30390d40f9a5 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 16 Aug 2022 11:40:19 -0400 Subject: [PATCH 2/3] FIX: correctly handle generic font families in svg text-as-text mode This: - expands the generic fonts families to the user configured specific fonts in the svg output - ensures that each font family only appears once per text element in the svg output - ensures that generic families are unquoted and specific families are closes #22528 closes #23492 --- lib/matplotlib/backends/backend_svg.py | 42 ++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index bc0f2565330a..721fb43ee6c6 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -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': From 6e99a524ac5c31498308b3d2675303aa1acdb5b0 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 31 Aug 2022 20:52:07 -0400 Subject: [PATCH 3/3] FIX: do not raise in lru_cached function If the cached function raises it will not be cached and we will continuously pay for cache misses. --- lib/matplotlib/font_manager.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 99e1fecabbeb..a5742ef88f61 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -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.""" @@ -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) @@ -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) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy