Skip to content

Commit 776fcb3

Browse files
committed
Cache various dviread constructs globally.
Previously, caching was done at the level of the renderer, so new renderers would have to reconstruct the PsfontsMap and Adobe encoding tables. Using a global cache greatly improves the performance: something like rcdefaults() gca().text(.5, .5, "$foo$", usetex=True) %timeit savefig("/tmp/test.svg") goes from ~187ms to ~37ms. %timeit savefig("/tmp/test.pdf") goes from ~124ms to ~53ms. Also moves TextToPath's _get_ps_font_map_and_encoding to use a standard lru_cache.
1 parent a0994d3 commit 776fcb3

File tree

5 files changed

+81
-92
lines changed

5 files changed

+81
-92
lines changed

doc/api/next_api_changes/2018-02-15-AL-deprecations.rst

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ The following modules are deprecated:
1111
The following classes, methods, functions, and attributes are deprecated:
1212

1313
- ``afm.parse_afm``,
14+
- ``backend_pdf.PdfFile.texFontMap``,
1415
- ``backend_pgf.get_texcommand``,
1516
- ``backend_ps.get_bbox``,
17+
- ``backend_qt5.FigureCanvasQT.keyAutoRepeat`` (directly check
18+
``event.guiEvent.isAutoRepeat()`` in the event handler to decide whether to
19+
handle autorepeated key presses).
1620
- ``backend_qt5.error_msg_qt``, ``backend_qt5.exception_handler``,
1721
- ``backend_wx.FigureCanvasWx.macros``,
1822
- ``cbook.GetRealpathAndStat``, ``cbook.Locked``,
@@ -22,21 +26,21 @@ The following classes, methods, functions, and attributes are deprecated:
2226
- ``contour.ContourLabeler.cl``, ``.cl_xy``, and ``.cl_cvalues``,
2327
- ``dates.DateFormatter.strftime_pre_1900``, ``dates.DateFormatter.strftime``,
2428
- ``font_manager.TempCache``,
29+
- ``image._ImageBase.iterpnames``, use the ``interpolation_names`` property
30+
instead. (this affects classes that inherit from ``_ImageBase`` including
31+
:class:`FigureImage`, :class:`BboxImage`, and :class:`AxesImage`),
2532
- ``mathtext.unichr_safe`` (use ``chr`` instead),
33+
- ``patches.Polygon.xy``,
2634
- ``table.Table.get_child_artists`` (use ``get_children`` instead),
2735
- ``testing.compare.ImageComparisonTest``, ``testing.compare.compare_float``,
2836
- ``testing.decorators.CleanupTest``,
2937
``testing.decorators.skip_if_command_unavailable``,
3038
- ``FigureCanvasQT.keyAutoRepeat`` (directly check
3139
``event.guiEvent.isAutoRepeat()`` in the event handler to decide whether to
3240
handle autorepeated key presses).
33-
- ``FigureCanvasWx.macros``,
34-
- ``_ImageBase.iterpnames``, use the ``interpolation_names`` property instead.
35-
(this affects classes that inherit from ``_ImageBase`` including
36-
:class:`FigureImage`, :class:`BboxImage`, and :class:`AxesImage`),
37-
- ``patches.Polygon.xy``,
3841
- ``texmanager.dvipng_hack_alpha``,
3942
- ``text.Annotation.arrow``,
43+
- ``textpath.TextToPath.tex_font_map``,
4044

4145
The following rcParams are deprecated:
4246
- ``pgf.debug`` (the pgf backend relies on logging),

lib/matplotlib/backends/backend_pdf.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -655,14 +655,11 @@ def fontName(self, fontprop):
655655
return Fx
656656

657657
@property
658+
@cbook.deprecated("3.0")
658659
def texFontMap(self):
659660
# lazy-load texFontMap, it takes a while to parse
660661
# and usetex is a relatively rare use case
661-
if self._texFontMap is None:
662-
self._texFontMap = dviread.PsfontsMap(
663-
dviread.find_tex_file('pdftex.map'))
664-
665-
return self._texFontMap
662+
return dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
666663

667664
def dviFontName(self, dvifont):
668665
"""
@@ -675,7 +672,8 @@ def dviFontName(self, dvifont):
675672
if dvi_info is not None:
676673
return dvi_info.pdfname
677674

678-
psfont = self.texFontMap[dvifont.texname]
675+
tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
676+
psfont = tex_font_map[dvifont.texname]
679677
if psfont.filename is None:
680678
raise ValueError(
681679
"No usable font file found for {} (TeX: {}); "

lib/matplotlib/dviread.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
...
1616
for x,y,height,width in page.boxes:
1717
...
18-
1918
"""
19+
2020
from collections import namedtuple
2121
import enum
2222
from functools import lru_cache, partial, wraps
@@ -34,6 +34,10 @@
3434

