From 92b1d40db258c3f055ff26074849e83db9907e4f Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 31 Dec 2020 16:48:12 +0100 Subject: [PATCH] Improve font spec for SVG font referencing. The 'font: ...' shorthand is much more concise than setting each property separately: 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... Also this revealed a bug in generate_css (we shouldn't run it through escape_attrib, as quotes (e.g. around the font family name) get mangled); and we don't need to load the font at all (we should just report whatever font the user actually requested). --- lib/matplotlib/backends/backend_svg.py | 74 ++++++++++++++---------- lib/matplotlib/tests/test_backend_svg.py | 6 +- lib/matplotlib/tests/test_mathtext.py | 17 +++--- 3 files changed, 56 insertions(+), 41 deletions(-) diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index cffee2552828..29e94d936c92 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -14,15 +14,13 @@ from PIL import Image import matplotlib as mpl -from matplotlib import _api, cbook +from matplotlib import _api, cbook, font_manager as fm from matplotlib.backend_bases import ( _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, RendererBase, _no_output_draw) 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.ft2font import LOAD_NO_HINTING from matplotlib.mathtext import MathTextParser from matplotlib.path import Path from matplotlib import _path @@ -94,6 +92,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 +163,8 @@ def start(self, tag, attrib={}, **extra): for k, v in {**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 @@ -262,15 +266,7 @@ def generate_transform(transform_list=[]): def generate_css(attrib={}): - if attrib: - output = StringIO() - attrib = attrib.items() - for k, v in attrib: - k = escape_attrib(k) - v = escape_attrib(v) - output.write("%s:%s;" % (k, v)) - return output.getvalue() - return '' + return "; ".join(f"{k}: {v}" for k, v in attrib.items()) _capstyle_d = {'projecting': 'square', 'butt': 'butt', 'round': 'round'} @@ -464,8 +460,8 @@ def _make_flip_transform(self, transform): .translate(0.0, self.height)) def _get_font(self, prop): - fname = findfont(prop) - font = get_font(fname) + fname = fm.findfont(prop) + font = fm.get_font(fname) font.clear() size = prop.get_size_in_points() font.set_size(size, 72.0) @@ -1128,16 +1124,23 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): style['opacity'] = short_float_fmt(alpha) if not ismath: - font = self._get_font(prop) - font.set_text(s, 0.0, flags=LOAD_NO_HINTING) - attrib = {} - style['font-family'] = str(font.family_name) - style['font-weight'] = str(prop.get_weight()).lower() - style['font-stretch'] = str(prop.get_stretch()).lower() - style['font-style'] = prop.get_style().lower() - # Must add "px" to workaround a Firefox bug - style['font-size'] = short_float_fmt(prop.get_size()) + 'px' + + font_parts = [] + if prop.get_style() != 'normal': + font_parts.append(prop.get_style()) + if prop.get_variant() != 'normal': + font_parts.append(prop.get_variant()) + weight = fm.weight_dict[prop.get_weight()] + if weight != 400: + font_parts.append(f'{weight}') + font_parts.extend([ + f'{short_float_fmt(prop.get_size())}px', + f'{prop.get_family()[0]!r}', # ensure quoting + ]) + style['font'] = ' '.join(font_parts) + if prop.get_stretch() != 'normal': + style['font-stretch'] = prop.get_stretch() attrib['style'] = generate_css(style) if mtext and (angle == 0 or mtext.get_rotation_mode() == "anchor"): @@ -1197,11 +1200,22 @@ 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 = fm.ttfFontProperty(font) + font_parts = [] + 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 + ]) + style = {'font': ' '.join(font_parts)} + if entry.stretch != 'normal': + style['font-stretch'] = entry.stretch + style = generate_css(style) if thetext == 32: thetext = 0xa0 # non-breaking space spans.setdefault(style, []).append((new_x, -new_y, thetext)) diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index 66ec48a477bf..f3dfadff93df 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -216,7 +216,7 @@ def test_unicode_won(): def test_svgnone_with_data_coordinates(): - plt.rcParams['svg.fonttype'] = 'none' + plt.rcParams.update({'svg.fonttype': 'none', 'font.stretch': 'condensed'}) expected = 'Unlikely to appear by chance' fig, ax = plt.subplots() @@ -229,9 +229,7 @@ def test_svgnone_with_data_coordinates(): fd.seek(0) buf = fd.read().decode() - assert expected in buf - for prop in ["family", "weight", "stretch", "style", "size"]: - assert f"font-{prop}:" in buf + assert expected in buf and "condensed" in buf def test_gid(): diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index b5fd906d2f9c..c6d3d32de460 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 @@ -349,7 +351,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' @@ -363,12 +365,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): 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