|
14 | 14 | from PIL import Image |
15 | 15 |
|
16 | 16 | import matplotlib as mpl |
17 | | -from matplotlib import _api, cbook |
| 17 | +from matplotlib import _api, cbook, font_manager as fm |
18 | 18 | from matplotlib.backend_bases import ( |
19 | 19 | _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, |
20 | 20 | RendererBase, _no_output_draw) |
21 | 21 | from matplotlib.backends.backend_mixed import MixedModeRenderer |
22 | 22 | from matplotlib.colors import rgb2hex |
23 | 23 | from matplotlib.dates import UTC |
24 | | -from matplotlib.font_manager import findfont, get_font |
25 | | -from matplotlib.ft2font import LOAD_NO_HINTING |
26 | 24 | from matplotlib.mathtext import MathTextParser |
27 | 25 | from matplotlib.path import Path |
28 | 26 | from matplotlib import _path |
@@ -94,6 +92,12 @@ def escape_attrib(s): |
94 | 92 | return s |
95 | 93 |
|
96 | 94 |
|
| 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 | + |
97 | 101 | def short_float_fmt(x): |
98 | 102 | """ |
99 | 103 | Create a short string representation of a float, which is %f |
@@ -159,8 +163,8 @@ def start(self, tag, attrib={}, **extra): |
159 | 163 | for k, v in {**attrib, **extra}.items(): |
160 | 164 | if v: |
161 | 165 | 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)) |
164 | 168 | self.__open = 1 |
165 | 169 | return len(self.__tags) - 1 |
166 | 170 |
|
@@ -262,15 +266,7 @@ def generate_transform(transform_list=[]): |
262 | 266 |
|
263 | 267 |
|
264 | 268 | 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()) |
274 | 270 |
|
275 | 271 |
|
276 | 272 | _capstyle_d = {'projecting': 'square', 'butt': 'butt', 'round': 'round'} |
@@ -464,8 +460,8 @@ def _make_flip_transform(self, transform): |
464 | 460 | .translate(0.0, self.height)) |
465 | 461 |
|
466 | 462 | 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) |
469 | 465 | font.clear() |
470 | 466 | size = prop.get_size_in_points() |
471 | 467 | 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): |
1128 | 1124 | style['opacity'] = short_float_fmt(alpha) |
1129 | 1125 |
|
1130 | 1126 | if not ismath: |
1131 | | - font = self._get_font(prop) |
1132 | | - font.set_text(s, 0.0, flags=LOAD_NO_HINTING) |
1133 | | - |
1134 | 1127 | 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() |
1141 | 1144 | attrib['style'] = generate_css(style) |
1142 | 1145 |
|
1143 | 1146 | 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): |
1197 | 1200 | # Sort the characters by font, and output one tspan for each. |
1198 | 1201 | spans = OrderedDict() |
1199 | 1202 | 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) |
1205 | 1219 | if thetext == 32: |
1206 | 1220 | thetext = 0xa0 # non-breaking space |
1207 | 1221 | spans.setdefault(style, []).append((new_x, -new_y, thetext)) |
|
0 commit comments