3535
_log = logging.getLogger(__name__)
3636

37+
# Many dvi related files are looked for by external processes, require
38+
# additional parsing, and are used many times per rendering, which is why they
39+
# are cached using lru_cache().
40+
3741
# Dvi is a bytecode format documented in
3842
# http://mirrors.ctan.org/systems/knuth/dist/texware/dvitype.web
3943
# http://texdoc.net/texmf-dist/doc/generic/knuth/texware/dvitype.pdf
@@ -808,14 +812,14 @@ class PsfontsMap(object):
808812
"""
809813
__slots__ = ('_font', '_filename')
810814

811-
def __init__(self, filename):
815+
@lru_cache()
816+
def __new__(cls, filename):
817+
self = object.__new__(cls)
812818
self._font = {}
813-
self._filename = filename
814-
if isinstance(filename, bytes):
815-
encoding = sys.getfilesystemencoding() or 'utf-8'
816-
self._filename = filename.decode(encoding, errors='replace')
819+
self._filename = os.fsdecode(filename)
817820
with open(filename, 'rb') as file:
818821
self._parse(file)
822+
return self
819823

820824
def __getitem__(self, texname):
821825
assert isinstance(texname, bytes)
@@ -956,7 +960,8 @@ def __init__(self, filename):
956960
def __iter__(self):
957961
yield from self.encoding
958962

959-
def _parse(self, file):
963+
@staticmethod
964+
def _parse(file):
960965
result = []
961966

962967
lines = (line.split(b'%', 1)[0].strip() for line in file)
@@ -975,6 +980,7 @@ def _parse(self, file):
975980
return re.findall(br'/([^][{}<>\s]+)', data)
976981

977982

983+
@lru_cache()
978984
def find_tex_file(filename, format=None):
979985
"""
980986
Find a file in the texmf tree.
@@ -1016,10 +1022,6 @@ def find_tex_file(filename, format=None):
10161022
return result.decode('ascii')
10171023

10181024

1019-
# With multiple text objects per figure (e.g., tick labels) we may end
1020-
# up reading the same tfm and vf files many times, so we implement a
1021-
# simple cache. TODO: is this worth making persistent?
1022-
10231025
@lru_cache()
10241026
def _fontfile(cls, suffix, texname):
10251027
filename = find_tex_file(texname + suffix)

lib/matplotlib/texmanager.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,13 @@
2929
"""
3030

3131
import copy
32-
import distutils.version
3332
import glob
3433
import hashlib
3534
import logging
3635
import os
3736
from pathlib import Path
3837
import re
39-
import shutil
4038
import subprocess
41-
import sys
42-
import warnings
4339

4440
import numpy as np
4541

lib/matplotlib/textpath.py

Lines changed: 55 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
from collections import OrderedDict
2+
import functools
23
import urllib.parse
34
import warnings
45

56
import numpy as np
67

7-
from matplotlib.path import Path
8-
from matplotlib import rcParams
9-
import matplotlib.font_manager as font_manager
10-
from matplotlib.ft2font import KERNING_DEFAULT, LOAD_NO_HINTING
11-
from matplotlib.ft2font import LOAD_TARGET_LIGHT
12-
from matplotlib.mathtext import MathTextParser
13-
import matplotlib.dviread as dviread
8+
from matplotlib import cbook, dviread, font_manager, rcParams
149
from matplotlib.font_manager import FontProperties, get_font
10+
from matplotlib.ft2font import (
11+
KERNING_DEFAULT, LOAD_NO_HINTING, LOAD_TARGET_LIGHT)
12+
from matplotlib.mathtext import MathTextParser
13+
from matplotlib.path import Path
1514
from matplotlib.transforms import Affine2D
1615

1716

17+
@functools.lru_cache(1)
18+
def _get_adobe_standard_encoding():
19+
enc_name = dviread.find_tex_file('8a.enc')
20+
enc = dviread.Encoding(enc_name)
21+
return {c: i for i, c in enumerate(enc.encoding)}
22+
23+
1824
class TextToPath(object):
1925
"""
2026
A class that convert a given text to a path using ttf fonts.
@@ -25,19 +31,12 @@ class TextToPath(object):
2531

2632
def __init__(self):
2733
self.mathtext_parser = MathTextParser('path')
28-
self.tex_font_map = None
29-
30-
from matplotlib.cbook import maxdict
31-
self._ps_fontd = maxdict(50)
32-
3334
self._texmanager = None
3435

