Skip to content

Cache various dviread constructs globally. #10954

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions doc/api/next_api_changes/2018-02-15-AL-deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ The following modules are deprecated:
The following classes, methods, functions, and attributes are deprecated:

- ``afm.parse_afm``,
- ``backend_pdf.PdfFile.texFontMap``,
- ``backend_pgf.get_texcommand``,
- ``backend_ps.get_bbox``,
- ``backend_qt5.FigureCanvasQT.keyAutoRepeat`` (directly check
``event.guiEvent.isAutoRepeat()`` in the event handler to decide whether to
handle autorepeated key presses).
- ``backend_qt5.error_msg_qt``, ``backend_qt5.exception_handler``,
- ``backend_wx.FigureCanvasWx.macros``,
- ``cbook.GetRealpathAndStat``, ``cbook.Locked``,
Expand All @@ -22,21 +26,21 @@ The following classes, methods, functions, and attributes are deprecated:
- ``contour.ContourLabeler.cl``, ``.cl_xy``, and ``.cl_cvalues``,
- ``dates.DateFormatter.strftime_pre_1900``, ``dates.DateFormatter.strftime``,
- ``font_manager.TempCache``,
- ``image._ImageBase.iterpnames``, use the ``interpolation_names`` property
instead. (this affects classes that inherit from ``_ImageBase`` including
:class:`FigureImage`, :class:`BboxImage`, and :class:`AxesImage`),
- ``mathtext.unichr_safe`` (use ``chr`` instead),
- ``patches.Polygon.xy``,
- ``table.Table.get_child_artists`` (use ``get_children`` instead),
- ``testing.compare.ImageComparisonTest``, ``testing.compare.compare_float``,
- ``testing.decorators.CleanupTest``,
``testing.decorators.skip_if_command_unavailable``,
- ``FigureCanvasQT.keyAutoRepeat`` (directly check
``event.guiEvent.isAutoRepeat()`` in the event handler to decide whether to
handle autorepeated key presses).
- ``FigureCanvasWx.macros``,
- ``_ImageBase.iterpnames``, use the ``interpolation_names`` property instead.
(this affects classes that inherit from ``_ImageBase`` including
:class:`FigureImage`, :class:`BboxImage`, and :class:`AxesImage`),
- ``patches.Polygon.xy``,
- ``texmanager.dvipng_hack_alpha``,
- ``text.Annotation.arrow``,
- ``textpath.TextToPath.tex_font_map``,

The following rcParams are deprecated:
- ``pgf.debug`` (the pgf backend relies on logging),
Expand Down
10 changes: 4 additions & 6 deletions lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,14 +655,11 @@ def fontName(self, fontprop):
return Fx

@property
@cbook.deprecated("3.0")
def texFontMap(self):
# lazy-load texFontMap, it takes a while to parse
# and usetex is a relatively rare use case
if self._texFontMap is None:
self._texFontMap = dviread.PsfontsMap(
dviread.find_tex_file('pdftex.map'))

return self._texFontMap
return dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))

def dviFontName(self, dvifont):
"""
Expand All @@ -675,7 +672,8 @@ def dviFontName(self, dvifont):
if dvi_info is not None:
return dvi_info.pdfname

psfont = self.texFontMap[dvifont.texname]
tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
psfont = tex_font_map[dvifont.texname]
if psfont.filename is None:
raise ValueError(
"No usable font file found for {} (TeX: {}); "
Expand Down
28 changes: 15 additions & 13 deletions lib/matplotlib/dviread.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
# iterate over pages:
for page in dvi:
w, h, d = page.width, page.height, page.descent
for x,y,font,glyph,width in page.text:
for x, y, font, glyph, width in page.text:
fontname = font.texname
pointsize = font.size
...
for x,y,height,width in page.boxes:
for x, y, height, width in page.boxes:
...

"""

from collections import namedtuple
import enum
from functools import lru_cache, partial, wraps
Expand All @@ -34,6 +34,10 @@

