8000 Merge pull request #13841 from meeseeksmachine/auto-backport-of-pr-12… · matplotlib/matplotlib@9f39678 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9f39678

Browse files
authored
Merge pull request #13841 from meeseeksmachine/auto-backport-of-pr-12928-on-v3.1.x
Backport PR #12928 on branch v3.1.x (textpath encoding)
2 parents 985116f + 4ffbeeb commit 9f39678

File tree

3 files changed

+104
-63
lines changed

3 files changed

+104
-63
lines changed

lib/matplotlib/dviread.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -968,10 +968,41 @@ def _parse(file):
968968
raise ValueError("Cannot locate end of encoding in {}"
969969
.format(file))
970970
data = data[:end]
971-
972971
return re.findall(br'/([^][{}<>\s]+)', data)
973972

974973

974+
# Note: this function should ultimately replace the Encoding class, which
975+
# appears to be mostly broken: because it uses b''.join(), there is no
976+
# whitespace left between glyph names (only slashes) so the final re.findall
977+
# returns a single string with all glyph names. However this does not appear
978+
# to bother backend_pdf, so that needs to be investigated more. (The fixed
979+
# version below is necessary for textpath/backend_svg, though.)
980+
def _parse_enc(path):
981+
r"""
982+
Parses a \*.enc file referenced from a psfonts.map style file.
983+
The format this class understands is a very limited subset of PostScript.
984+
985+
Parameters
986+
----------
987+
path : os.PathLike
988+
989+
Returns
990+
-------
991+
encoding : list
992+
The nth entry of the list is the PostScript glyph name of the nth
993+
glyph.
994+
"""
995+
with open(path, encoding="ascii") as file:
996+
no_comments = "\n".join(line.split("%")[0].rstrip() for line in file)
997+
array = re.search(r"(?s)\[(.*)\]", no_comments).group(1)
998+
lines = [line for line in array.split("\n") if line]
999+
if all(line.startswith("/") for line in lines):
1000+
return [line[1:] for line in lines]
1001+
else:
1002+
raise ValueError(
1003+
"Failed to parse {} as Postscript encoding".format(path))
1004+
1005+
9751006
@lru_cache()
9761007
def find_tex_file(filename, format=None):
9771008
"""

lib/matplotlib/tests/test_backend_svg.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import numpy as np
22
from io import BytesIO
33
import os
4+
import re
45
import tempfile
56
import warnings
67
import xml.parsers.expat
78

89
import pytest
910

11+
import matplotlib as mpl
12+
from matplotlib import dviread
13+
from matplotlib.figure import Figure
1014
import matplotlib.pyplot as plt
1115
from matplotlib.testing.decorators import image_comparison
12-
import matplotlib
13-
from matplotlib import dviread
1416

1517

1618
with warnings.catch_warnings():
1719
warnings.simplefilter('ignore')
1820
needs_usetex = pytest.mark.skipif(
19-
not matplotlib.checkdep_usetex(True),
21+
not mpl.checkdep_usetex(True),
2022
reason="This test needs a TeX installation")
2123

2224

@@ -107,15 +109,10 @@ def test_bold_font_output_with_none_fonttype():
107109

108110
def _test_determinism_save(filename, usetex):
109111
# This function is mostly copy&paste from "def test_visibility"
110-
# To require no GUI, we use Figure and FigureCanvasSVG
111-
# instead of plt.figure and fig.savefig
112-
from matplotlib.figure import Figure
113-
from matplotlib.backends.backend_svg import FigureCanvasSVG
114-
from matplotlib import rc
115-
rc('svg', hashsalt='asdf')
116-
rc('text', usetex=usetex)
112+
mpl.rc('svg', hashsalt='asdf')
113+
mpl.rc('text', usetex=usetex)
117114

118-
fig = Figure()
115+
fig = Figure() # Require no GUI.
119116
ax = fig.add_subplot(111)
120117

121118
x = np.linspace(0, 4 * np.pi, 50)
@@ -129,7 +126,7 @@ def _test_determinism_save(filename, usetex):
129126
ax.set_xlabel('A string $1+2+\\sigma$')
130127
ax.set_ylabel('A string $1+2+\\sigma$')
131128

132-
FigureCanvasSVG(fig).print_svg(filename)
129+
fig.savefig(filename, format="svg")
133130

134131

135132
@pytest.mark.parametrize(
@@ -172,15 +169,30 @@ def test_determinism(filename, usetex):
172169
@needs_usetex
173170
def test_missing_psfont(monkeypatch):
174171
"""An error is raised if a TeX font lacks a Type-1 equivalent"""
175-
from matplotlib import rc
176172

177173
def psfont(*args, **kwargs):
178174
return dviread.PsFont(texname='texfont', psname='Some Font',
179175
effects=None, encoding=None, filename=None)
180176

181177
monkeypatch.setattr(dviread.PsfontsMap, '__getitem__', psfont)
182-
rc('text', usetex=True)
178+
mpl.rc('text', usetex=True)
183179
fig, ax = plt.subplots()
184180
ax.text(0.5, 0.5, 'hello')
185181
with tempfile.TemporaryFile() as tmpfile, pytest.raises(ValueError):
186182
fig.savefig(tmpfile, format='svg')
183+
184+
185+
# Use Computer Modern Sans Serif, not Helvetica (which has no \textwon).
186+
@pytest.mark.style('default')
187+
@needs_usetex
188+
def test_unicode_won():
189+
fig = Figure()
190+
fig.text(.5, .5, r'\textwon', usetex=True)
191+
192+
with BytesIO() as fd:
193+
fig.savefig(fd, format='svg')
194+
buf = fd.getvalue().decode('ascii')
195+
196+
won_id = 'Computer_Modern_Sans_Serif-142'
197+
assert re.search(r'<path d=(.|\s)*?id="{0}"/>'.format(won_id), buf)
198+
assert re.search(r'<use[^/>]*? xlink:href="#{0}"/>'.format(won_id), buf)

lib/matplotlib/textpath.py

Lines changed: 46 additions & 48 deletions
< 1241 tr class="diff-line-row">
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,6 @@
1616
_log = logging.getLogger(__name__)
1717

1818

19-
@functools.lru_cache(1)
20-
def _get_adobe_standard_encoding():
21-
enc_name = dviread.find_tex_file('8a.enc')
22-
enc = dviread.Encoding(enc_name)
23-
return {c: i for i, c in enumerate(enc.encoding)}
24-
25-
2619
class TextToPath(object):
2720
"""A class that converts strings to paths."""
2821

@@ -291,12 +284,8 @@ def get_texmanager(self):
291284

292285
def get_glyphs_tex(self, prop, s, glyph_map=None,
293286
return_new_glyphs_only=False):
294-
"""
295-
Process string *s* with usetex and convert it to a (vertices, codes)
296-
pair.
297-
"""
298-
299-
# Implementation mostly borrowed from pdf backend.
287+
"""Convert the string *s* to vertices and codes using usetex mode."""
288+
# Mostly borrowed from pdf backend.
300289

301290
dvifile = self.get_texmanager().make_dvi(s, self.FONT_SCALE)
302291
with dviread.Dvi(dvifile, self.DPI) as dvi:
@@ -321,21 +310,20 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
321310
if char_id not in glyph_map:
322311
font.clear()
323312
font.set_size(self.FONT_SCALE, self.DPI)
324-
if enc:
325-
charcode = enc.get(glyph, None)
326-
else:
327-
charcode = glyph
328-
329-
ft2font_flag = LOAD_TARGET_LIGHT
330-
if charcode is not None:
331-
glyph0 = font.load_char(charcode, flags=ft2font_flag)
313+
# See comments in _get_ps_font_and_encoding.
314+
if enc is not None:
315+
if glyph not in enc:
316+
_log.warning(
317+
"The glyph %d of font %s cannot be converted with "
318+
"the encoding; glyph may be wrong.",
319+
glyph, font.fname)
320+
font.load_char(glyph, flags=LOAD_TARGET_LIGHT)
321+
else:
322+
index = font.get_name_index(enc[glyph])
323+
font.load_glyph(index, flags=LOAD_TARGET_LIGHT)
332324
else:
333-
_log.warning("The glyph (%d) of font (%s) cannot be "
334-
"converted with the encoding. Glyph may "
335-
"be wrong.", glyph, font.fname)
336-
337-
glyph0 = font.load_char(glyph, flags=ft2font_flag)
338-
325+
index = glyph
326+
font.load_char(index, flags=LOAD_TARGET_LIGHT)
339327
glyph_map_new[char_id] = font.get_path()
340328

341329
glyph_ids.append(char_id)
@@ -363,31 +351,41 @@ def _get_ps_font_and_encoding(texname):
363351
font_bunch = tex_font_map[texname]
364352
if font_bunch.filename is None:
365353
raise ValueError(
366-
("No usable font file found for %s (%s). "
367-
"The font may lack a Type-1 version.")
368-
% (font_bunch.psname, texname))
354+
f"No usable font file found for {font_bunch.psname} "
355+
f"({texname}). The font may lack a Type-1 version.")
369356

370357
font = get_font(font_bunch.filename)
371358

372-
for charmap_name, charmap_code in [("ADOBE_CUSTOM", 1094992451),
373-
("ADOBE_STANDARD", 1094995778)]:
374-
try:
375-
font.select_charmap(charmap_code)
376-
except (ValueError, RuntimeError):
377-
pass
378-
else:
379-
break
359+
if font_bunch.encoding:
360+
# If psfonts.map specifies an encoding, use it: it gives us a
361+
# mapping of glyph indices to Adobe glyph names; use it to convert
362+
# dvi indices to glyph names and use the FreeType-synthesized
363+
# unicode charmap to convert glyph names to glyph indices (with
364+
# FT_Get_Name_Index/get_name_index), and load the glyph using
365+
# FT_Load_Glyph/load_glyph. (That charmap has a coverage at least
366+
# as good as, and possibly better than, the native charmaps.)
367+
enc = dviread._parse_enc(font_bunch.encoding)
380368
else:
381-
charmap_name = ""
382-
_log.warning("No supported encoding in font (%s).",
383-
font_bunch.filename)
384-
385-
if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding:
386-
enc0 = dviread.Encoding(font_bunch.encoding)
387-
enc = {i: _get_adobe_standard_encoding().get(c, None)
388-
for i, c in enumerate(enc0.encoding)}
389-
else:
390-
enc = {}
369+
# If psfonts.map specifies no encoding, the indices directly
370+
# map to the font's "native" charmap; so don't use the
371+
# FreeType-synthesized charmap but the native ones (we can't
372+
# directly identify it but it's typically an Adobe charmap), and
373+
# directly load the dvi glyph indices using FT_Load_Char/load_char.
374+
for charmap_name, charmap_code in [
375+
("ADOBE_CUSTOM", 1094992451),
376+
("ADOBE_STANDARD", 1094995778),
377+
]:
378+
try:
379+
font.select_charmap(charmap_code)
380+
except (ValueError, RuntimeError):
381+
pass
382+
else:
383+
break
384+
else:
385+
charmap_name = ""
386+
_log.warning("No supported encoding in font (%s).",
387+
font_bunch.filename)
388+
enc = None
391389

392390
return font, enc
393391

0 commit comments

Comments
 (0)
0