35-
self._adobe_standard_encoding = None
36-
37-
def _get_adobe_standard_encoding(self):
38-
enc_name = dviread.find_tex_file('8a.enc')
39-
enc = dviread.Encoding(enc_name)
40-
return {c: i for i, c in enumerate(enc.encoding)}
36+
@property
37+
@cbook.deprecated("3.0")
38+
def tex_font_map(self):
39+
return dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
4140

4241
def _get_font(self, prop):
4342
"""
@@ -281,13 +280,6 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
281280

282281
texmanager = self.get_texmanager()
283282

284-
if self.tex_font_map is None:
285-
self.tex_font_map = dviread.PsfontsMap(
286-
dviread.find_tex_file('pdftex.map'))
287-
288-
if self._adobe_standard_encoding is None:
289-
self._adobe_standard_encoding = self._get_adobe_standard_encoding()
290-
291283
fontsize = prop.get_size_in_points()
292284
if hasattr(texmanager, "get_dvi"):
293285
dvifilelike = texmanager.get_dvi(s, self.FONT_SCALE)
@@ -312,46 +304,7 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
312304
# characters into strings.
313305
# oldfont, seq = None, []
314306
for x1, y1, dvifont, glyph, width in page.text:
315-
font_and_encoding = self._ps_fontd.get(dvifont.texname)
316-
font_bunch = self.tex_font_map[dvifont.texname]
317-
318-
if font_and_encoding is None:
319-
if font_bunch.filename is None:
320-
raise ValueError(
321-
("No usable font file found for %s (%s). "
322-
"The font may lack a Type-1 version.")
323-
% (font_bunch.psname, dvifont.texname))
324-
325-
font = get_font(font_bunch.filename)
326-
327-
for charmap_name, charmap_code in [("ADOBE_CUSTOM",
328-
1094992451),
329-
("ADOBE_STANDARD",
330-
1094995778)]:
331-
try:
332-
font.select_charmap(charmap_code)
333-
except (ValueError, RuntimeError):
334-
pass
335-
else:
336-
break
337-
else:
338-
charmap_name = ""
339-
warnings.warn("No supported encoding in font (%s)." %
340-
font_bunch.filename)
341-
342-
if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding:
343-
enc0 = dviread.Encoding(font_bunch.encoding)
344-
enc = {i: self._adobe_standard_encoding.get(c, None)
345-
for i, c in enumerate(enc0.encoding)}
346-
else:
347-
enc = {}
348-
self._ps_fontd[dvifont.texname] = font, enc
349-
350-
else:
351-
font, enc = font_and_encoding
352-
353-
ft2font_flag = LOAD_TARGET_LIGHT
354-
307+
font, enc = self._get_ps_font_and_encoding(dvifont.texname)
355308
char_id = self._get_char_id_ps(font, glyph)
356309

357310
if char_id not in glyph_map:
@@ -362,12 +315,13 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
362315
else:
363316
charcode = glyph
364317

318+
ft2font_flag = LOAD_TARGET_LIGHT
365319
if charcode is not None:
366320
glyph0 = font.load_char(charcode, flags=ft2font_flag)
367321
else:
368322
warnings.warn("The glyph (%d) of font (%s) cannot be "
369323
"converted with the encoding. Glyph may "
370-
"be wrong" % (glyph, font_bunch.filename))
324+
"be wrong" % (glyph, font.fname))
371325

372326
glyph0 = font.load_char(glyph, flags=ft2font_flag)
373327

@@ -391,6 +345,41 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
391345
return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
392346
glyph_map_new, myrects)
393347

348+
@staticmethod
349+
@functools.lru_cache(50)
350+
def _get_ps_font_and_encoding(texname):
351+
tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
352+
font_bunch = tex_font_map[texname]
353+
if font_bunch.filename is None:
354+
raise ValueError(
355+
("No usable font file found for %s (%s). "
356+
"The font may lack a Type-1 version.")
357+
% (font_bunch.psname, texname))
358+
359+
font = get_font(font_bunch.filename)
360+
361+
for charmap_name, charmap_code in [("ADOBE_CUSTOM", 1094992451),
362+
("ADOBE_STANDARD", 1094995778)]:
363+
try:
364+
font.select_charmap(charmap_code)
365+
except (ValueError, RuntimeError):
366+
pass
367+
else:
368+
break
369+
else:
370+
charmap_name = ""
371+
warnings.warn("No supported encoding in font (%s)." %
372+
font_bunch.filename)
373+
374+
if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding:
375+
enc0 = dviread.Encoding(font_bunch.encoding)
376+
enc = {i: _get_adobe_standard_encoding().get(c, None)
377+
for i, c in enumerate(enc0.encoding)}
378+
else:
379+
enc = {}
380+
381+
return font, enc
382+
394383

395384
text_to_path = TextToPath()
396385

0 commit comments

Comments
 (0)