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)
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