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

Skip to content

Commit e66a701

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 792fc13 commit e66a701

File tree

3 files changed

+53
-38
lines changed

3 files changed

+53
-38
lines changed

lib/matplotlib/backends/backend_svg.py

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@
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
24+
from matplotlib.font_manager import findfont, get_font, ttfFontProperty
2625
from matplotlib.mathtext import MathTextParser
2726
from matplotlib.path import Path
2827
from matplotlib import _path
@@ -94,6 +93,12 @@ def escape_attrib(s):
9493
return s
9594

9695

96+
def _quote_escape_attrib(s):
97+
return ('"' + escape_cdata(s) + '"' if '"' not in s else
98+
"'" + escape_cdata(s) + "'" if "'" not in s else
99+
'"' + escape_attrib(s) + '"')
100+
101+
97102
def short_float_fmt(x):
98103
"""
99104
Create a short string representation of a float, which is %f
@@ -159,8 +164,8 @@ def start(self, tag, attrib={}, **extra):
159164
for k, v in sorted({**attrib, **extra}.items()):
160165
if v:
161166
k = escape_cdata(k)
162-
v = escape_attrib(v)
163-
self.__write(' %s="%s"' % (k, v))
167+
v = _quote_escape_attrib(v)
168+
self.__write(' %s=%s' % (k, v))
164169
self.__open = 1
165170
return len(self.__tags) - 1
166171

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

263268

264269
def generate_css(attrib={}):
265-
if attrib:
266-
output = StringIO()
267-
attrib = sorted(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 ''
270+
return '; '.join(f'{k}: {v}' for k, v in sorted(attrib.items()))
274271

275272

276273
_capstyle_d = {'projecting': 'square', 'butt': 'butt', 'round': 'round'}
@@ -1128,16 +1125,22 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None):
11281125
style['opacity'] = short_float_fmt(alpha)
11291126

11301127
if not ismath:
1131-
font = self._get_font(prop)
1132-
font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
1133-
11341128
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'
1129+
1130+
font_parts = []
1131+
if prop.get_style() != 'normal':
1132+
font_parts.append(prop.get_style())
1133+
if prop.get_variant() != 'normal':
1134+
font_parts.append(prop.get_variant())
1135+
if prop.get_weight() not in ['normal', 'regular', 400]:
1136+
font_parts.append(prop.get_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 = 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
@@ -328,7 +330,7 @@ def test_mathtext_fallback_to_cm_invalid():
328330
("stix", ['DejaVu Sans', 'mpltest', 'STIXGeneral'])])
329331
def test_mathtext_fallback(fallback, fontlist):
330332
mpl.font_manager.fontManager.addfont(
331-
os.path.join((os.path.dirname(os.path.realpath(__file__))), 'mpltest.ttf'))
333+
str(Path(__file__).resolve().parent / 'mpltest.ttf'))
332334
mpl.rcParams["svg.fonttype"] = 'none'
333335
mpl.rcParams['mathtext.fontset'] = 'custom'
334336
mpl.rcParams['mathtext.rm'] = 'mpltest'
@@ -342,12 +344,13 @@ def test_mathtext_fallback(fallback, fontlist):
342344
fig, ax = plt.subplots()
343345
fig.text(.5, .5, test_str, fontsize=40, ha='center')
344346
fig.savefig(buff, format="svg")
345-
char_fonts = [
346-
line.split("font-family:")[-1].split(";")[0]
347-
for line in str(buff.getvalue()).split(r"\n") if "tspan" in line
348-
]
347+
tspans = (ET.fromstring(buff.getvalue())
348+
.findall(".//{http://www.w3.org/2000/svg}tspan[@style]"))
349+
# Getting the last element of the style attrib is a close enough
350+
# approximation for parsing the font property.
351+
char_fonts = [shlex.split(tspan.attrib["style"])[-1] for tspan in tspans]
349352
assert char_fonts == fontlist
350-
mpl.font_manager.fontManager.ttflist = mpl.font_manager.fontManager.ttflist[:-1]
353+
mpl.font_manager.fontManager.ttflist.pop()
351354

352355

353356
def test_math_to_image(tmpdir):

0 commit comments

Comments
 (0)