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

Skip to content

Commit ed32a04

Browse files
authored
Merge pull request #19253 from anntzer/svgfontspec
Improve font spec for SVG font referencing.
2 parents 78e3b5f + 92b1d40 commit ed32a04

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
@@ -13,15 +13,13 @@
1313
from PIL import Image
1414

1515
import matplotlib as mpl
16-
from matplotlib import _api, cbook
16+
from matplotlib import _api, cbook, font_manager as fm
1717
from matplotlib.backend_bases import (
1818
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
1919
RendererBase)
2020
from matplotlib.backends.backend_mixed import MixedModeRenderer
2121
from matplotlib.colors import rgb2hex
2222
from matplotlib.dates import UTC
23-
from matplotlib.font_manager import findfont, get_font
24-
from matplotlib.ft2font import LOAD_NO_HINTING
2523
from matplotlib.mathtext import MathTextParser
2624
from matplotlib.path import Path
2725
from matplotlib import _path
@@ -93,6 +91,12 @@ def escape_attrib(s):
9391
return s
9492

9593

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

@@ -261,15 +265,7 @@ def generate_transform(transform_list=[]):
261265

262266

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

274270

275271
_capstyle_d = {'projecting': 'square', 'butt': 'butt', 'round': 'round'}
@@ -462,8 +458,8 @@ def _make_flip_transform(self, transform):
462458
.translate(0.0, self.height))
463459

464460
def _get_font(self, prop):
465-
fname = findfont(prop)
466-
font = get_font(fname)
461+
fname = fm.findfont(prop)
462+
font = fm.get_font(fname)
467463
font.clear()
468464
size = prop.get_size_in_points()
469465
font.set_size(size, 72.0)
@@ -1106,16 +1102,23 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None):
11061102
style['opacity'] = short_float_fmt(alpha)
11071103

11081104
if not ismath:
1109-
font = self._get_font(prop)
1110-
font.set_text(s, 0.0, flags=LOAD_NO_HINTING)
1111-
11121105
attrib = {}
1113-
style['font-family'] = str(font.family_name)
1114-
style['font-weight'] = str(prop.get_weight()).lower()
1115-
style['font-stretch'] = str(prop.get_stretch()).lower()
1116-
style['font-style'] = prop.get_style().lower()
1117-
# Must add "px" to workaround a Firefox bug
1118-
style['font-size'] = short_float_fmt(prop.get_size()) + 'px'
1106+
1107+
font_parts = []
1108+
if prop.get_style() != 'normal':
1109+
font_parts.append(prop.get_style())
1110+
if prop.get_variant() != 'normal':
1111+
font_parts.append(prop.get_variant())
1112+
weight = fm.weight_dict[prop.get_weight()]
1113+
if weight != 400:
1114+
font_parts.append(f'{weight}')
1115+
font_parts.extend([
1116+
f'{short_float_fmt(prop.get_size())}px',
1117+
f'{prop.get_family()[0]!r}', # ensure quoting
1118+
])
1119+
style['font'] = ' '.join(font_parts)
1120+
if prop.get_stretch() != 'normal':
1121+
style['font-stretch'] = prop.get_stretch()
11191122
attrib['style'] = generate_css(style)
11201123

11211124
if mtext and (angle == 0 or mtext.get_rotation_mode() == "anchor"):
@@ -1175,11 +1178,22 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None):
11751178
# Sort the characters by font, and output one tspan for each.
11761179
spans = {}
11771180
for font, fontsize, thetext, new_x, new_y in glyphs:
1178-
style = generate_css({
1179-
'font-size': short_float_fmt(fontsize) + 'px',
1180-
'font-family': font.family_name,
1181-
'font-style': font.style_name.lower(),
1182-
'font-weight': font.style_name.lower()})
1181+
entry = fm.ttfFontProperty(font)
1182+
font_parts = []
1183+
if entry.style != 'normal':
1184+
font_parts.append(entry.style)
1185+
if entry.variant != 'normal':
1186+
font_parts.append(entry.variant)
1187+
if entry.weight != 400:
1188+
font_parts.append(f'{entry.weight}')
1189+
font_parts.extend([
1190+
f'{short_float_fmt(fontsize)}px',
1191+
f'{entry.name!r}', # ensure quoting
1192+
])
1193+
style = {'font': ' '.join(font_parts)}
1194+
if entry.stretch != 'normal':
1195+
style['font-stretch'] = entry.stretch
1196+
style = generate_css(style)
11831197
if thetext == 32:
11841198
thetext = 0xa0 # non-breaking space
11851199
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
@@ -343,7 +345,7 @@ def test_mathtext_fallback_invalid():
343345
("stix", ['DejaVu Sans', 'mpltest', 'STIXGeneral'])])
344346
def test_mathtext_fallback(fallback, fontlist):
345347
mpl.font_manager.fontManager.addfont(
346-
os.path.join((os.path.dirname(os.path.realpath(__file__))), 'mpltest.ttf'))
348+
str(Path(__file__).resolve().parent / 'mpltest.ttf'))
347349
mpl.rcParams["svg.fonttype"] = 'none'
348350
mpl.rcParams['mathtext.fontset'] = 'custom'
349351
mpl.rcParams['mathtext.rm'] = 'mpltest'
@@ -357,12 +359,13 @@ def test_mathtext_fallback(fallback, fontlist):
357359
fig, ax = plt.subplots()
358360
fig.text(.5, .5, test_str, fontsize=40, ha='center')
359361
fig.savefig(buff, format="svg")
360-
char_fonts = [
361-
line.split("font-family:")[-1].split(";")[0]
362-
for line in str(buff.getvalue()).split(r"\n") if "tspan" in line
363-
]
362+
tspans = (ET.fromstring(buff.getvalue())
363+
.findall(".//{http://www.w3.org/2000/svg}tspan[@style]"))
364+
# Getting the last element of the style attrib is a close enough
365+
# approximation for parsing the font property.
366+
char_fonts = [shlex.split(tspan.attrib["style"])[-1] for tspan in tspans]
364367
assert char_fonts == fontlist
365-
mpl.font_manager.fontManager.ttflist = mpl.font_manager.fontManager.ttflist[:-1]
368+
mpl.font_manager.fontManager.ttflist.pop()
366369

367370

368371
def test_math_to_image(tmpdir):

0 commit comments

Comments
 (0)