diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index 2202431b005d..feb72ca04ec0 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -23,7 +23,6 @@ import functools import hashlib import logging -import os from pathlib import Path import subprocess from tempfile import TemporaryDirectory @@ -63,7 +62,7 @@ class TexManager: Repeated calls to this constructor always return the same instance. """ - _texcache = os.path.join(mpl.get_cachedir(), 'tex.cache') + _cache_dir = Path(mpl.get_cachedir(), 'tex.cache') _grey_arrayd = {} _font_families = ('serif', 'sans-serif', 'cursive', 'monospace') @@ -109,7 +108,7 @@ class TexManager: @functools.lru_cache # Always return the same instance. def __new__(cls): - Path(cls._texcache).mkdir(parents=True, exist_ok=True) + cls._cache_dir.mkdir(parents=True, exist_ok=True) return object.__new__(cls) @classmethod @@ -167,23 +166,30 @@ def _get_font_preamble_and_command(cls): return preamble, fontcmd @classmethod - def get_basefile(cls, tex, fontsize, dpi=None): + def _get_base_path(cls, tex, fontsize, dpi=None): """ - Return a filename based on a hash of the string, fontsize, and dpi. + Return a file path based on a hash of the string, fontsize, and dpi. """ src = cls._get_tex_source(tex, fontsize) + str(dpi) filehash = hashlib.sha256( src.encode('utf-8'), usedforsecurity=False ).hexdigest() - filepath = Path(cls._texcache) + filepath = cls._cache_dir num_letters, num_levels = 2, 2 for i in range(0, num_letters*num_levels, num_letters): - filepath = filepath / Path(filehash[i:i+2]) + filepath = filepath / filehash[i:i+2] filepath.mkdir(parents=True, exist_ok=True) - return os.path.join(filepath, filehash) + return filepath / filehash + + @classmethod + def get_basefile(cls, tex, fontsize, dpi=None): # Kept for backcompat. + """ + Return a filename based on a hash of the string, fontsize, and dpi. + """ + return str(cls._get_base_path(tex, fontsize, dpi)) @classmethod def get_font_preamble(cls): @@ -243,17 +249,16 @@ def make_tex(cls, tex, fontsize): Return the file name. """ - texfile = cls.get_basefile(tex, fontsize) + ".tex" - Path(texfile).write_text(cls._get_tex_source(tex, fontsize), - encoding='utf-8') - return texfile + texpath = cls._get_base_path(tex, fontsize).with_suffix(".tex") + texpath.write_text(cls._get_tex_source(tex, fontsize), encoding='utf-8') + return str(texpath) @classmethod def _run_checked_subprocess(cls, command, tex, *, cwd=None): _log.debug(cbook._pformat_subprocess(command)) try: report = subprocess.check_output( - command, cwd=cwd if cwd is not None else cls._texcache, + command, cwd=cwd if cwd is not None else cls._cache_dir, stderr=subprocess.STDOUT) except FileNotFoundError as exc: raise RuntimeError( @@ -281,8 +286,8 @@ def make_dvi(cls, tex, fontsize): Return the file name. """ - dvifile = Path(cls.get_basefile(tex, fontsize)).with_suffix(".dvi") - if not dvifile.exists(): + dvipath = cls._get_base_path(tex, fontsize).with_suffix(".dvi") + if not dvipath.exists(): # Generate the tex and 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 @@ -292,17 +297,17 @@ def make_dvi(cls, tex, fontsize): # the absolute path may contain characters (e.g. ~) that TeX does # not support; n.b. relative paths cannot traverse parents, or it # will be blocked when `openin_any = p` in texmf.cnf). - with TemporaryDirectory(dir=dvifile.parent) as tmpdir: + with TemporaryDirectory(dir=dvipath.parent) as tmpdir: Path(tmpdir, "file.tex").write_text( cls._get_tex_source(tex, fontsize), encoding='utf-8') cls._run_checked_subprocess( ["latex", "-interaction=nonstopmode", "--halt-on-error", "file.tex"], tex, cwd=tmpdir) - Path(tmpdir, "file.dvi").replace(dvifile) + Path(tmpdir, "file.dvi").replace(dvipath) # Also move the tex source to the main cache directory, but # only for backcompat. - Path(tmpdir, "file.tex").replace(dvifile.with_suffix(".tex")) - return str(dvifile) + Path(tmpdir, "file.tex").replace(dvipath.with_suffix(".tex")) + return str(dvipath) @classmethod def make_png(cls, tex, fontsize, dpi): @@ -311,13 +316,12 @@ def make_png(cls, tex, fontsize, dpi): Return the file name. """ - pngfile = Path(cls.get_basefile(tex, fontsize)).with_suffix(".png") - # see get_rgba for a discussion of the background - if not pngfile.exists(): - dvifile = cls.make_dvi(tex, fontsize) - with TemporaryDirectory(dir=pngfile.parent) as tmpdir: + pngpath = cls._get_base_path(tex, fontsize, dpi).with_suffix(".png") + if not pngpath.exists(): + dvipath = cls.make_dvi(tex, fontsize) + with TemporaryDirectory(dir=pngpath.parent) as tmpdir: cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi), - "-T", "tight", "-o", "file.png", dvifile] + "-T", "tight", "-o", "file.png", dvipath] # 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 @@ -326,8 +330,8 @@ def make_png(cls, tex, fontsize, dpi): mpl._get_executable_info("dvipng").raw_version != "1.16"): cmd.insert(1, "--freetype0") cls._run_checked_subprocess(cmd, tex, cwd=tmpdir) - Path(tmpdir, "file.png").replace(pngfile) - return str(pngfile) + Path(tmpdir, "file.png").replace(pngpath) + return str(pngpath) @classmethod def get_grey(cls, tex, fontsize=None, dpi=None): @@ -338,7 +342,7 @@ def get_grey(cls, tex, fontsize=None, dpi=None): alpha = cls._grey_arrayd.get(key) if alpha is None: pngfile = cls.make_png(tex, fontsize, dpi) - rgba = mpl.image.imread(os.path.join(cls._texcache, pngfile)) + rgba = mpl.image.imread(pngfile) cls._grey_arrayd[key] = alpha = rgba[:, :, -1] return alpha @@ -364,9 +368,9 @@ def get_text_width_height_descent(cls, tex, fontsize, renderer=None): """Return width, height and descent of the text.""" if tex.strip() == '': return 0, 0, 0 - dvifile = cls.make_dvi(tex, fontsize) + dvipath = cls.make_dvi(tex, fontsize) dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1 - with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi: + with dviread.Dvi(dvipath, 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