diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 7f90a13f1086..4c2adabd1bb1 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -291,40 +291,11 @@ def _read(self): Read one page from the file. Return True if successful, False if there were no more pages. """ - # Pages appear to start with the sequence - # bop (begin of page) - # xxx comment - # # if using chemformula - # down - # push - # down - # # if using xcolor - # down - # push - # down (possibly multiple) - # push <= here, v is the baseline position. - # etc. - # (dviasm is useful to explore this structure.) - # Thus, we use the vertical position at the first time the stack depth - # reaches 3, while at least three "downs" have been executed (excluding - # those popped out (corresponding to the chemformula preamble)), as the - # baseline (the "down" count is necessary to handle xcolor). - down_stack = [0] self._baseline_v = None while True: byte = self.file.read(1)[0] self._dtable[byte](self, byte) name = self._dtable[byte].__name__ - if name == "_push": - down_stack.append(down_stack[-1]) - elif name == "_pop": - down_stack.pop() - elif name == "_down": - down_stack[-1] += 1 - if (self._baseline_v is None - and len(getattr(self, "stack", [])) == 3 - and down_stack[-1] >= 4): - self._baseline_v = self.v if byte == 140: # end of page return True if self.state is _dvistate.post_post: # end of file @@ -457,6 +428,8 @@ def _fnt_num(self, new_f): @_dispatch(min=239, max=242, args=('ulen1',)) def _xxx(self, datalen): special = self.file.read(datalen) + if special == b'matplotlibbaselinemarker': + self._baseline_v = self.v _log.debug( 'Dvi._xxx: encountered special: %s', ''.join([chr(ch) if 32 <= ch < 127 else '<%02x>' % ch diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 0b703adadc25..70210c9d84fd 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -776,26 +776,11 @@ def test_metrics_cache(): fig = plt.figure() fig.text(.3, .5, "foo\nbar") + fig.text(.5, .5, "foo\nbar") fig.text(.3, .5, "foo\nbar", usetex=True) fig.text(.5, .5, "foo\nbar", usetex=True) fig.canvas.draw() - renderer = fig._cachedRenderer - ys = {} # mapping of strings to where they were drawn in y with draw_tex. - - def call(*args, **kwargs): - renderer, x, y, s, *_ = args - ys.setdefault(s, set()).add(y) - - renderer.draw_tex = call - fig.canvas.draw() - assert [*ys] == ["foo", "bar"] - # Check that both TeX strings were drawn with the same y-position for both - # single-line substrings. Previously, there used to be an incorrect cache - # collision with the non-TeX string (drawn first here) whose metrics would - # get incorrectly reused by the first TeX string. - assert len(ys["foo"]) == len(ys["bar"]) == 1 info = mpl.text._get_text_metrics_with_cache_impl.cache_info() - # Every string gets a miss for the first layouting (extents), then a hit - # when drawing, but "foo\nbar" gets two hits as it's drawn twice. - assert info.hits > info.misses + # Each string gets drawn twice, so the second draw results in a hit. + assert info.hits == info.misses diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index 5f94c52df237..8a7f4834147a 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -161,10 +161,9 @@ 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 '')]) + src = self._get_tex_source(tex, fontsize) + str(dpi) return os.path.join( - self.texcache, hashlib.md5(s.encode('utf-8')).hexdigest()) + self.texcache, hashlib.md5(src.encode('utf-8')).hexdigest()) def get_font_preamble(self): """ @@ -176,26 +175,44 @@ def get_custom_preamble(self): """Return a string containing user additions to the tex preamble.""" return rcParams['text.latex.preamble'] - def _get_preamble(self): + def _get_tex_source(self, tex, fontsize): + """Return the complete TeX source for processing a TeX string.""" + self.get_font_config() # Updates self._font_preamble. + baselineskip = 1.25 * fontsize + fontcmd = (r'\sffamily' if self._font_family == 'sans-serif' else + r'\ttfamily' if self._font_family == 'monospace' else + r'\rmfamily') 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"% Pass-through \mathdefault, which is used in non-usetex mode", + r"% to use the default text font but was historically suppressed", + r"% 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"% geometry is loaded before the custom preamble as ", + r"% convert_psfrags relies on a custom preamble to change the ", + r"% 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 + r"% Use `underscore` package to take care of underscores in text.", + r"% 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. + r"% Custom packages (e.g. newtxtext) may already have loaded ", + r"% textcomp with different options.", _usepackage_if_not_loaded("textcomp"), + r"\pagestyle{empty}", + r"\begin{document}", + r"% The empty hbox ensures that a page is printed even for empty", + r"% inputs, except when using psfrag which gets confused by it.", + r"% matplotlibbaselinemarker is used by dviread to detect the", + r"% last line's baseline.", + rf"\fontsize{{{fontsize}}}{{{baselineskip}}}%", + r"\ifdefined\psfrag\else\hbox{}\fi%", + rf"{{\obeylines{fontcmd} {tex}}}%", + r"\special{matplotlibbaselinemarker}%", + r"\end{document}", ]) def make_tex(self, tex, fontsize): @@ -204,30 +221,8 @@ def make_tex(self, tex, fontsize): Return the file name. """ - basefile = self.get_basefile(tex, fontsize) - texfile = '%s.tex' % basefile - fontcmd = (r'\sffamily' if self._font_family == 'sans-serif' else - r'\ttfamily' if self._font_family == 'monospace' else - r'\rmfamily') - tex_template = r""" -%(preamble)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{%(fontsize)f}{%(baselineskip)f}%% -\ifdefined\psfrag\else\hbox{}\fi%% -{%(fontcmd)s %(tex)s} -\end{document} -""" - Path(texfile).write_text(tex_template % { - "preamble": self._get_preamble(), - "fontsize": fontsize, - "baselineskip": fontsize * 1.25, - "fontcmd": fontcmd, - "tex": tex, - }, encoding="utf-8") - + texfile = self.get_basefile(tex, fontsize) + ".tex" + Path(texfile).write_text(self._get_tex_source(tex, fontsize)) return texfile def _run_checked_subprocess(self, command, tex, *, cwd=None): diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index a98b534d2ef6..f10f4bcfa8d1 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -295,7 +295,8 @@ def _get_layout(self, renderer): of a rotated text when necessary. """ thisx, thisy = 0.0, 0.0 - lines = self.get_text().split("\n") # Ensures lines is not empty. + text = self.get_text() + lines = [text] if self.get_usetex() else text.split("\n") # Not empty. ws = [] hs = []