_log = logging.getLogger(__name__)

# Many dvi related files are looked for by external processes, require
# additional parsing, and are used many times per rendering, which is why they
# are cached using lru_cache().

# Dvi is a bytecode format documented in
# http://mirrors.ctan.org/systems/knuth/dist/texware/dvitype.web
# http://texdoc.net/texmf-dist/doc/generic/knuth/texware/dvitype.pdf
Expand Down Expand Up @@ -808,14 +812,14 @@ class PsfontsMap(object):
"""
__slots__ = ('_font', '_filename')

def __init__(self, filename):
@lru_cache()
def __new__(cls, filename):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a bit dodgy. Would a helper function (still using lru_cache) also work here instead of decorating __new__?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like

@lru_cache()
def _psfontmap_factory(filename): return PsfontsMap(filename)

and use the factory function instead? TBH that seems worse to me but if you prefer that form let me know (but I'm not sure I got what you mean).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tacaswell and I talked, and while this, as a gut reaction, feels a bit magical, it's probably the best mix of being direct and magical, versus other solutions. It wouldn't hurt to have a comment or two here, but I'm not going to hold this up for that.

self = object.__new__(cls)
self._font = {}
self._filename = filename
if isinstance(filename, bytes):
encoding = sys.getfilesystemencoding() or 'utf-8'
self._filename = filename.decode(encoding, errors='replace')
self._filename = os.fsdecode(filename)
with open(filename, 'rb') as file:
self._parse(file)
return self

def __getitem__(self, texname):
assert isinstance(texname, bytes)
Expand Down Expand Up @@ -956,7 +960,8 @@ def __init__(self, filename):
def __iter__(self):
yield from self.encoding

def _parse(self, file):
@staticmethod
def _parse(file):
result = []

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


@lru_cache()
def find_tex_file(filename, format=None):
"""
Find a file in the texmf tree.
Expand Down Expand Up @@ -1016,10 +1022,6 @@ def find_tex_file(filename, format=None):
return result.decode('ascii')


# With multiple text objects per figure (e.g., tick labels) we may end
# up reading the same tfm and vf files many times, so we implement a
# simple cache. TODO: is this worth making persistent?

@lru_cache()
def _fontfile(cls, suffix, texname):
filename = find_tex_file(texname + suffix)
Expand Down
4 changes: 0 additions & 4 deletions lib/matplotlib/texmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,13 @@
"""

import copy
import distutils.version
import glob
import hashlib
import logging
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import warnings

import numpy as np

Expand Down
121 changes: 55 additions & 66 deletions lib/matplotlib/textpath.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
from collections import OrderedDict
import functools
import urllib.parse
import warnings

import numpy as np

from matplotlib.path import Path
from matplotlib import rcParams
import matplotlib.font_manager as font_manager
from matplotlib.ft2font import KERNING_DEFAULT, LOAD_NO_HINTING
from matplotlib.ft2font import LOAD_TARGET_LIGHT
from matplotlib.mathtext import MathTextParser
import matplotlib.dviread as dviread
from matplotlib import cbook, dviread, font_manager, rcParams
from matplotlib.font_manager import FontProperties, get_font
from matplotlib.ft2font import (
KERNING_DEFAULT, LOAD_NO_HINTING, LOAD_TARGET_LIGHT)
from matplotlib.mathtext import MathTextParser
from matplotlib.path import Path
from matplotlib.transforms import Affine2D


@functools.lru_cache(1)
def _get_adobe_standard_encoding():
enc_name = dviread.find_tex_file('8a.enc')
enc = dviread.Encoding(enc_name)
return {c: i for i, c in enumerate(enc.encoding)}


class TextToPath(object):
"""
A class that convert a given text to a path using ttf fonts.
Expand All @@ -25,19 +31,12 @@ class TextToPath(object):

def __init__(self):
self.mathtext_parser = MathTextParser('path')
self.tex_font_map = None

