Skip to content

Commit d56049b

Browse files
committed
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).
1 parent e429603 commit d56049b

File tree

3 files changed

+56
-41
lines changed

3 files changed

+56
-41
lines changed

lib/matplotlib/backends/backend_svg.py

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,13 @@
1414
from PIL import Image
1515

1616
import matplotlib as mpl
17-
from matplotlib import _api, cbook
17+
from matplotlib import _api, cbook, font_manager as fm
1818
from matplotlib.backend_bases import (
1919
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
2020
RendererBase, _no_output_draw)
2121
from matplotlib.backends.backend_mixed import MixedModeRenderer
2222
from matplotlib.colors import rgb2hex
2323
from matplotlib.dates import UTC
24-
from matplotlib.font_manager import findfont, get_font
25-
from matplotlib.ft2font import LOAD_NO_HINTING
2624
from matplotlib.mathtext import MathTextParser
2725
from matplotlib.path import Path
2826
from matplotlib import _path
@@ -94,6 +92,12 @@ def escape_attrib(s):
9492
return s
9593

9694

95+
def _quote_escape_attrib(s):
96+
return ('"' + escape_cdata(s) + '"' if '"' not in s else
97+
"'" + escape_cdata(s) + "'" if "'" not in s else
98+
'"' + escape_attrib(s) + '"')
99+
100+
97101
def short_float_fmt(x):
98102
"""
99103
Create a short string representation of a float, which is %f
@@ -159,8 +163,8 @@ def start(self, tag, attrib={}, **extra):
159163
for k, v in {**attrib, **extra}.items():
160164
if v:
161165
k = escape_cdata(k)
162-
v = escape_attrib(v)
163-
self.__write(' %s="%s"' % (k, v))
166+
v = _quote_escape_attrib(v)
167+
self.__write(' %s=%s' % (k, v))
164168
self.__open = 1
165169
return len(self.__tags) - 1
166170

@@ -262,15 +266,7 @@ def generate_transform(transform_list=[]):
262266

263267

264268
def generate_css(attrib={}):
265-
if attrib:
266-
output = StringIO()
267-
attrib = attrib.items()
268-
for k, v in attrib:
269-
k = escape_attrib(k)
270-
v = escape_attrib(v)
271-
output.write("%s:%s;" % (k, v))
272-
return output.getvalue()
273-
return ''
269+
return '; '.join(f'{k}: {v}' for k, v in attrib.items())
274270

275271

276272
_capstyle_d = {'projecting': 'square', 'butt': 'butt', 'round': 'round'}
@@ -464,8 +460,8 @@ def _make_flip_transform(self, transform):
464460
.translate(0.0, self.height))
465461

466462
def _get_font(self, prop):
467-
fname = findfont(prop)
468-
font = get_font(fname)
463+
fname = fm.findfont(prop)
464+
font = fm.get_font(fname)
469465
font.clear()
470466
size = prop.get_size_in_points()
471467
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):
11281124
style['opacity'] = short_float_fmt(alpha)
11291125

11301126
if not ismath:
1131-
font = self._get_font(prop)
1132-
font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
1133-
11341127
attrib = {}
1135-
style['font-family'] = str(font.family_name)
1136-
style['font-weight'] = str(prop.get_weight()).lower()
1137-
style['font-stretch'] = str(prop.get_stretch()).lower()
1138-
style['font-style'] = prop.get_style().lower()
1139-
# Must add "px" to workaround a Firefox bug
1140-
style['font-size'] = short_float_fmt(prop.get_size()) + 'px'
1128+
1129+
font_parts = []
1130+
if prop.get_style() != 'normal':
1131+
font_parts.append(prop.get_style())
1132+
if prop.get_variant() != 'normal':
1133+
font_parts.append(prop.get_variant())
1134+
weight = fm.weight_dict[prop.get_weight()]
1135+
if weight != 400:
1136+
font_parts.append(f'{weight}')
1137+
font_parts.extend([
1138+
f'{short_float_fmt(prop.get_size())}px',
1139+
f'{prop.get_family()[0]!r}', # ensure quoting
1140+
])
1141+
style['font'] = ' '.join(font_parts)
1142+
if prop.get_stretch() != 'normal':
1143+
style['font-stretch'] = prop.get_stretch()
11411144
attrib['style'] = generate_css(style)
11421145

11431146
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):
11971200
# Sort the characters by font, and output one tspan for each.
11981201
spans = OrderedDict()
11991202
for font, fontsize, thetext, new_x, new_y in glyphs:
1200-
style = generate_css({
1201-
'font-size': short_float_fmt(fontsize) + 'px',
1202-
'font-family': font.family_name,
1203-
'font-style': font.style_name.lower(),
1204-
'font-weight': font.style_name.lower()})
1203+
entry = fm.ttfFontProperty(font)
1204+
font_parts = []
1205+
if entry.style != 'normal':
1206+
font_parts.append(entry.style)
1207+
if entry.variant != 'normal':
1208+
font_parts.append(entry.variant)
1209+
if entry.weight != 400:
1210+
font_parts.append(f'{entry.weight}')
1211+
font_parts.extend([
1212+
f'{short_float_fmt(fontsize)}px',
1213+
f'{entry.name!r}', # ensure quoting
1214+
])
1215+
style = {'font': ' '.join(font_parts)}
1216+
if entry.stretch != 'normal':
1217+
style['font-stretch'] = entry.stretch
1218+
style = generate_css(style)
12051219
if thetext == 32:
12061220
thetext = 0xa0 # non-breaking space
12071221
spans.setdefault(style, []).append((new_x, -new_y, thetext))

lib/matplotlib/tests/test_backend_svg.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def test_unicode_won():
216216

217217

218218
def test_svgnone_with_data_coordinates():
219-
plt.rcParams['svg.fonttype'] = 'none'
219+
plt.rcParams.update({'svg.fonttype': 'none', 'font.stretch': 'condensed'})
220220
expected = 'Unlikely to appear by chance'
221221

222222
fig, ax = plt.subplots()
@@ -229,9 +229,7 @@ def test_svgnone_with_data_coordinates():
229229
fd.seek(0)
230230
buf = fd.read().decode()
231231

232-
assert expected in buf
233-
for prop in ["family", "weight", "stretch", "style", "size"]:
234-
assert f"font-{prop}:" in buf
232+
assert expected in buf and "condensed" in buf
235233

236234

237235
def test_gid():

lib/matplotlib/tests/test_mathtext.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import io
2-
import os
2+
from pathlib import Path
33
import re
4+
import shlex
5+
from xml.etree import ElementTree as ET
46

57
import numpy as np
68
import pytest
@@ -349,7 +351,7 @@ def test_mathtext_fallback_to_cm_invalid():
349351
("stix", ['DejaVu Sans', 'mpltest', 'STIXGeneral'])])
350352
def test_mathtext_fallback(fallback, fontlist):
351353
mpl.font_manager.fontManager.addfont(
352-
os.path.join((os.path.dirname(os.path.realpath(__file__))), 'mpltest.ttf'))
354+
str(Path(__file__).resolve().parent / 'mpltest.ttf'))
353355
mpl.rcParams["svg.fonttype"] = 'none'
354356
mpl.rcParams['mathtext.fontset'] = 'custom'
355357
mpl.rcParams['mathtext.rm'] = 'mpltest'
@@ -363,12 +365,13 @@ def test_mathtext_fallback(fallback, fontlist):
363365
fig, ax = plt.subplots()
364366
fig.text(.5, .5, test_str, fontsize=40, ha='center')
365367
fig.savefig(buff, format="svg")
366-
char_fonts = [
367-
line.split("font-family:")[-1].split(";")[0]
368-
for line in str(buff.getvalue()).split(r"\n") if "tspan" in line
369-
]
368+
tspans = (ET.fromstring(buff.getvalue())
369+
.findall(".//{http://www.w3.org/2000/svg}tspan[@style]"))
370+
# Getting the last element of the style attrib is a close enough
371+
# approximation for parsing the font property.
372+
char_fonts = [shlex.split(tspan.attrib["style"])[-1] for tspan in tspans]
370373
assert char_fonts == fontlist
371-
mpl.font_manager.fontManager.ttflist = mpl.font_manager.fontManager.ttflist[:-1]
374+
mpl.font_manager.fontManager.ttflist.pop()
372375

373376

374377
def test_math_to_image(tmpdir):

0 commit comments

Comments
 (0)
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