Skip to content

Commit fcd3bf6

Browse files
committed
Fix loading of encoded fonts in textpath.
Consider the following example. import matplotlib.pyplot as plt plt.rcParams['text.usetex'] = True plt.rcParams['text.latex.preamble'] = r'\usepackage{siunitx}' plt.rcParams['text.hinting_factor'] = 1 plt.text(.5, .5, r'$\si{\degree}$') plt.text(.5, .4, r'ff\textwon') plt.gca().set_axis_off() plt.savefig('/tmp/plot.svg') plt.savefig('/tmp/plot.pdf') plt.savefig('/tmp/plot.png') plt.show() In the svg output, one sees that the \degree and \textwon characters (which come from a different font that the ff ligature) are now correctly loaded, *but* at a too small size -- this still needs to be fixed. (pdf and png output are unaffected.)
1 parent 369618a commit fcd3bf6

File tree

2 files changed

+66
-50
lines changed

2 files changed

+66
-50
lines changed

lib/matplotlib/dviread.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -968,10 +968,36 @@ 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+
return re.findall(r"(?<=/)[A-za-z0-9._]+", array)
999+
1000+
9751001
@lru_cache()
9761002
def find_tex_file(filename, format=None):
9771003
"""

lib/matplotlib/textpath.py

Lines changed: 39 additions & 49 deletions
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
"""
2821
A class that convert a given text to a path using ttf fonts.
@@ -297,12 +290,8 @@ def get_texmanager(self):
297290

298291
def get_glyphs_tex(self, prop, s, glyph_map=None,
299292
return_new_glyphs_only=False):
300-
"""
301-
convert the string *s* to vertices and codes using matplotlib's usetex
302-
mode.
303-
"""
304-
305-
# codes are modstly borrowed from pdf backend.
293+
"""Convert the string *s* to vertices and codes using usetex mode."""
294+
# Mostly borrowed from pdf backend.
306295

307296
dvifile = self.get_texmanager().make_dvi(s, self.FONT_SCALE)
308297
with dviread.Dvi(dvifile, self.DPI) as dvi:
@@ -320,29 +309,20 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
320309

321310
# Gather font information and do some setup for combining
322311
# characters into strings.
323-
# oldfont, seq = None, []
324312
for x1, y1, dvifont, glyph, width in page.text:
325313
font, enc = self._get_ps_font_and_encoding(dvifont.texname)
326314
char_id = self._get_char_id_ps(font, glyph)
327315

328316
if char_id not in glyph_map:
329317
font.clear()
330318
font.set_size(self.FONT_SCALE, self.DPI)
331-
if enc:
332-
charcode = enc.get(glyph, None)
333-
else:
334-
charcode = glyph
335-
336-
ft2font_flag = LOAD_TARGET_LIGHT
337-
if charcode is not None:
338-
glyph0 = font.load_char(charcode, flags=ft2font_flag)
319+
# See comments in _get_ps_font_and_encoding.
320+
if enc is not None:
321+
index = font.get_name_index(enc[glyph])
322+
font.load_glyph(index, flags=LOAD_TARGET_LIGHT)
339323
else:
340-
_log.warning("The glyph (%d) of font (%s) cannot be "
341-
"converted with the encoding. Glyph may "
342-
"be wrong.", glyph, font.fname)
343-
344-
glyph0 = font.load_char(glyph, flags=ft2font_flag)
345-
324+
index = glyph
325+
font.load_char(index, flags=LOAD_TARGET_LIGHT)
346326
glyph_map_new[char_id] = self.glyph_to_path(font)
347327

348328
glyph_ids.append(char_id)
@@ -370,31 +350,41 @@ def _get_ps_font_and_encoding(texname):
370350
font_bunch = tex_font_map[texname]
371351
if font_bunch.filename is None:
372352
raise ValueError(
373-
("No usable font file found for %s (%s). "
374-
"The font may lack a Type-1 version.")
375-
% (font_bunch.psname, texname))
353+
"No usable font file found for {} ({}). "
354+
"The font may lack a Type-1 version."
355+
.format(font_bunch.psname, texname))
376356

377357
font = get_font(font_bunch.filename)
378358

379-
for charmap_name, charmap_code in [("ADOBE_CUSTOM", 1094992451),
380-
("ADOBE_STANDARD", 1094995778)]:
381-
try:
382-
font.select_charmap(charmap_code)
383-
except (ValueError, RuntimeError):
384-
pass
385-
else:
386-
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)
387368
else:
388-
charmap_name = ""
389-
_log.warning("No supported encoding in font (%s).",
390-
font_bunch.filename)
391-
392-
if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding:
393-
enc0 = dviread.Encoding(font_bunch.encoding)
394-
enc = {i: _get_adobe_standard_encoding().get(c, None)
395-
for i, c in enumerate(enc0.encoding)}
396-
else:
397-
enc = {}
369+
# If psfonts.map specifies no encoding, the indices directly map to
370+
# the font's builtin charmap (see the pdftex manual, section 6.1
371+
# -- Map files); so don't use the FreeType-synthesized charmap but
372+
# the native ones (we can't directly identify it but it's typically
373+
# an Adobe charmap), and directly load the dvi glyph indices using
374+
# FT_Load_Char/load_char.
375+
for charmap_name, charmap_code in [("ADOBE_CUSTOM", 1094992451),
376+
("ADOBE_STANDARD", 1094995778)]:
377+
try:
378+
font.select_charmap(charmap_code)
379+
except (ValueError, RuntimeError):
380+
pass
381+
else:
382+
break
383+
else:
384+
charmap_name = ""
385+
_log.warning("No supported encoding in font (%s).",
386+
font_bunch.filename)
387+
enc = None
398388

399389
return font, enc
400390

0 commit comments

Comments
 (0)