Skip to content

Commit 85fb163

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 d9b5b4e commit 85fb163

File tree

5 files changed

+73
-87
lines changed

5 files changed

+73
-87
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ 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_wx.FigureCanvasWx.macros``,
1516
- ``cbook.GetRealpathAndStat``, ``cbook.Locked``,
1617
- ``cbook.is_numlike`` (use ``isinstance(..., numbers.Number)`` instead),
@@ -20,6 +21,7 @@ The following classes, methods, functions, and attributes are deprecated:
2021
- ``font_manager.TempCache``,
2122
- ``mathtext.unichr_safe`` (use ``chr`` instead),
2223
- ``testing.ImageComparisonTest``,
24+
- ``textpath.TextToPath.tex_font_map``,
2325
- ``FigureCanvasWx.macros``,
2426
- ``_ImageBase.iterpnames``, use the ``interpolation_names`` property instead.
2527
(this affects classes that inherit from ``_ImageBase`` including

lib/matplotlib/backends/backend_pdf.py

+4-6
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

+13-11
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

-4
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

+54-66
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,11 @@ 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+
@cbook.deprecated("3.0")
37+
def tex_font_map(self):
38+
return dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
4139

4240
def _get_font(self, prop):
4341
"""
@@ -287,13 +285,6 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
287285

288286
texmanager = self.get_texmanager()
289287

290-
if self.tex_font_map is None:
291-
self.tex_font_map = dviread.PsfontsMap(
292-
dviread.find_tex_file('pdftex.map'))
293-
294-
if self._adobe_standard_encoding is None:
295-
self._adobe_standard_encoding = self._get_adobe_standard_encoding()
296-
297288
fontsize = prop.get_size_in_points()
298289
if hasattr(texmanager, "get_dvi"):
299290
dvifilelike = texmanager.get_dvi(s, self.FONT_SCALE)
@@ -318,46 +309,7 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
318309
# characters into strings.
319310
# oldfont, seq = None, []
320311
for x1, y1, dvifont, glyph, width in page.text:
321-
font_and_encoding = self._ps_fontd.get(dvifont.texname)
322-
font_bunch = self.tex_font_map[dvifont.texname]
323-
324-
if font_and_encoding is None:
325-
if font_bunch.filename is None:
326-
raise ValueError(
327-
("No usable font file found for %s (%s). "
328-
"The font may lack a Type-1 version.")
329-
% (font_bunch.psname, dvifont.texname))
330-
331-
font = get_font(font_bunch.filename)
332-
333-
for charmap_name, charmap_code in [("ADOBE_CUSTOM",
334-
1094992451),
335-
("ADOBE_STANDARD",
336-
1094995778)]:
337-
try:
338-
font.select_charmap(charmap_code)
339-
except (ValueError, RuntimeError):
340-
pass
341-
else:
342-
break
343-
else:
344-
charmap_name = ""
345-
warnings.warn("No supported encoding in font (%s)." %
346-
font_bunch.filename)
347-
348-
if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding:
349-
enc0 = dviread.Encoding(font_bunch.encoding)
350-
enc = {i: self._adobe_standard_encoding.get(c, None)
351-
for i, c in enumerate(enc0.encoding)}
352-
else:
353-
enc = {}
354-
self._ps_fontd[dvifont.texname] = font, enc
355-
356-
else:
357-
font, enc = font_and_encoding
358-
359-
ft2font_flag = LOAD_TARGET_LIGHT
360-
312+
font, enc = self._get_ps_font_and_encoding(dvifont.texname)
361313
char_id = self._get_char_id_ps(font, glyph)
362314

363315
if char_id not in glyph_map:
@@ -368,12 +320,13 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
368320
else:
369321
charcode = glyph
370322

323+
ft2font_flag = LOAD_TARGET_LIGHT
371324
if charcode is not None:
372325
glyph0 = font.load_char(charcode, flags=ft2font_flag)
373326
else:
374327
warnings.warn("The glyph (%d) of font (%s) cannot be "
375328
"converted with the encoding. Glyph may "
376-
"be wrong" % (glyph, font_bunch.filename))
329+
"be wrong" % (glyph, font.fname))
377330

378331
glyph0 = font.load_char(glyph, flags=ft2font_flag)
379332

@@ -397,6 +350,41 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
397350
return (list(zip(glyph_ids, xpositions, ypositions, sizes)),
398351
glyph_map_new, myrects)
399352

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

401389
text_to_path = TextToPath()
402390

0 commit comments

Comments
 (0)