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

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)