diff --git a/doc/api/next_api_changes/deprecations/22127-OG.rst b/doc/api/next_api_changes/deprecations/22127-OG.rst new file mode 100644 index 000000000000..0abcb2882800 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/22127-OG.rst @@ -0,0 +1,22 @@ +Classes, functions, and named tuples in ``dviread`` deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The classes +- ``DviFont``, +- ``Vf``, +- ``Tfm``, +the function +- ``find_tex_file`` +and the named tuples +- ``Box``, +- ``Page``, +- ``Text`` +from the module ``matplotlib.dviread`` are considered internal and public +access is deprecated. + + +Module ``texmanager`` deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The module ``matplotlib.texmanager`` is considered internal and public access +is deprecated. diff --git a/lib/matplotlib/_dviread.py b/lib/matplotlib/_dviread.py new file mode 100644 index 000000000000..655515da4e79 --- /dev/null +++ b/lib/matplotlib/_dviread.py @@ -0,0 +1,439 @@ +""" +A module for reading dvi files output by TeX. Several limitations make +this not (currently) useful as a general-purpose dvi preprocessor, but +it is currently used by the pdf backend for processing usetex text. + +Interface:: + + with Dvi(filename, 72) as dvi: + # 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: + fontname = font.texname + pointsize = font.size + ... + for x, y, height, width in page.boxes: + ... +""" + +from collections import namedtuple +from functools import lru_cache, partial, wraps +import logging +import os +from pathlib import Path +import re +import struct +import subprocess +import sys + +from matplotlib import _api, cbook + +_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(). + +# The marks on a page consist of text and boxes. A page also has dimensions. +Page = namedtuple('Page', 'text boxes height width descent') +Text = namedtuple('Text', 'x y font glyph width') +Box = namedtuple('Box', 'x y height width') + + +# Opcode argument parsing +# +# Each of the following functions takes a Dvi object and delta, +# which is the difference between the opcode and the minimum opcode +# with the same meaning. Dvi opcodes often encode the number of +# argument bytes in this delta. + +def _arg_raw(dvi, delta): + """Return *delta* without reading anything more from the dvi file.""" + return delta + + +def _arg(nbytes, signed, dvi, _): + """ + Read *nbytes* bytes, returning the bytes interpreted as a signed integer + if *signed* is true, unsigned otherwise. + """ + return dvi._arg(nbytes, signed) + + +def _arg_slen(dvi, delta): + """ + Read *delta* bytes, returning None if *delta* is zero, and the bytes + interpreted as a signed integer otherwise. + """ + if delta == 0: + return None + return dvi._arg(delta, True) + + +def _arg_slen1(dvi, delta): + """ + Read *delta*+1 bytes, returning the bytes interpreted as signed. + """ + return dvi._arg(delta + 1, True) + + +def _arg_ulen1(dvi, delta): + """ + Read *delta*+1 bytes, returning the bytes interpreted as unsigned. + """ + return dvi._arg(delta + 1, False) + + +def _arg_olen1(dvi, delta): + """ + Read *delta*+1 bytes, returning the bytes interpreted as + unsigned integer for 0<=*delta*<3 and signed if *delta*==3. + """ + return dvi._arg(delta + 1, delta == 3) + + +_arg_mapping = dict(raw=_arg_raw, + u1=partial(_arg, 1, False), + u4=partial(_arg, 4, False), + s4=partial(_arg, 4, True), + slen=_arg_slen, + olen1=_arg_olen1, + slen1=_arg_slen1, + ulen1=_arg_ulen1) + + +def _dispatch(table, min, max=None, state=None, args=('raw',)): + """ + Decorator for dispatch by opcode. Sets the values in *table* + from *min* to *max* to this method, adds a check that the Dvi state + matches *state* if not None, reads arguments from the file according + to *args*. + + Parameters + ---------- + table : dict[int, callable] + The dispatch table to be filled in. + + min, max : int + Range of opcodes that calls the registered function; *max* defaults to + *min*. + + state : _dvistate, optional + State of the Dvi object in which these opcodes are allowed. + + args : list[str], default: ['raw'] + Sequence of argument specifications: + + - 'raw': opcode minus minimum + - 'u1': read one unsigned byte + - 'u4': read four bytes, treat as an unsigned number + - 's4': read four bytes, treat as a signed number + - 'slen': read (opcode - minimum) bytes, treat as signed + - 'slen1': read (opcode - minimum + 1) bytes, treat as signed + - 'ulen1': read (opcode - minimum + 1) bytes, treat as unsigned + - 'olen1': read (opcode - minimum + 1) bytes, treat as unsigned + if under four bytes, signed if four bytes + """ + def decorate(method): + get_args = [_arg_mapping[x] for x in args] + + @wraps(method) + def wrapper(self, byte): + if state is not None and self.state != state: + raise ValueError("state precondition failed") + return method(self, *[f(self, byte-min) for f in get_args]) + if max is None: + table[min] = wrapper + else: + for i in range(min, max+1): + assert table[i] is None + table[i] = wrapper + return wrapper + return decorate + + +class DviFont: + """ + Encapsulation of a font that a DVI file can refer to. + + This class holds a font's texname and size, supports comparison, + and knows the widths of glyphs in the same units as the AFM file. + There are also internal attributes (for use by dviread.py) that + are *not* used for comparison. + + The size is in Adobe points (converted from TeX points). + + Parameters + ---------- + scale : float + Factor by which the font is scaled from its natural size. + tfm : Tfm + TeX font metrics for this font + texname : bytes + Name of the font as used internally by TeX and friends, as an ASCII + bytestring. This is usually very different from any external font + names; `PsfontsMap` can be used to find the external name of the font. + vf : Vf + A TeX "virtual font" file, or None if this font is not virtual. + + Attributes + ---------- + texname : bytes + size : float + Size of the font in Adobe points, converted from the slightly + smaller TeX points. + widths : list + Widths of glyphs in glyph-space units, typically 1/1000ths of + the point size. + + """ + __slots__ = ('texname', 'size', 'widths', '_scale', '_vf', '_tfm') + + def __init__(self, scale, tfm, texname, vf): + _api.check_isinstance(bytes, texname=texname) + self._scale = scale + self._tfm = tfm + self.texname = texname + self._vf = vf + self.size = scale * (72.0 / (72.27 * 2**16)) + try: + nchars = max(tfm.width) + 1 + except ValueError: + nchars = 0 + self.widths = [(1000*tfm.width.get(char, 0)) >> 20 + for char in range(nchars)] + + def __eq__(self, other): + return (type(self) == type(other) + and self.texname == other.texname and self.size == other.size) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "<{}: {}>".format(type(self).__name__, self.texname) + + def _width_of(self, char): + """Width of char in dvi units.""" + width = self._tfm.width.get(char, None) + if width is not None: + return _mul2012(width, self._scale) + _log.debug('No width for char %d in font %s.', char, self.texname) + return 0 + + def _height_depth_of(self, char): + """Height and depth of char in dvi units.""" + result = [] + for metric, name in ((self._tfm.height, "height"), + (self._tfm.depth, "depth")): + value = metric.get(char, None) + if value is None: + _log.debug('No %s for char %d in font %s', + name, char, self.texname) + result.append(0) + else: + result.append(_mul2012(value, self._scale)) + # cmsyXX (symbols font) glyph 0 ("minus") has a nonzero descent + # so that TeX aligns equations properly + # (https://tex.stackexchange.com/q/526103/) + # but we actually care about the rasterization depth to align + # the dvipng-generated images. + if re.match(br'^cmsy\d+$', self.texname) and char == 0: + result[-1] = 0 + return result + + +def _mul2012(num1, num2): + """Multiply two numbers in 20.12 fixed point format.""" + # Separated into a function because >> has surprising precedence + return (num1*num2) >> 20 + + +class Tfm: + """ + A TeX Font Metric file. + + This implementation covers only the bare minimum needed by the Dvi class. + + Parameters + ---------- + filename : str or path-like + + Attributes + ---------- + checksum : int + Used for verifying against the dvi file. + design_size : int + Design size of the font (unknown units) + width, height, depth : dict + Dimensions of each character, need to be scaled by the factor + specified in the dvi file. These are dicts because indexing may + not start from 0. + """ + __slots__ = ('checksum', 'design_size', 'width', 'height', 'depth') + + def __init__(self, filename): + _log.debug('opening tfm file %s', filename) + with open(filename, 'rb') as file: + header1 = file.read(24) + lh, bc, ec, nw, nh, nd = struct.unpack('!6H', header1[2:14]) + _log.debug('lh=%d, bc=%d, ec=%d, nw=%d, nh=%d, nd=%d', + lh, bc, ec, nw, nh, nd) + header2 = file.read(4*lh) + self.checksum, self.design_size = struct.unpack('!2I', header2[:8]) + # there is also encoding information etc. + char_info = file.read(4*(ec-bc+1)) + widths = struct.unpack(f'!{nw}i', file.read(4*nw)) + heights = struct.unpack(f'!{nh}i', file.read(4*nh)) + depths = struct.unpack(f'!{nd}i', file.read(4*nd)) + self.width, self.height, self.depth = {}, {}, {} + for idx, char in enumerate(range(bc, ec+1)): + byte0 = char_info[4*idx] + byte1 = char_info[4*idx+1] + self.width[char] = widths[byte0] + self.height[char] = heights[byte1 >> 4] + self.depth[char] = depths[byte1 & 0xf] + + +def _parse_enc(path): + r""" + Parse a \*.enc file referenced from a psfonts.map style file. + + The format supported by this function is a tiny subset of PostScript. + + Parameters + ---------- + path : os.PathLike + + Returns + ------- + list + The nth entry of the list is the PostScript glyph name of the nth + glyph. + """ + no_comments = re.sub("%.*", "", Path(path).read_text(encoding="ascii")) + array = re.search(r"(?s)\[(.*)\]", no_comments).group(1) + lines = [line for line in array.split() if line] + if all(line.startswith("/") for line in lines): + return [line[1:] for line in lines] + else: + raise ValueError( + "Failed to parse {} as Postscript encoding".format(path)) + + +class _LuatexKpsewhich: + @lru_cache() # A singleton. + def __new__(cls): + self = object.__new__(cls) + self._proc = self._new_proc() + return self + + def _new_proc(self): + return subprocess.Popen( + ["luatex", "--luaonly", + str(cbook._get_data_path("kpsewhich.lua"))], + stdin=subprocess.PIPE, stdout=subprocess.PIPE) + + def search(self, filename): + if self._proc.poll() is not None: # Dead, restart it. + self._proc = self._new_proc() + self._proc.stdin.write(os.fsencode(filename) + b"\n") + self._proc.stdin.flush() + out = self._proc.stdout.readline().rstrip() + return None if out == b"nil" else os.fsdecode(out) + + +@lru_cache() +@_api.delete_parameter("3.5", "format") +def _find_tex_file(filename, format=None): + """ + Find a file in the texmf tree using kpathsea_. + + The kpathsea library, provided by most existing TeX distributions, both + on Unix-like systems and on Windows (MikTeX), is invoked via a long-lived + luatex process if luatex is installed, or via kpsewhich otherwise. + + .. _kpathsea: https://www.tug.org/kpathsea/ + + Parameters + ---------- + filename : str or path-like + format : str or bytes + Used as the value of the ``--format`` option to :program:`kpsewhich`. + Could be e.g. 'tfm' or 'vf' to limit the search to that type of files. + Deprecated. + + Raises + ------ + FileNotFoundError + If the file is not found. + """ + + # we expect these to always be ascii encoded, but use utf-8 + # out of caution + if isinstance(filename, bytes): + filename = filename.decode('utf-8', errors='replace') + if isinstance(format, bytes): + format = format.decode('utf-8', errors='replace') + + try: + lk = _LuatexKpsewhich() + except FileNotFoundError: + lk = None # Fallback to directly calling kpsewhich, as below. + + if lk and format is None: + path = lk.search(filename) + + else: + if os.name == 'nt': + # On Windows only, kpathsea can use utf-8 for cmd args and output. + # The `command_line_encoding` environment variable is set to force + # it to always use utf-8 encoding. See Matplotlib issue #11848. + kwargs = {'env': {**os.environ, 'command_line_encoding': 'utf-8'}, + 'encoding': 'utf-8'} + else: # On POSIX, run through the equivalent of os.fsdecode(). + kwargs = {'encoding': sys.getfilesystemencoding(), + 'errors': 'surrogateescape'} + + cmd = ['kpsewhich'] + if format is not None: + cmd += ['--format=' + format] + cmd += [filename] + try: + path = (cbook._check_and_log_subprocess(cmd, _log, **kwargs) + .rstrip('\n')) + except (FileNotFoundError, RuntimeError): + path = None + + if path: + return path + else: + raise FileNotFoundError( + f"Matplotlib's TeX implementation searched for a file named " + f"{filename!r} in your texmf tree, but could not find it") + + +# After the deprecation period elapses, delete this shim and rename +# _find_tex_file to find_tex_file everywhere. +@_api.delete_parameter("3.5", "format") +def find_tex_file(filename, format=None): + try: + return (_find_tex_file(filename, format) if format is not None else + _find_tex_file(filename)) + except FileNotFoundError as exc: + _api.warn_deprecated( + "3.6", message=f"{exc.args[0]}; in the future, this will raise a " + f"FileNotFoundError.") + return "" + + +find_tex_file.__doc__ = _find_tex_file.__doc__ + + +@lru_cache() +def _fontfile(cls, suffix, texname): + return cls(_find_tex_file(texname + suffix)) + + +_tfmfile = partial(_fontfile, Tfm, ".tfm") diff --git a/lib/matplotlib/_texmanager.py b/lib/matplotlib/_texmanager.py new file mode 100644 index 000000000000..c9960c8c14cd --- /dev/null +++ b/lib/matplotlib/_texmanager.py @@ -0,0 +1,338 @@ +r""" +Support for embedded TeX expressions in Matplotlib. + +Requirements: + +* LaTeX. +* \*Agg backends: dvipng>=1.6. +* PS backend: PSfrag, dvips, and Ghostscript>=9.0. +* PDF and SVG backends: if LuaTeX is present, it will be used to speed up some + post-processing steps, but note that it is not used to parse the TeX string + itself (only LaTeX is supported). + +To enable TeX rendering of all text in your Matplotlib figure, set +:rc:`text.usetex` to True. + +TeX and dvipng/dvips processing results are cached +in ~/.matplotlib/tex.cache for reuse between sessions. + +`TexManager.get_rgba` can also be used to directly obtain raster output as RGBA +NumPy arrays. +""" + +import functools +import hashlib +import logging +import os +from pathlib import Path +import subprocess +from tempfile import TemporaryDirectory + +import numpy as np + +import matplotlib as mpl +from matplotlib import _api, cbook, dviread, rcParams + +_log = logging.getLogger(__name__) + + +def _usepackage_if_not_loaded(package, *, option=None): + """ + Output LaTeX code that loads a package (possibly with an option) if it + hasn't been loaded yet. + + LaTeX cannot load twice a package with different options, so this helper + can be used to protect against users loading arbitrary packages/options in + their custom preamble. + """ + option = f"[{option}]" if option is not None else "" + return ( + r"\makeatletter" + r"\@ifpackageloaded{%(package)s}{}{\usepackage%(option)s{%(package)s}}" + r"\makeatother" + ) % {"package": package, "option": option} + + +class TexManager: + """ + Convert strings to dvi files using TeX, caching the results to a directory. + + Repeated calls to this constructor always return the same instance. + """ + + texcache = os.path.join(mpl.get_cachedir(), 'tex.cache') + + _grey_arrayd = {} + _font_family = 'serif' + _font_families = ('serif', 'sans-serif', 'cursive', 'monospace') + _font_info = { + 'new century schoolbook': ('pnc', r'\renewcommand{\rmdefault}{pnc}'), + 'bookman': ('pbk', r'\renewcommand{\rmdefault}{pbk}'), + 'times': ('ptm', r'\usepackage{mathptmx}'), + 'palatino': ('ppl', r'\usepackage{mathpazo}'), + 'zapf chancery': ('pzc', r'\usepackage{chancery}'), + 'cursive': ('pzc', r'\usepackage{chancery}'), + 'charter': ('pch', r'\usepackage{charter}'), + 'serif': ('cmr', ''), + 'sans-serif': ('cmss', ''), + 'helvetica': ('phv', r'\usepackage{helvet}'), + 'avant garde': ('pag', r'\usepackage{avant}'), + 'courier': ('pcr', r'\usepackage{courier}'), + # Loading the type1ec package ensures that cm-super is installed, which + # is necessary for unicode computer modern. (It also allows the use of + # computer modern at arbitrary sizes, but that's just a side effect.) + 'monospace': ('cmtt', r'\usepackage{type1ec}'), + 'computer modern roman': ('cmr', r'\usepackage{type1ec}'), + 'computer modern sans serif': ('cmss', r'\usepackage{type1ec}'), + 'computer modern typewriter': ('cmtt', r'\usepackage{type1ec}')} + _font_types = { + 'new century schoolbook': 'serif', 'bookman': 'serif', + 'times': 'serif', 'palatino': 'serif', 'charter': 'serif', + 'computer modern roman': 'serif', 'zapf chancery': 'cursive', + 'helvetica': 'sans-serif', 'avant garde': 'sans-serif', + 'computer modern sans serif': 'sans-serif', + 'courier': 'monospace', 'computer modern typewriter': 'monospace'} + + grey_arrayd = _api.deprecate_privatize_attribute("3.5") + font_family = _api.deprecate_privatize_attribute("3.5") + font_families = _api.deprecate_privatize_attribute("3.5") + font_info = _api.deprecate_privatize_attribute("3.5") + + @functools.lru_cache() # Always return the same instance. + def __new__(cls): + Path(cls.texcache).mkdir(parents=True, exist_ok=True) + return object.__new__(cls) + + def get_font_config(self): + ff = rcParams['font.family'] + ff_val = ff[0].lower() if len(ff) == 1 else None + reduced_notation = False + if len(ff) == 1 and ff_val in self._font_families: + self._font_family = ff_val + elif len(ff) == 1 and ff_val in self._font_info: + reduced_notation = True + self._font_family = self._font_types[ff_val] + else: + _log.info('font.family must be one of (%s) when text.usetex is ' + 'True. serif will be used by default.', + ', '.join(self._font_families)) + self._font_family = 'serif' + + fontconfig = [self._font_family] + fonts = {} + for font_family in self._font_families: + if reduced_notation and self._font_family == font_family: + fonts[font_family] = self._font_info[ff_val] + else: + for font in rcParams['font.' + font_family]: + if font.lower() in self._font_info: + fonts[font_family] = self._font_info[font.lower()] + _log.debug( + 'family: %s, font: %s, info: %s', + font_family, font, self._font_info[font.lower()]) + break + else: + _log.debug('%s font is not compatible with usetex.', + font) + else: + _log.info('No LaTeX-compatible font found for the %s font' + 'family in rcParams. Using default.', + font_family) + fonts[font_family] = self._font_info[font_family] + fontconfig.append(fonts[font_family][0]) + # Add a hash of the latex preamble to fontconfig so that the + # correct png is selected for strings rendered with same font and dpi + # even if the latex preamble changes within the session + preamble_bytes = self.get_custom_preamble().encode('utf-8') + fontconfig.append(hashlib.md5(preamble_bytes).hexdigest()) + + # The following packages and commands need to be included in the latex + # file's preamble: + cmd = {fonts[family][1] + for family in ['serif', 'sans-serif', 'monospace']} + if self._font_family == 'cursive': + cmd.add(fonts['cursive'][1]) + cmd.add(r'\usepackage{type1cm}') + self._font_preamble = '\n'.join(sorted(cmd)) + + return ''.join(fontconfig) + + def get_basefile(self, tex, fontsize, dpi=None): + """ + Return a filename based on a hash of the string, fontsize, and dpi. + """ + s = ''.join([tex, self.get_font_config(), '%f' % fontsize, + self.get_custom_preamble(), str(dpi or '')]) + return os.path.join( + self.texcache, hashlib.md5(s.encode('utf-8')).hexdigest()) + + def get_font_preamble(self): + """ + Return a string containing font configuration for the tex preamble. + """ + return self._font_preamble + + def get_custom_preamble(self): + """Return a string containing user additions to the tex preamble.""" + return rcParams['text.latex.preamble'] + + def _get_preamble(self): + return "\n".join([ + r"\documentclass{article}", + # Pass-through \mathdefault, which is used in non-usetex mode to + # use the default text font but was historically suppressed in + # usetex mode. + r"\newcommand{\mathdefault}[1]{#1}", + self._font_preamble, + r"\usepackage[utf8]{inputenc}", + r"\DeclareUnicodeCharacter{2212}{\ensuremath{-}}", + # geometry is loaded before the custom preamble as convert_psfrags + # relies on a custom preamble to change the geometry. + r"\usepackage[papersize=72in, margin=1in]{geometry}", + self.get_custom_preamble(), + # Use `underscore` package to take care of underscores in text + # The [strings] option allows to use underscores in file names + _usepackage_if_not_loaded("underscore", option="strings"), + # Custom packages (e.g. newtxtext) may already have loaded textcomp + # with different options. + _usepackage_if_not_loaded("textcomp"), + ]) + + def make_tex(self, tex, fontsize): + """ + Generate a tex file to render the tex string at a specific font size. + + Return the file name. + """ + basefile = self.get_basefile(tex, fontsize) + texfile = '%s.tex' % basefile + fontcmd = {'sans-serif': r'{\sffamily %s}', + 'monospace': r'{\ttfamily %s}'}.get(self._font_family, + r'{\rmfamily %s}') + + Path(texfile).write_text( + r""" +%s +\pagestyle{empty} +\begin{document} +%% The empty hbox ensures that a page is printed even for empty inputs, except +%% when using psfrag which gets confused by it. +\fontsize{%f}{%f}%% +\ifdefined\psfrag\else\hbox{}\fi%% +%s +\end{document} +""" % (self._get_preamble(), fontsize, fontsize * 1.25, fontcmd % tex), + encoding='utf-8') + + return texfile + + def _run_checked_subprocess(self, command, tex, *, cwd=None): + _log.debug(cbook._pformat_subprocess(command)) + try: + report = subprocess.check_output( + command, cwd=cwd if cwd is not None else self.texcache, + stderr=subprocess.STDOUT) + except FileNotFoundError as exc: + raise RuntimeError( + 'Failed to process string with tex because {} could not be ' + 'found'.format(command[0])) from exc + except subprocess.CalledProcessError as exc: + raise RuntimeError( + '{prog} was not able to process the following string:\n' + '{tex!r}\n\n' + 'Here is the full report generated by {prog}:\n' + '{exc}\n\n'.format( + prog=command[0], + tex=tex.encode('unicode_escape'), + exc=exc.output.decode('utf-8'))) from exc + _log.debug(report) + return report + + def make_dvi(self, tex, fontsize): + """ + Generate a dvi file containing latex's layout of tex string. + + Return the file name. + """ + basefile = self.get_basefile(tex, fontsize) + dvifile = '%s.dvi' % basefile + if not os.path.exists(dvifile): + texfile = Path(self.make_tex(tex, fontsize)) + # Generate the dvi in a temporary directory to avoid race + # conditions e.g. if multiple processes try to process the same tex + # string at the same time. Having tmpdir be a subdirectory of the + # final output dir ensures that they are on the same filesystem, + # and thus replace() works atomically. It also allows referring to + # the texfile with a relative path (for pathological MPLCONFIGDIRs, + # the absolute path may contain characters (e.g. ~) that TeX does + # not support.) + with TemporaryDirectory(dir=Path(dvifile).parent) as tmpdir: + self._run_checked_subprocess( + ["latex", "-interaction=nonstopmode", "--halt-on-error", + f"../{texfile.name}"], tex, cwd=tmpdir) + (Path(tmpdir) / Path(dvifile).name).replace(dvifile) + return dvifile + + def make_png(self, tex, fontsize, dpi): + """ + Generate a png file containing latex's rendering of tex string. + + Return the file name. + """ + basefile = self.get_basefile(tex, fontsize, dpi) + pngfile = '%s.png' % basefile + # see get_rgba for a discussion of the background + if not os.path.exists(pngfile): + dvifile = self.make_dvi(tex, fontsize) + cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi), + "-T", "tight", "-o", pngfile, dvifile] + # When testing, disable FreeType rendering for reproducibility; but + # dvipng 1.16 has a bug (fixed in f3ff241) that breaks --freetype0 + # mode, so for it we keep FreeType enabled; the image will be + # slightly off. + if (getattr(mpl, "_called_from_pytest", False) and + mpl._get_executable_info("dvipng").raw_version != "1.16"): + cmd.insert(1, "--freetype0") + self._run_checked_subprocess(cmd, tex) + return pngfile + + def get_grey(self, tex, fontsize=None, dpi=None): + """Return the alpha channel.""" + if not fontsize: + fontsize = rcParams['font.size'] + if not dpi: + dpi = rcParams['savefig.dpi'] + key = tex, self.get_font_config(), fontsize, dpi + alpha = self._grey_arrayd.get(key) + if alpha is None: + pngfile = self.make_png(tex, fontsize, dpi) + rgba = mpl.image.imread(os.path.join(self.texcache, pngfile)) + self._grey_arrayd[key] = alpha = rgba[:, :, -1] + return alpha + + def get_rgba(self, tex, fontsize=None, dpi=None, rgb=(0, 0, 0)): + r""" + Return latex's rendering of the tex string as an rgba array. + + Examples + -------- + >>> texmanager = TexManager() + >>> s = r"\TeX\ is $\displaystyle\sum_n\frac{-e^{i\pi}}{2^n}$!" + >>> Z = texmanager.get_rgba(s, fontsize=12, dpi=80, rgb=(1, 0, 0)) + """ + alpha = self.get_grey(tex, fontsize, dpi) + rgba = np.empty((*alpha.shape, 4)) + rgba[..., :3] = mpl.colors.to_rgb(rgb) + rgba[..., -1] = alpha + return rgba + + def get_text_width_height_descent(self, tex, fontsize, renderer=None): + """Return width, height and descent of the text.""" + if tex.strip() == '': + return 0, 0, 0 + dvifile = self.make_dvi(tex, fontsize) + dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1 + with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi: + page, = dvi + # A total height (including the descent) needs to be returned. + return page.width, page.height + page.descent, page.descent diff --git a/lib/matplotlib/_vf.py b/lib/matplotlib/_vf.py new file mode 100644 index 000000000000..7beb013d5464 --- /dev/null +++ b/lib/matplotlib/_vf.py @@ -0,0 +1,123 @@ +from functools import partial +import logging + +from matplotlib.dviread import Dvi +from matplotlib._dviread import _dvistate, _fontfile, Page + + +_log = logging.getLogger(__name__) + + +class Vf(Dvi): + r""" + A virtual font (\*.vf file) containing subroutines for dvi files. + + Parameters + ---------- + filename : str or path-like + + Notes + ----- + The virtual font format is a derivative of dvi: + http://mirrors.ctan.org/info/knuth/virtual-fonts + This class reuses some of the machinery of `Dvi` + but replaces the `_read` loop and dispatch mechanism. + + Examples + -------- + :: + + vf = Vf(filename) + glyph = vf[code] + glyph.text, glyph.boxes, glyph.width + """ + + def __init__(self, filename): + super().__init__(filename, 0) + try: + self._first_font = None + self._chars = {} + self._read() + finally: + self.close() + + def __getitem__(self, code): + return self._chars[code] + + def _read(self): + """ + Read one page from the file. Return True if successful, + False if there were no more pages. + """ + packet_char, packet_ends = None, None + packet_len, packet_width = None, None + while True: + byte = self.file.read(1)[0] + # If we are in a packet, execute the dvi instructions + if self.state is _dvistate.inpage: + byte_at = self.file.tell()-1 + if byte_at == packet_ends: + self._finalize_packet(packet_char, packet_width) + packet_len, packet_char, packet_width = None, None, None + # fall through to out-of-packet code + elif byte_at > packet_ends: + raise ValueError("Packet length mismatch in vf file") + else: + if byte in (139, 140) or byte >= 243: + raise ValueError( + "Inappropriate opcode %d in vf file" % byte) + Dvi._dtable[byte](self, byte) + continue + + # We are outside a packet + if byte < 242: # a short packet (length given by byte) + packet_len = byte + packet_char, packet_width = self._arg(1), self._arg(3) + packet_ends = self._init_packet(byte) + self.state = _dvistate.inpage + elif byte == 242: # a long packet + packet_len, packet_char, packet_width = \ + [self._arg(x) for x in (4, 4, 4)] + self._init_packet(packet_len) + elif 243 <= byte <= 246: + k = self._arg(byte - 242, byte == 246) + c, s, d, a, l = [self._arg(x) for x in (4, 4, 4, 1, 1)] + self._fnt_def_real(k, c, s, d, a, l) + if self._first_font is None: + self._first_font = k + elif byte == 247: # preamble + i, k = self._arg(1), self._arg(1) + x = self.file.read(k) + cs, ds = self._arg(4), self._arg(4) + self._pre(i, x, cs, ds) + elif byte == 248: # postamble (just some number of 248s) + break + else: + raise ValueError("Unknown vf opcode %d" % byte) + + def _init_packet(self, pl): + if self.state != _dvistate.outer: + raise ValueError("Misplaced packet in vf file") + self.h, self.v, self.w, self.x, self.y, self.z = 0, 0, 0, 0, 0, 0 + self.stack, self.text, self.boxes = [], [], [] + self.f = self._first_font + return self.file.tell() + pl + + def _finalize_packet(self, packet_char, packet_width): + self._chars[packet_char] = Page( + text=self.text, boxes=self.boxes, width=packet_width, + height=None, descent=None) + self.state = _dvistate.outer + + def _pre(self, i, x, cs, ds): + if self.state is not _dvistate.pre: + raise ValueError("pre command in middle of vf file") + if i != 202: + raise ValueError("Unknown vf format %d" % i) + if len(x): + _log.debug('vf file comment: %s', x) + self.state = _dvistate.outer + # cs = checksum, ds = design size + + +_vffile = partial(_fontfile, Vf, ".vf") diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 9eb0d76a1e52..b1a10e055dba 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -26,7 +26,7 @@ from PIL import Image import matplotlib as mpl -from matplotlib import _api, _text_helpers, cbook +from matplotlib import _api, _dviread, _text_helpers, cbook, dviread from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, @@ -36,7 +36,6 @@ from matplotlib.font_manager import findfont, get_font from matplotlib.afm import AFM import matplotlib.type1font as type1font -import matplotlib.dviread as dviread from matplotlib.ft2font import (FIXED_WIDTH, ITALIC, LOAD_NO_SCALE, LOAD_NO_HINTING, KERNING_UNFITTED, FT2Font) from matplotlib.mathtext import MathTextParser @@ -891,7 +890,7 @@ def dviFontName(self, dvifont): if dvi_info is not None: return dvi_info.pdfname - tex_font_map = dviread.PsfontsMap(dviread._find_tex_file('pdftex.map')) + tex_font_map = dviread.PsfontsMap() psfont = tex_font_map[dvifont.texname] if psfont.filename is None: raise ValueError( @@ -966,7 +965,7 @@ def _embedTeXFont(self, fontinfo): fontdict['Encoding'] = { 'Type': Name('Encoding'), 'Differences': [ - 0, *map(Name, dviread._parse_enc(fontinfo.encodingfile))], + 0, *map(Name, _dviread._parse_enc(fontinfo.encodingfile))], } # If no file is specified, stop short diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 33c41202dbb4..1bdb2ab1b2ab 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -33,7 +33,7 @@ from matplotlib.mathtext import MathTextParser from matplotlib._mathtext_data import uni2type1 from matplotlib.path import Path -from matplotlib.texmanager import TexManager +from matplotlib._texmanager import TexManager from matplotlib.transforms import Affine2D from matplotlib.backends.backend_mixed import MixedModeRenderer from . import _backend_pdf_ps diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 7f90a13f1086..0cc584bf6198 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -18,22 +18,36 @@ """ from collections import namedtuple +from functools import lru_cache, partial import enum -from functools import lru_cache, partial, wraps import logging import os -from pathlib import Path import re -import struct -import subprocess -import sys import numpy as np -from matplotlib import _api, cbook +from matplotlib import _api, _dviread _log = logging.getLogger(__name__) + +@_api.caching_module_getattr +class __getattr__: + locals().update({ + name: _api.deprecated("3.6")( + property(lambda self, _mod=_dviread, + _name=name: getattr(_mod, _name))) + for name in ["Book", "Page", "Text", "DviFont", "Tfm", + "find_tex_file"]}) + + +@_api.deprecated("3.6") +class Vf: + def __init__(self, filename): + from matplotlib import _vf + return _vf.Vf(filename) + + # 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(). @@ -56,123 +70,6 @@ _dvistate = enum.Enum('DviState', 'pre outer inpage post_post finale') -# The marks on a page consist of text and boxes. A page also has dimensions. -Page = namedtuple('Page', 'text boxes height width descent') -Text = namedtuple('Text', 'x y font glyph width') -Box = namedtuple('Box', 'x y height width') - - -# Opcode argument parsing -# -# Each of the following functions takes a Dvi object and delta, -# which is the difference between the opcode and the minimum opcode -# with the same meaning. Dvi opcodes often encode the number of -# argument bytes in this delta. - -def _arg_raw(dvi, delta): - """Return *delta* without reading anything more from the dvi file.""" - return delta - - -def _arg(nbytes, signed, dvi, _): - """ - Read *nbytes* bytes, returning the bytes interpreted as a signed integer - if *signed* is true, unsigned otherwise. - """ - return dvi._arg(nbytes, signed) - - -def _arg_slen(dvi, delta): - """ - Read *delta* bytes, returning None if *delta* is zero, and the bytes - interpreted as a signed integer otherwise. - """ - if delta == 0: - return None - return dvi._arg(delta, True) - - -def _arg_slen1(dvi, delta): - """ - Read *delta*+1 bytes, returning the bytes interpreted as signed. - """ - return dvi._arg(delta + 1, True) - - -def _arg_ulen1(dvi, delta): - """ - Read *delta*+1 bytes, returning the bytes interpreted as unsigned. - """ - return dvi._arg(delta + 1, False) - - -def _arg_olen1(dvi, delta): - """ - Read *delta*+1 bytes, returning the bytes interpreted as - unsigned integer for 0<=*delta*<3 and signed if *delta*==3. - """ - return dvi._arg(delta + 1, delta == 3) - - -_arg_mapping = dict(raw=_arg_raw, - u1=partial(_arg, 1, False), - u4=partial(_arg, 4, False), - s4=partial(_arg, 4, True), - slen=_arg_slen, - olen1=_arg_olen1, - slen1=_arg_slen1, - ulen1=_arg_ulen1) - - -def _dispatch(table, min, max=None, state=None, args=('raw',)): - """ - Decorator for dispatch by opcode. Sets the values in *table* - from *min* to *max* to this method, adds a check that the Dvi state - matches *state* if not None, reads arguments from the file according - to *args*. - - Parameters - ---------- - table : dict[int, callable] - The dispatch table to be filled in. - - min, max : int - Range of opcodes that calls the registered function; *max* defaults to - *min*. - - state : _dvistate, optional - State of the Dvi object in which these opcodes are allowed. - - args : list[str], default: ['raw'] - Sequence of argument specifications: - - - 'raw': opcode minus minimum - - 'u1': read one unsigned byte - - 'u4': read four bytes, treat as an unsigned number - - 's4': read four bytes, treat as a signed number - - 'slen': read (opcode - minimum) bytes, treat as signed - - 'slen1': read (opcode - minimum + 1) bytes, treat as signed - - 'ulen1': read (opcode - minimum + 1) bytes, treat as unsigned - - 'olen1': read (opcode - minimum + 1) bytes, treat as unsigned - if under four bytes, signed if four bytes - """ - def decorate(method): - get_args = [_arg_mapping[x] for x in args] - - @wraps(method) - def wrapper(self, byte): - if state is not None and self.state != state: - raise ValueError("state precondition failed") - return method(self, *[f(self, byte-min) for f in get_args]) - if max is None: - table[min] = wrapper - else: - for i in range(min, max+1): - assert table[i] is None - table[i] = wrapper - return wrapper - return decorate - class Dvi: """ @@ -191,7 +88,7 @@ class Dvi: """ # dispatch table _dtable = [None] * 256 - _dispatch = partial(_dispatch, _dtable) + _dispatch = partial(_dviread._dispatch, _dtable) def __init__(self, filename, dpi): """ @@ -250,7 +147,7 @@ def _output(self): minx, miny, maxx, maxy = np.inf, np.inf, -np.inf, -np.inf maxy_pure = -np.inf for elt in self.text + self.boxes: - if isinstance(elt, Box): + if isinstance(elt, _dviread.Box): x, y, h, w = elt e = 0 # zero depth else: # glyph @@ -266,25 +163,26 @@ def _output(self): self._baseline_v = None if not self.text and not self.boxes: # Avoid infs/nans from inf+/-inf. - return Page(text=[], boxes=[], width=0, height=0, descent=0) + return _dviread.Page(text=[], boxes=[], width=0, + height=0, descent=0) if self.dpi is None: # special case for ease of debugging: output raw dvi coordinates - return Page(text=self.text, boxes=self.boxes, - width=maxx-minx, height=maxy_pure-miny, - descent=maxy-maxy_pure) + return _dviread.Page(text=self.text, boxes=self.boxes, + width=maxx-minx, height=maxy_pure-miny, + descent=maxy-maxy_pure) # convert from TeX's "scaled points" to dpi units d = self.dpi / (72.27 * 2**16) descent = (maxy - maxy_pure) * d - text = [Text((x-minx)*d, (maxy-y)*d - descent, f, g, w*d) + text = [_dviread.Text((x-minx)*d, (maxy-y)*d - descent, f, g, w*d) for (x, y, f, g, w) in self.text] - boxes = [Box((x-minx)*d, (maxy-y)*d - descent, h*d, w*d) + boxes = [_dviread.Box((x-minx)*d, (maxy-y)*d - descent, h*d, w*d) for (x, y, h, w) in self.boxes] - return Page(text=text, boxes=boxes, width=(maxx-minx)*d, - height=(maxy_pure-miny)*d, descent=descent) + return _dviread.Page(text=text, boxes=boxes, width=(maxx-minx)*d, + height=(maxy_pure-miny)*d, descent=descent) def _read(self): """ @@ -366,20 +264,24 @@ def _put_char(self, char): def _put_char_real(self, char): font = self.fonts[self.f] if font._vf is None: - self.text.append(Text(self.h, self.v, font, char, - font._width_of(char))) + self.text.append(_dviread.Text(self.h, self.v, font, char, + font._width_of(char))) else: scale = font._scale for x, y, f, g, w in font._vf[char].text: - newf = DviFont(scale=_mul2012(scale, f._scale), - tfm=f._tfm, texname=f.texname, vf=f._vf) - self.text.append(Text(self.h + _mul2012(x, scale), - self.v + _mul2012(y, scale), - newf, g, newf._width_of(g))) - self.boxes.extend([Box(self.h + _mul2012(x, scale), - self.v + _mul2012(y, scale), - _mul2012(a, scale), _mul2012(b, scale)) - for x, y, a, b in font._vf[char].boxes]) + newf = _dviread.DviFont( + scale=_dviread._mul2012(scale, f._scale), + tfm=f._tfm, texname=f.texname, vf=f._vf) + self.text.append(_dviread.Text( + self.h + _dviread._mul2012(x, scale), + self.v + _dviread._mul2012(y, scale), + newf, g, newf._width_of(g))) + self.boxes.extend( + [_dviread.Box(self.h + _dviread._mul2012(x, scale), + self.v + _dviread._mul2012(y, scale), + _dviread._mul2012(a, scale), + _dviread._mul2012(b, scale)) + for x, y, a, b in font._vf[char].boxes]) @_dispatch(137, state=_dvistate.inpage, args=('s4', 's4')) def _put_rule(self, a, b): @@ -387,7 +289,7 @@ def _put_rule(self, a, b): def _put_rule_real(self, a, b): if a > 0 and b > 0: - self.boxes.append(Box(self.h, self.v, a, b)) + self.boxes.append(_dviread.Box(self.h, self.v, a, b)) @_dispatch(138) def _nop(self, _): @@ -469,14 +371,15 @@ def _fnt_def(self, k, c, s, d, a, l): def _fnt_def_real(self, k, c, s, d, a, l): n = self.file.read(a + l) fontname = n[-l:].decode('ascii') - tfm = _tfmfile(fontname) + tfm = _dviread._tfmfile(fontname) if c != 0 and tfm.checksum != 0 and c != tfm.checksum: raise ValueError('tfm checksum mismatch: %s' % n) try: + from matplotlib._vf import _vffile vf = _vffile(fontname) except FileNotFoundError: vf = None - self.fonts[k] = DviFont(scale=s, tfm=tfm, texname=n, vf=vf) + self.fonts[k] = _dviread.DviFont(scale=s, tfm=tfm, texname=n, vf=vf) @_dispatch(247, state=_dvistate.pre, args=('u1', 'u4', 'u4', 'u4', 'u1')) def _pre(self, i, num, den, mag, k): @@ -511,261 +414,6 @@ def _malformed(self, offset): raise ValueError(f"unknown command: byte {250 + offset}") -class DviFont: - """ - Encapsulation of a font that a DVI file can refer to. - - This class holds a font's texname and size, supports comparison, - and knows the widths of glyphs in the same units as the AFM file. - There are also internal attributes (for use by dviread.py) that - are *not* used for comparison. - - The size is in Adobe points (converted from TeX points). - - Parameters - ---------- - scale : float - Factor by which the font is scaled from its natural size. - tfm : Tfm - TeX font metrics for this font - texname : bytes - Name of the font as used internally by TeX and friends, as an ASCII - bytestring. This is usually very different from any external font - names; `PsfontsMap` can be used to find the external name of the font. - vf : Vf - A TeX "virtual font" file, or None if this font is not virtual. - - Attributes - ---------- - texname : bytes - size : float - Size of the font in Adobe points, converted from the slightly - smaller TeX points. - widths : list - Widths of glyphs in glyph-space units, typically 1/1000ths of - the point size. - - """ - __slots__ = ('texname', 'size', 'widths', '_scale', '_vf', '_tfm') - - def __init__(self, scale, tfm, texname, vf): - _api.check_isinstance(bytes, texname=texname) - self._scale = scale - self._tfm = tfm - self.texname = texname - self._vf = vf - self.size = scale * (72.0 / (72.27 * 2**16)) - try: - nchars = max(tfm.width) + 1 - except ValueError: - nchars = 0 - self.widths = [(1000*tfm.width.get(char, 0)) >> 20 - for char in range(nchars)] - - def __eq__(self, other): - return (type(self) == type(other) - and self.texname == other.texname and self.size == other.size) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "<{}: {}>".format(type(self).__name__, self.texname) - - def _width_of(self, char): - """Width of char in dvi units.""" - width = self._tfm.width.get(char, None) - if width is not None: - return _mul2012(width, self._scale) - _log.debug('No width for char %d in font %s.', char, self.texname) - return 0 - - def _height_depth_of(self, char): - """Height and depth of char in dvi units.""" - result = [] - for metric, name in ((self._tfm.height, "height"), - (self._tfm.depth, "depth")): - value = metric.get(char, None) - if value is None: - _log.debug('No %s for char %d in font %s', - name, char, self.texname) - result.append(0) - else: - result.append(_mul2012(value, self._scale)) - # cmsyXX (symbols font) glyph 0 ("minus") has a nonzero descent - # so that TeX aligns equations properly - # (https://tex.stackexchange.com/q/526103/) - # but we actually care about the rasterization depth to align - # the dvipng-generated images. - if re.match(br'^cmsy\d+$', self.texname) and char == 0: - result[-1] = 0 - return result - - -class Vf(Dvi): - r""" - A virtual font (\*.vf file) containing subroutines for dvi files. - - Parameters - ---------- - filename : str or path-like - - Notes - ----- - The virtual font format is a derivative of dvi: - http://mirrors.ctan.org/info/knuth/virtual-fonts - This class reuses some of the machinery of `Dvi` - but replaces the `_read` loop and dispatch mechanism. - - Examples - -------- - :: - - vf = Vf(filename) - glyph = vf[code] - glyph.text, glyph.boxes, glyph.width - """ - - def __init__(self, filename): - super().__init__(filename, 0) - try: - self._first_font = None - self._chars = {} - self._read() - finally: - self.close() - - def __getitem__(self, code): - return self._chars[code] - - def _read(self): - """ - Read one page from the file. Return True if successful, - False if there were no more pages. - """ - packet_char, packet_ends = None, None - packet_len, packet_width = None, None - while True: - byte = self.file.read(1)[0] - # If we are in a packet, execute the dvi instructions - if self.state is _dvistate.inpage: - byte_at = self.file.tell()-1 - if byte_at == packet_ends: - self._finalize_packet(packet_char, packet_width) - packet_len, packet_char, packet_width = None, None, None - # fall through to out-of-packet code - elif byte_at > packet_ends: - raise ValueError("Packet length mismatch in vf file") - else: - if byte in (139, 140) or byte >= 243: - raise ValueError( - "Inappropriate opcode %d in vf file" % byte) - Dvi._dtable[byte](self, byte) - continue - - # We are outside a packet - if byte < 242: # a short packet (length given by byte) - packet_len = byte - packet_char, packet_width = self._arg(1), self._arg(3) - packet_ends = self._init_packet(byte) - self.state = _dvistate.inpage - elif byte == 242: # a long packet - packet_len, packet_char, packet_width = \ - [self._arg(x) for x in (4, 4, 4)] - self._init_packet(packet_len) - elif 243 <= byte <= 246: - k = self._arg(byte - 242, byte == 246) - c, s, d, a, l = [self._arg(x) for x in (4, 4, 4, 1, 1)] - self._fnt_def_real(k, c, s, d, a, l) - if self._first_font is None: - self._first_font = k - elif byte == 247: # preamble - i, k = self._arg(1), self._arg(1) - x = self.file.read(k) - cs, ds = self._arg(4), self._arg(4) - self._pre(i, x, cs, ds) - elif byte == 248: # postamble (just some number of 248s) - break - else: - raise ValueError("Unknown vf opcode %d" % byte) - - def _init_packet(self, pl): - if self.state != _dvistate.outer: - raise ValueError("Misplaced packet in vf file") - self.h, self.v, self.w, self.x, self.y, self.z = 0, 0, 0, 0, 0, 0 - self.stack, self.text, self.boxes = [], [], [] - self.f = self._first_font - return self.file.tell() + pl - - def _finalize_packet(self, packet_char, packet_width): - self._chars[packet_char] = Page( - text=self.text, boxes=self.boxes, width=packet_width, - height=None, descent=None) - self.state = _dvistate.outer - - def _pre(self, i, x, cs, ds): - if self.state is not _dvistate.pre: - raise ValueError("pre command in middle of vf file") - if i != 202: - raise ValueError("Unknown vf format %d" % i) - if len(x): - _log.debug('vf file comment: %s', x) - self.state = _dvistate.outer - # cs = checksum, ds = design size - - -def _mul2012(num1, num2): - """Multiply two numbers in 20.12 fixed point format.""" - # Separated into a function because >> has surprising precedence - return (num1*num2) >> 20 - - -class Tfm: - """ - A TeX Font Metric file. - - This implementation covers only the bare minimum needed by the Dvi class. - - Parameters - ---------- - filename : str or path-like - - Attributes - ---------- - checksum : int - Used for verifying against the dvi file. - design_size : int - Design size of the font (unknown units) - width, height, depth : dict - Dimensions of each character, need to be scaled by the factor - specified in the dvi file. These are dicts because indexing may - not start from 0. - """ - __slots__ = ('checksum', 'design_size', 'width', 'height', 'depth') - - def __init__(self, filename): - _log.debug('opening tfm file %s', filename) - with open(filename, 'rb') as file: - header1 = file.read(24) - lh, bc, ec, nw, nh, nd = struct.unpack('!6H', header1[2:14]) - _log.debug('lh=%d, bc=%d, ec=%d, nw=%d, nh=%d, nd=%d', - lh, bc, ec, nw, nh, nd) - header2 = file.read(4*lh) - self.checksum, self.design_size = struct.unpack('!2I', header2[:8]) - # there is also encoding information etc. - char_info = file.read(4*(ec-bc+1)) - widths = struct.unpack(f'!{nw}i', file.read(4*nw)) - heights = struct.unpack(f'!{nh}i', file.read(4*nh)) - depths = struct.unpack(f'!{nd}i', file.read(4*nd)) - self.width, self.height, self.depth = {}, {}, {} - for idx, char in enumerate(range(bc, ec+1)): - byte0 = char_info[4*idx] - byte1 = char_info[4*idx+1] - self.width[char] = widths[byte0] - self.height[char] = heights[byte1 >> 4] - self.depth[char] = depths[byte1 & 0xf] - - PsFont = namedtuple('PsFont', 'texname psname effects encoding filename') @@ -777,6 +425,10 @@ class PsfontsMap: ---------- filename : str or path-like + find_tex_file : bool (default False) + If ``True``, *filename* is looked up in the tex build directory. + If ``False`` (default), *filename* must be a fully qualified path. + Notes ----- For historical reasons, TeX knows many Type-1 fonts by different @@ -801,7 +453,7 @@ class PsfontsMap: Examples -------- - >>> map = PsfontsMap(find_tex_file('pdftex.map')) + >>> map = PsfontsMap('pdftex.map', find_tex_file=True) >>> entry = map[b'ptmbo8r'] >>> entry.texname b'ptmbo8r' @@ -819,9 +471,12 @@ class PsfontsMap: # `PsfontsMap(filename)` with the same filename a second time immediately # returns the same object. @lru_cache() - def __new__(cls, filename): + def __new__(cls, filename, *, find_tex_file=False): self = object.__new__(cls) - self._filename = os.fsdecode(filename) + if find_tex_file: + self._filename = os.fsdecode(_dviread.find_tex_file(filename)) + else: + self._filename = os.fsdecode(filename) # Some TeX distributions have enormous pdftex.map files which would # take hundreds of milliseconds to parse, but it is easy enough to just # store the unparsed lines (keyed by the first word, which is the @@ -937,159 +592,15 @@ def _parse_and_cache_line(self, line): if basename is None: basename = tfmname if encodingfile is not None: - encodingfile = _find_tex_file(encodingfile) + encodingfile = _dviread._find_tex_file(encodingfile) if fontfile is not None: - fontfile = _find_tex_file(fontfile) + fontfile = _dviread._find_tex_file(fontfile) self._parsed[tfmname] = PsFont( texname=tfmname, psname=basename, effects=effects, encoding=encodingfile, filename=fontfile) return True -def _parse_enc(path): - r""" - Parse a \*.enc file referenced from a psfonts.map style file. - - The format supported by this function is a tiny subset of PostScript. - - Parameters - ---------- - path : os.PathLike - - Returns - ------- - list - The nth entry of the list is the PostScript glyph name of the nth - glyph. - """ - no_comments = re.sub("%.*", "", Path(path).read_text(encoding="ascii")) - array = re.search(r"(?s)\[(.*)\]", no_comments).group(1) - lines = [line for line in array.split() if line] - if all(line.startswith("/") for line in lines): - return [line[1:] for line in lines] - else: - raise ValueError( - "Failed to parse {} as Postscript encoding".format(path)) - - -class _LuatexKpsewhich: - @lru_cache() # A singleton. - def __new__(cls): - self = object.__new__(cls) - self._proc = self._new_proc() - return self - - def _new_proc(self): - return subprocess.Popen( - ["luatex", "--luaonly", - str(cbook._get_data_path("kpsewhich.lua"))], - stdin=subprocess.PIPE, stdout=subprocess.PIPE) - - def search(self, filename): - if self._proc.poll() is not None: # Dead, restart it. - self._proc = self._new_proc() - self._proc.stdin.write(os.fsencode(filename) + b"\n") - self._proc.stdin.flush() - out = self._proc.stdout.readline().rstrip() - return None if out == b"nil" else os.fsdecode(out) - - -@lru_cache() -@_api.delete_parameter("3.5", "format") -def _find_tex_file(filename, format=None): - """ - Find a file in the texmf tree using kpathsea_. - - The kpathsea library, provided by most existing TeX distributions, both - on Unix-like systems and on Windows (MikTeX), is invoked via a long-lived - luatex process if luatex is installed, or via kpsewhich otherwise. - - .. _kpathsea: https://www.tug.org/kpathsea/ - - Parameters - ---------- - filename : str or path-like - format : str or bytes - Used as the value of the ``--format`` option to :program:`kpsewhich`. - Could be e.g. 'tfm' or 'vf' to limit the search to that type of files. - Deprecated. - - Raises - ------ - FileNotFoundError - If the file is not found. - """ - - # we expect these to always be ascii encoded, but use utf-8 - # out of caution - if isinstance(filename, bytes): - filename = filename.decode('utf-8', errors='replace') - if isinstance(format, bytes): - format = format.decode('utf-8', errors='replace') - - try: - lk = _LuatexKpsewhich() - except FileNotFoundError: - lk = None # Fallback to directly calling kpsewhich, as below. - - if lk and format is None: - path = lk.search(filename) - - else: - if os.name == 'nt': - # On Windows only, kpathsea can use utf-8 for cmd args and output. - # The `command_line_encoding` environment variable is set to force - # it to always use utf-8 encoding. See Matplotlib issue #11848. - kwargs = {'env': {**os.environ, 'command_line_encoding': 'utf-8'}, - 'encoding': 'utf-8'} - else: # On POSIX, run through the equivalent of os.fsdecode(). - kwargs = {'encoding': sys.getfilesystemencoding(), - 'errors': 'surrogateescape'} - - cmd = ['kpsewhich'] - if format is not None: - cmd += ['--format=' + format] - cmd += [filename] - try: - path = (cbook._check_and_log_subprocess(cmd, _log, **kwargs) - .rstrip('\n')) - except (FileNotFoundError, RuntimeError): - path = None - - if path: - return path - else: - raise FileNotFoundError( - f"Matplotlib's TeX implementation searched for a file named " - f"{filename!r} in your texmf tree, but could not find it") - - -# After the deprecation period elapses, delete this shim and rename -# _find_tex_file to find_tex_file everywhere. -@_api.delete_parameter("3.5", "format") -def find_tex_file(filename, format=None): - try: - return (_find_tex_file(filename, format) if format is not None else - _find_tex_file(filename)) - except FileNotFoundError as exc: - _api.warn_deprecated( - "3.6", message=f"{exc.args[0]}; in the future, this will raise a " - f"FileNotFoundError.") - return "" - - -find_tex_file.__doc__ = _find_tex_file.__doc__ - - -@lru_cache() -def _fontfile(cls, suffix, texname): - return cls(_find_tex_file(texname + suffix)) - - -_tfmfile = partial(_fontfile, Tfm, ".tfm") -_vffile = partial(_fontfile, Vf, ".vf") - - if __name__ == '__main__': from argparse import ArgumentParser import itertools @@ -1099,7 +610,7 @@ def _fontfile(cls, suffix, texname): parser.add_argument("dpi", nargs="?", type=float, default=None) args = parser.parse_args() with Dvi(args.filename, args.dpi) as dvi: - fontmap = PsfontsMap(_find_tex_file('pdftex.map')) + fontmap = PsfontsMap("pdftex.map", find_tex_file=True) for page in dvi: print(f"=== new page === " f"(w: {page.width}, h: {page.height}, d: {page.descent})") diff --git a/lib/matplotlib/mpl-data/kpsewhich.lua b/lib/matplotlib/mpl-data/kpsewhich.lua index be07b5858ae8..4ea2eb1d9995 100644 --- a/lib/matplotlib/mpl-data/kpsewhich.lua +++ b/lib/matplotlib/mpl-data/kpsewhich.lua @@ -1,3 +1,3 @@ --- see dviread._LuatexKpsewhich +-- see _dviread._LuatexKpsewhich kpse.set_program_name("tex") while true do print(kpse.lookup(io.read():gsub("\r", ""))); io.flush(); end diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index 754277c41f43..9f279cd62d5c 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -79,7 +79,7 @@ def _check_for_pgf(texsystem): def _has_tex_package(package): try: - mpl.dviread._find_tex_file(f"{package}.sty") + mpl._dviread._find_tex_file(f"{package}.sty") return True except FileNotFoundError: return False diff --git a/lib/matplotlib/tests/test_dviread.py b/lib/matplotlib/tests/test_dviread.py index 7e10975f44d5..f4e54007e9b6 100644 --- a/lib/matplotlib/tests/test_dviread.py +++ b/lib/matplotlib/tests/test_dviread.py @@ -3,14 +3,19 @@ import shutil import matplotlib.dviread as dr +import matplotlib._dviread as _dr import pytest def test_PsfontsMap(monkeypatch): - monkeypatch.setattr(dr, '_find_tex_file', lambda x: x) + monkeypatch.setattr(_dr, '_find_tex_file', lambda x: x) filename = str(Path(__file__).parent / 'baseline_images/dviread/test.map') - fontmap = dr.PsfontsMap(filename) + fontmap = _dr.PsfontsMap(filename) + fontmap2 = dr.get_tex_font_map(filename) + + assert fontmap == fontmap2 + # Check all properties of a few fonts for n in [1, 2, 3, 4, 5]: key = b'TeXfont%d' % n diff --git a/lib/matplotlib/tests/test_texmanager.py b/lib/matplotlib/tests/test_texmanager.py index 5f98112043bb..503ccf0b6d05 100644 --- a/lib/matplotlib/tests/test_texmanager.py +++ b/lib/matplotlib/tests/test_texmanager.py @@ -2,7 +2,7 @@ import re import matplotlib.pyplot as plt -from matplotlib.texmanager import TexManager +from matplotlib._texmanager import TexManager import pytest diff --git a/lib/matplotlib/tests/test_usetex.py b/lib/matplotlib/tests/test_usetex.py index b726f9c26cfb..770599a1a571 100644 --- a/lib/matplotlib/tests/test_usetex.py +++ b/lib/matplotlib/tests/test_usetex.py @@ -4,7 +4,7 @@ import pytest import matplotlib as mpl -from matplotlib import dviread +from matplotlib import _dviread from matplotlib.testing import _has_tex_package from matplotlib.testing.decorators import check_figures_equal, image_comparison import matplotlib.pyplot as plt @@ -129,8 +129,8 @@ def test_usetex_with_underscore(): def test_missing_psfont(fmt, monkeypatch): """An error is raised if a TeX font lacks a Type-1 equivalent""" monkeypatch.setattr( - dviread.PsfontsMap, '__getitem__', - lambda self, k: dviread.PsFont( + _dviread.PsfontsMap, '__getitem__', + lambda self, k: _dviread.PsFont( texname='texfont', psname='Some Font', effects=None, encoding=None, filename=None)) mpl.rcParams['text.usetex'] = True diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index c9960c8c14cd..e9c910192470 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -1,338 +1,5 @@ -r""" -Support for embedded TeX expressions in Matplotlib. - -Requirements: - -* LaTeX. -* \*Agg backends: dvipng>=1.6. -* PS backend: PSfrag, dvips, and Ghostscript>=9.0. -* PDF and SVG backends: if LuaTeX is present, it will be used to speed up some - post-processing steps, but note that it is not used to parse the TeX string - itself (only LaTeX is supported). - -To enable TeX rendering of all text in your Matplotlib figure, set -:rc:`text.usetex` to True. - -TeX and dvipng/dvips processing results are cached -in ~/.matplotlib/tex.cache for reuse between sessions. - -`TexManager.get_rgba` can also be used to directly obtain raster output as RGBA -NumPy arrays. -""" - -import functools -import hashlib -import logging -import os -from pathlib import Path -import subprocess -from tempfile import TemporaryDirectory - -import numpy as np - -import matplotlib as mpl -from matplotlib import _api, cbook, dviread, rcParams - -_log = logging.getLogger(__name__) - - -def _usepackage_if_not_loaded(package, *, option=None): - """ - Output LaTeX code that loads a package (possibly with an option) if it - hasn't been loaded yet. - - LaTeX cannot load twice a package with different options, so this helper - can be used to protect against users loading arbitrary packages/options in - their custom preamble. - """ - option = f"[{option}]" if option is not None else "" - return ( - r"\makeatletter" - r"\@ifpackageloaded{%(package)s}{}{\usepackage%(option)s{%(package)s}}" - r"\makeatother" - ) % {"package": package, "option": option} - - -class TexManager: - """ - Convert strings to dvi files using TeX, caching the results to a directory. - - Repeated calls to this constructor always return the same instance. - """ - - texcache = os.path.join(mpl.get_cachedir(), 'tex.cache') - - _grey_arrayd = {} - _font_family = 'serif' - _font_families = ('serif', 'sans-serif', 'cursive', 'monospace') - _font_info = { - 'new century schoolbook': ('pnc', r'\renewcommand{\rmdefault}{pnc}'), - 'bookman': ('pbk', r'\renewcommand{\rmdefault}{pbk}'), - 'times': ('ptm', r'\usepackage{mathptmx}'), - 'palatino': ('ppl', r'\usepackage{mathpazo}'), - 'zapf chancery': ('pzc', r'\usepackage{chancery}'), - 'cursive': ('pzc', r'\usepackage{chancery}'), - 'charter': ('pch', r'\usepackage{charter}'), - 'serif': ('cmr', ''), - 'sans-serif': ('cmss', ''), - 'helvetica': ('phv', r'\usepackage{helvet}'), - 'avant garde': ('pag', r'\usepackage{avant}'), - 'courier': ('pcr', r'\usepackage{courier}'), - # Loading the type1ec package ensures that cm-super is installed, which - # is necessary for unicode computer modern. (It also allows the use of - # computer modern at arbitrary sizes, but that's just a side effect.) - 'monospace': ('cmtt', r'\usepackage{type1ec}'), - 'computer modern roman': ('cmr', r'\usepackage{type1ec}'), - 'computer modern sans serif': ('cmss', r'\usepackage{type1ec}'), - 'computer modern typewriter': ('cmtt', r'\usepackage{type1ec}')} - _font_types = { - 'new century schoolbook': 'serif', 'bookman': 'serif', - 'times': 'serif', 'palatino': 'serif', 'charter': 'serif', - 'computer modern roman': 'serif', 'zapf chancery': 'cursive', - 'helvetica': 'sans-serif', 'avant garde': 'sans-serif', - 'computer modern sans serif': 'sans-serif', - 'courier': 'monospace', 'computer modern typewriter': 'monospace'} - - grey_arrayd = _api.deprecate_privatize_attribute("3.5") - font_family = _api.deprecate_privatize_attribute("3.5") - font_families = _api.deprecate_privatize_attribute("3.5") - font_info = _api.deprecate_privatize_attribute("3.5") - - @functools.lru_cache() # Always return the same instance. - def __new__(cls): - Path(cls.texcache).mkdir(parents=True, exist_ok=True) - return object.__new__(cls) - - def get_font_config(self): - ff = rcParams['font.family'] - ff_val = ff[0].lower() if len(ff) == 1 else None - reduced_notation = False - if len(ff) == 1 and ff_val in self._font_families: - self._font_family = ff_val - elif len(ff) == 1 and ff_val in self._font_info: - reduced_notation = True - self._font_family = self._font_types[ff_val] - else: - _log.info('font.family must be one of (%s) when text.usetex is ' - 'True. serif will be used by default.', - ', '.join(self._font_families)) - self._font_family = 'serif' - - fontconfig = [self._font_family] - fonts = {} - for font_family in self._font_families: - if reduced_notation and self._font_family == font_family: - fonts[font_family] = self._font_info[ff_val] - else: - for font in rcParams['font.' + font_family]: - if font.lower() in self._font_info: - fonts[font_family] = self._font_info[font.lower()] - _log.debug( - 'family: %s, font: %s, info: %s', - font_family, font, self._font_info[font.lower()]) - break - else: - _log.debug('%s font is not compatible with usetex.', - font) - else: - _log.info('No LaTeX-compatible font found for the %s font' - 'family in rcParams. Using default.', - font_family) - fonts[font_family] = self._font_info[font_family] - fontconfig.append(fonts[font_family][0]) - # Add a hash of the latex preamble to fontconfig so that the - # correct png is selected for strings rendered with same font and dpi - # even if the latex preamble changes within the session - preamble_bytes = self.get_custom_preamble().encode('utf-8') - fontconfig.append(hashlib.md5(preamble_bytes).hexdigest()) - - # The following packages and commands need to be included in the latex - # file's preamble: - cmd = {fonts[family][1] - for family in ['serif', 'sans-serif', 'monospace']} - if self._font_family == 'cursive': - cmd.add(fonts['cursive'][1]) - cmd.add(r'\usepackage{type1cm}') - self._font_preamble = '\n'.join(sorted(cmd)) - - return ''.join(fontconfig) - - def get_basefile(self, tex, fontsize, dpi=None): - """ - Return a filename based on a hash of the string, fontsize, and dpi. - """ - s = ''.join([tex, self.get_font_config(), '%f' % fontsize, - self.get_custom_preamble(), str(dpi or '')]) - return os.path.join( - self.texcache, hashlib.md5(s.encode('utf-8')).hexdigest()) - - def get_font_preamble(self): - """ - Return a string containing font configuration for the tex preamble. - """ - return self._font_preamble - - def get_custom_preamble(self): - """Return a string containing user additions to the tex preamble.""" - return rcParams['text.latex.preamble'] - - def _get_preamble(self): - return "\n".join([ - r"\documentclass{article}", - # Pass-through \mathdefault, which is used in non-usetex mode to - # use the default text font but was historically suppressed in - # usetex mode. - r"\newcommand{\mathdefault}[1]{#1}", - self._font_preamble, - r"\usepackage[utf8]{inputenc}", - r"\DeclareUnicodeCharacter{2212}{\ensuremath{-}}", - # geometry is loaded before the custom preamble as convert_psfrags - # relies on a custom preamble to change the geometry. - r"\usepackage[papersize=72in, margin=1in]{geometry}", - self.get_custom_preamble(), - # Use `underscore` package to take care of underscores in text - # The [strings] option allows to use underscores in file names - _usepackage_if_not_loaded("underscore", option="strings"), - # Custom packages (e.g. newtxtext) may already have loaded textcomp - # with different options. - _usepackage_if_not_loaded("textcomp"), - ]) - - def make_tex(self, tex, fontsize): - """ - Generate a tex file to render the tex string at a specific font size. - - Return the file name. - """ - basefile = self.get_basefile(tex, fontsize) - texfile = '%s.tex' % basefile - fontcmd = {'sans-serif': r'{\sffamily %s}', - 'monospace': r'{\ttfamily %s}'}.get(self._font_family, - r'{\rmfamily %s}') - - Path(texfile).write_text( - r""" -%s -\pagestyle{empty} -\begin{document} -%% The empty hbox ensures that a page is printed even for empty inputs, except -%% when using psfrag which gets confused by it. -\fontsize{%f}{%f}%% -\ifdefined\psfrag\else\hbox{}\fi%% -%s -\end{document} -""" % (self._get_preamble(), fontsize, fontsize * 1.25, fontcmd % tex), - encoding='utf-8') - - return texfile - - def _run_checked_subprocess(self, command, tex, *, cwd=None): - _log.debug(cbook._pformat_subprocess(command)) - try: - report = subprocess.check_output( - command, cwd=cwd if cwd is not None else self.texcache, - stderr=subprocess.STDOUT) - except FileNotFoundError as exc: - raise RuntimeError( - 'Failed to process string with tex because {} could not be ' - 'found'.format(command[0])) from exc - except subprocess.CalledProcessError as exc: - raise RuntimeError( - '{prog} was not able to process the following string:\n' - '{tex!r}\n\n' - 'Here is the full report generated by {prog}:\n' - '{exc}\n\n'.format( - prog=command[0], - tex=tex.encode('unicode_escape'), - exc=exc.output.decode('utf-8'))) from exc - _log.debug(report) - return report - - def make_dvi(self, tex, fontsize): - """ - Generate a dvi file containing latex's layout of tex string. - - Return the file name. - """ - basefile = self.get_basefile(tex, fontsize) - dvifile = '%s.dvi' % basefile - if not os.path.exists(dvifile): - texfile = Path(self.make_tex(tex, fontsize)) - # Generate the dvi in a temporary directory to avoid race - # conditions e.g. if multiple processes try to process the same tex - # string at the same time. Having tmpdir be a subdirectory of the - # final output dir ensures that they are on the same filesystem, - # and thus replace() works atomically. It also allows referring to - # the texfile with a relative path (for pathological MPLCONFIGDIRs, - # the absolute path may contain characters (e.g. ~) that TeX does - # not support.) - with TemporaryDirectory(dir=Path(dvifile).parent) as tmpdir: - self._run_checked_subprocess( - ["latex", "-interaction=nonstopmode", "--halt-on-error", - f"../{texfile.name}"], tex, cwd=tmpdir) - (Path(tmpdir) / Path(dvifile).name).replace(dvifile) - return dvifile - - def make_png(self, tex, fontsize, dpi): - """ - Generate a png file containing latex's rendering of tex string. - - Return the file name. - """ - basefile = self.get_basefile(tex, fontsize, dpi) - pngfile = '%s.png' % basefile - # see get_rgba for a discussion of the background - if not os.path.exists(pngfile): - dvifile = self.make_dvi(tex, fontsize) - cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi), - "-T", "tight", "-o", pngfile, dvifile] - # When testing, disable FreeType rendering for reproducibility; but - # dvipng 1.16 has a bug (fixed in f3ff241) that breaks --freetype0 - # mode, so for it we keep FreeType enabled; the image will be - # slightly off. - if (getattr(mpl, "_called_from_pytest", False) and - mpl._get_executable_info("dvipng").raw_version != "1.16"): - cmd.insert(1, "--freetype0") - self._run_checked_subprocess(cmd, tex) - return pngfile - - def get_grey(self, tex, fontsize=None, dpi=None): - """Return the alpha channel.""" - if not fontsize: - fontsize = rcParams['font.size'] - if not dpi: - dpi = rcParams['savefig.dpi'] - key = tex, self.get_font_config(), fontsize, dpi - alpha = self._grey_arrayd.get(key) - if alpha is None: - pngfile = self.make_png(tex, fontsize, dpi) - rgba = mpl.image.imread(os.path.join(self.texcache, pngfile)) - self._grey_arrayd[key] = alpha = rgba[:, :, -1] - return alpha - - def get_rgba(self, tex, fontsize=None, dpi=None, rgb=(0, 0, 0)): - r""" - Return latex's rendering of the tex string as an rgba array. - - Examples - -------- - >>> texmanager = TexManager() - >>> s = r"\TeX\ is $\displaystyle\sum_n\frac{-e^{i\pi}}{2^n}$!" - >>> Z = texmanager.get_rgba(s, fontsize=12, dpi=80, rgb=(1, 0, 0)) - """ - alpha = self.get_grey(tex, fontsize, dpi) - rgba = np.empty((*alpha.shape, 4)) - rgba[..., :3] = mpl.colors.to_rgb(rgb) - rgba[..., -1] = alpha - return rgba - - def get_text_width_height_descent(self, tex, fontsize, renderer=None): - """Return width, height and descent of the text.""" - if tex.strip() == '': - return 0, 0, 0 - dvifile = self.make_dvi(tex, fontsize) - dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1 - with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi: - page, = dvi - # A total height (including the descent) needs to be returned. - return page.width, page.height + page.descent, page.descent +from matplotlib._texmanager import * # noqa: F401, F403 +from matplotlib import _api +_api.warn_deprecated( + "3.6", message="The module %(name)s is deprecated since %(since)s.", + name=f"{__name__}") diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 5ef56e4be885..88fc64ed910a 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -5,7 +5,7 @@ import numpy as np -from matplotlib import _text_helpers, dviread, font_manager +from matplotlib import _text_helpers, dviread, font_manager, _dviread from matplotlib.font_manager import FontProperties, get_font from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_TARGET_LIGHT from matplotlib.mathtext import MathTextParser @@ -218,7 +218,7 @@ def get_glyphs_mathtext(self, prop, s, glyph_map=None, def get_texmanager(self): """Return the cached `~.texmanager.TexManager` instance.""" if self._texmanager is None: - from matplotlib.texmanager import TexManager + from matplotlib._texmanager import TexManager self._texmanager = TexManager() return self._texmanager @@ -279,7 +279,7 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, @staticmethod @functools.lru_cache(50) def _get_ps_font_and_encoding(texname): - tex_font_map = dviread.PsfontsMap(dviread._find_tex_file('pdftex.map')) + tex_font_map = dviread.PsfontsMap('pdftex.map', find_tex_file=True) psfont = tex_font_map[texname] if psfont.filename is None: raise ValueError( @@ -296,7 +296,7 @@ def _get_ps_font_and_encoding(texname): # FT_Get_Name_Index/get_name_index), and load the glyph using # FT_Load_Glyph/load_glyph. (That charmap has a coverage at least # as good as, and possibly better than, the native charmaps.) - enc = dviread._parse_enc(psfont.encoding) + enc = _dviread._parse_enc(psfont.encoding) else: # If psfonts.map specifies no encoding, the indices directly # map to the font's "native" charmap; so don't use the