from matplotlib.cbook import maxdict
self._ps_fontd = maxdict(50)

self._texmanager = None

self._adobe_standard_encoding = None

def _get_adobe_standard_encoding(self):
enc_name = dviread.find_tex_file('8a.enc')
enc = dviread.Encoding(enc_name)
return {c: i for i, c in enumerate(enc.encoding)}
@property
@cbook.deprecated("3.0")
def tex_font_map(self):
return dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))

def _get_font(self, prop):
"""
Expand Down Expand Up @@ -281,13 +280,6 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,

texmanager = self.get_texmanager()

if self.tex_font_map is None:
self.tex_font_map = dviread.PsfontsMap(
dviread.find_tex_file('pdftex.map'))

if self._adobe_standard_encoding is None:
self._adobe_standard_encoding = self._get_adobe_standard_encoding()

fontsize = prop.get_size_in_points()
if hasattr(texmanager, "get_dvi"):
dvifilelike = texmanager.get_dvi(s, self.FONT_SCALE)
Expand All @@ -312,46 +304,7 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
# characters into strings.
# oldfont, seq = None, []
for x1, y1, dvifont, glyph, width in page.text:
font_and_encoding = self._ps_fontd.get(dvifont.texname)
font_bunch = self.tex_font_map[dvifont.texname]

if font_and_encoding is None:
if font_bunch.filename is None:
raise ValueError(
("No usable font file found for %s (%s). "
"The font may lack a Type-1 version.")
% (font_bunch.psname, dvifont.texname))

font = get_font(font_bunch.filename)

for charmap_name, charmap_code in [("ADOBE_CUSTOM",
1094992451),
("ADOBE_STANDARD",
1094995778)]:
try:
font.select_charmap(charmap_code)
except (ValueError, RuntimeError):
pass
else:
break
else:
charmap_name = ""
warnings.warn("No supported encoding in font (%s)." %
font_bunch.filename)

if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding:
enc0 = dviread.Encoding(font_bunch.encoding)
enc = {i: self._adobe_standard_encoding.get(c, None)
for i, c in enumerate(enc0.encoding)}
else:
enc = {}
self._ps_fontd[dvifont.texname] = font, enc

else:
font, enc = font_and_encoding

ft2font_flag = LOAD_TARGET_LIGHT

font, enc = self._get_ps_font_and_encoding(dvifont.texname)
char_id = self._get_char_id_ps(font, glyph)

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

ft2font_flag = LOAD_TARGET_LIGHT
if charcode is not None:
glyph0 = font.load_char(charcode, flags=ft2font_flag)
else:
warnings.warn("The glyph (%d) of font (%s) cannot be "
"converted with the encoding. Glyph may "
"be wrong" % (glyph, font_bunch.filename))
"be wrong" % (glyph, font.fname))

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

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

@staticmethod
@functools.lru_cache(50)
def _get_ps_font_and_encoding(texname):
tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map'))
font_bunch = tex_font_map[texname]
if font_bunch.filename is None:
raise ValueError(
("No usable font file found for %s (%s). "
"The font may lack a Type-1 version.")
% (font_bunch.psname, texname))

font = get_font(font_bunch.filename)

for charmap_name, charmap_code in [("ADOBE_CUSTOM", 1094992451),
("ADOBE_STANDARD", 1094995778)]:
try:
font.select_charmap(charmap_code)
except (ValueError, RuntimeError):
pass
else:
break
else:
charmap_name = ""
warnings.warn("No supported encoding in font (%s)." %
font_bunch.filename)

if charmap_name == "ADOBE_STANDARD" and font_bunch.encoding:
enc0 = dviread.Encoding(font_bunch.encoding)
enc = {i: _get_adobe_standard_encoding().get(c, None)
for i, c in enumerate(enc0.encoding)}
else:
enc = {}

return font, enc


text_to_path = TextToPath()

Expand Down