From 5fd8ae8509338a2ebb755f82d527c28cd631f417 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 31 Dec 2020 16:48:12 +0100 Subject: [PATCH 1/3] Improve font spec for SVG font referencing. This replaces e.g. `"font-family:DejaVu Sans;font-size:12px;font-style:book;font-weight:book;"` by `"font: 400 12px 'DejaVu Sans'"`. Note that the previous font weight was plain wrong... --- lib/matplotlib/backends/backend_svg.py | 32 +++++++++++++++++++------- lib/matplotlib/tests/test_mathtext.py | 17 ++++++++------ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index ce6ad0c115f8..468181ae49e9 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -21,7 +21,7 @@ from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.colors import rgb2hex from matplotlib.dates import UTC -from matplotlib.font_manager import findfont, get_font +from matplotlib.font_manager import findfont, get_font, ttfFontProperty from matplotlib.ft2font import LOAD_NO_HINTING from matplotlib.mathtext import MathTextParser from matplotlib.path import Path @@ -94,6 +94,12 @@ def escape_attrib(s): return s +def _quote_escape_attrib(s): + return ('"' + escape_cdata(s) + '"' if '"' not in s else + "'" + escape_cdata(s) + "'" if "'" not in s else + '"' + escape_attrib(s) + '"') + + def short_float_fmt(x): """ Create a short string representation of a float, which is %f @@ -159,8 +165,8 @@ def start(self, tag, attrib={}, **extra): for k, v in sorted({**attrib, **extra}.items()): if v: k = escape_cdata(k) - v = escape_attrib(v) - self.__write(' %s="%s"' % (k, v)) + v = _quote_escape_attrib(v) + self.__write(' %s=%s' % (k, v)) self.__open = 1 return len(self.__tags) - 1 @@ -1197,11 +1203,21 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): # Sort the characters by font, and output one tspan for each. spans = OrderedDict() for font, fontsize, thetext, new_x, new_y in glyphs: - style = generate_css({ - 'font-size': short_float_fmt(fontsize) + 'px', - 'font-family': font.family_name, - 'font-style': font.style_name.lower(), - 'font-weight': font.style_name.lower()}) + entry = ttfFontProperty(font) + font_parts = ['font:'] + if entry.style != 'normal': + font_parts.append(entry.style) + if entry.variant != 'normal': + font_parts.append(entry.variant) + if entry.weight != 400: + font_parts.append(f'{entry.weight}') + font_parts.extend([ + f'{short_float_fmt(fontsize)}px', + f'{entry.name!r}', # ensure quoting + ]) + if entry.stretch != 'normal': + font_parts.extend(['; font-stretch:', entry.stretch]) + style = ' '.join(font_parts) if thetext == 32: thetext = 0xa0 # non-breaking space spans.setdefault(style, []).append((new_x, -new_y, thetext)) diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index 8c5250ac5021..ef072c04dbbb 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -1,6 +1,8 @@ import io -import os +from pathlib import Path import re +import shlex +from xml.etree import ElementTree as ET import numpy as np import pytest @@ -328,7 +330,7 @@ def test_mathtext_fallback_to_cm_invalid(): ("stix", ['DejaVu Sans', 'mpltest', 'STIXGeneral'])]) def test_mathtext_fallback(fallback, fontlist): mpl.font_manager.fontManager.addfont( - os.path.join((os.path.dirname(os.path.realpath(__file__))), 'mpltest.ttf')) + str(Path(__file__).resolve().parent / 'mpltest.ttf')) mpl.rcParams["svg.fonttype"] = 'none' mpl.rcParams['mathtext.fontset'] = 'custom' mpl.rcParams['mathtext.rm'] = 'mpltest' @@ -342,12 +344,13 @@ def test_mathtext_fallback(fallback, fontlist): fig, ax = plt.subplots() fig.text(.5, .5, test_str, fontsize=40, ha='center') fig.savefig(buff, format="svg") - char_fonts = [ - line.split("font-family:")[-1].split(";")[0] - for line in str(buff.getvalue()).split(r"\n") if "tspan" in line - ] + tspans = (ET.fromstring(buff.getvalue()) + .findall(".//{http://www.w3.org/2000/svg}tspan[@style]")) + # Getting the last element of the style attrib is a close enough + # approximation for parsing the font property. + char_fonts = [shlex.split(tspan.attrib["style"])[-1] for tspan in tspans] assert char_fonts == fontlist - mpl.font_manager.fontManager.ttflist = mpl.font_manager.fontManager.ttflist[:-1] + mpl.font_manager.fontManager.ttflist.pop() def test_math_to_image(tmpdir): From 15f6905826839c1c1d012f1326a8fa13d516b88f Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 5 Jan 2021 07:23:22 +0100 Subject: [PATCH 2/3] Add machinery for svg-only mathtext tests. --- lib/matplotlib/testing/compare.py | 19 +++++++++++++++-- lib/matplotlib/tests/test_mathtext.py | 30 +++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index fb2e55880cde..a91c67ad8d81 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -181,7 +181,7 @@ def __call__(self, orig, dest): # just be reported as a regular exception below). "DISPLAY": "", # Do not load any user options. - "INKSCAPE_PROFILE_DIR": os.devnull, + "INKSCAPE_PROFILE_DIR": self._tmpdir.name, } # Old versions of Inkscape (e.g. 0.48.3.1) seem to sometimes # deadlock when stderr is redirected to a pipe, so we redirect it @@ -233,6 +233,15 @@ def __del__(self): self._tmpdir.cleanup() +class _SVGWithMatplotlibFontsConverter(_SVGConverter): + def __call__(self, orig, dest): + if not hasattr(self, "_tmpdir"): + self._tmpdir = TemporaryDirectory() + shutil.copytree(cbook._get_data_path("fonts/ttf"), + Path(self._tmpdir.name, "fonts")) + return super().__call__(orig, dest) + + def _update_converter(): try: mpl._get_executable_info("gs") @@ -254,6 +263,7 @@ def _update_converter(): #: extension to png format. converter = {} _update_converter() +_svg_with_matplotlib_fonts_converter = _SVGWithMatplotlibFontsConverter() def comparable_formats(): @@ -303,7 +313,12 @@ def convert(filename, cache): return str(newpath) _log.debug("For %s: converting to png.", filename) - converter[path.suffix[1:]](path, newpath) + convert = converter[path.suffix[1:]] + if path.suffix == ".svg": + contents = path.read_text() + if 'style="font:' in contents: # for svg.fonttype = none. + convert = _svg_with_matplotlib_fonts_converter + convert(path, newpath) if cache_dir is not None: _log.debug("For %s: caching conversion result.", filename) diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index ef072c04dbbb..35c43453ceaa 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -112,6 +112,8 @@ r'$\left(X\right)_{a}^{b}$', # github issue 7615 r'$\dfrac{\$100.00}{y}$', # github issue #1888 ] +svg_only_math_tests = [ +] digits = "0123456789" uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -167,8 +169,6 @@ for set in chars: font_tests.append(wrapper % set) -font_tests = list(filter(lambda x: x[1] is not None, enumerate(font_tests))) - @pytest.fixture def baseline_images(request, fontset, index): @@ -192,6 +192,32 @@ def test_mathtext_rendering(baseline_images, fontset, index, test): horizontalalignment='center', verticalalignment='center') +cur_svg_only_math_tests = list( + filter(lambda x: x[1] is not None, enumerate(svg_only_math_tests))) + + +@pytest.mark.parametrize('index, test', cur_svg_only_math_tests, + ids=[str(idx) for idx, _ in cur_svg_only_math_tests]) +@pytest.mark.parametrize('fontset', ['all']) +@pytest.mark.parametrize('baseline_images', ['mathtext1'], indirect=True) +@image_comparison( + baseline_images=None, extensions=['svg'], + savefig_kwarg={ + 'metadata': { # Minimize svg size. + 'Creator': None, 'Date': None, 'Format': None, 'Type': None}}) +def test_mathtext_rendering_svg_only(baseline_images, fontset, index, test): + mpl.rcParams['svg.fonttype'] = 'none' + fig = plt.figure(figsize=(5.25, 5.25)) + fig.patch.set_visible(False) # Minimize svg size. + fontsets = ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif'] + for i, fontset in enumerate(fontsets): + fig.text(0.5, (i + .5) / len(fontsets), test, math_fontfamily=fontset, + horizontalalignment='center', verticalalignment='center') + + +font_tests = list(filter(lambda x: x[1] is not None, enumerate(font_tests))) + + @pytest.mark.parametrize('index, test', font_tests, ids=[str(index) for index, _ in font_tests]) @pytest.mark.parametrize('fontset', From f9cea50f40fec605670c7cf8f4844696a5c1a711 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 5 Jan 2021 07:24:03 +0100 Subject: [PATCH 3/3] Support \sqrt[not-a-number]{...}. --- lib/matplotlib/_mathtext.py | 7 ++- .../test_mathtext/mathtext1_all_00.svg | 63 +++++++++++++++++++ lib/matplotlib/tests/test_mathtext.py | 1 + 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_mathtext/mathtext1_all_00.svg diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 96a69919c86b..45e51f53984d 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -2158,7 +2158,8 @@ def __init__(self): p.sqrt <<= Group( Suppress(Literal(r"\sqrt")) - - ((Optional(p.lbracket + p.int_literal + p.rbracket, default=None) + - ((Group(Optional( + p.lbracket + OneOrMore(~p.rbracket + p.token) + p.rbracket)) + p.required_group) | Error("Expected \\sqrt{value}")) ) @@ -2864,10 +2865,10 @@ def sqrt(self, s, loc, toks): # Add the root and shift it upward so it is above the tick. # The value of 0.6 is a hard-coded hack ;) - if root is None: + if not root: root = Box(check.width * 0.5, 0., 0.) else: - root = Hlist([Char(x, state) for x in root]) + root = Hlist(root) root.shrink() root.shrink() diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext1_all_00.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext1_all_00.svg new file mode 100644 index 000000000000..4884f97924ed --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext1_all_00.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + ab + p + 123 + + + + + + + + + ab + √123 + + + + + + + + + 𝘢𝘣 + √𝟣𝟤𝟥 + + + + + + + + + ab + + 123 + + + + + + + + + ab + + 123 + + + + + + diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index 35c43453ceaa..88184e37218d 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -113,6 +113,7 @@ r'$\dfrac{\$100.00}{y}$', # github issue #1888 ] svg_only_math_tests = [ + r'$\sqrt[ab]{123}$', # github issue #8665 ] digits = "0123456789"