diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index d3850cc7cb95..7ce621b8a28e 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -543,8 +543,7 @@ def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): r'\psfrag{%s}[Bl][Bl][1][%f]{\fontsize{%f}{%f}%s}' % ( thetext, angle, fontsize, fontsize*1.25, tex)) else: - # Stick to the bottom alignment, but this may give incorrect - # baseline some times. + # Stick to the bottom alignment. pos = _nums_to_str(x-corr, y-bl) self.psfrag.append( r'\psfrag{%s}[bl][bl][1][%f]{\fontsize{%f}{%f}%s}' % ( @@ -1234,19 +1233,21 @@ def convert_psfrags(tmpfile, psfrags, font_preamble, custom_preamble, """ with mpl.rc_context({ "text.latex.preamble": - rcParams["text.latex.preamble"] + - r"\usepackage{psfrag,color}" - r"\usepackage[dvips]{graphicx}" - r"\PassOptionsToPackage{dvips}{geometry}"}): + mpl.rcParams["text.latex.preamble"] + + r"\usepackage{psfrag,color}""\n" + r"\usepackage[dvips]{graphicx}""\n" + r"\geometry{papersize={%(width)sin,%(height)sin}," + r"body={%(width)sin,%(height)sin},margin=0in}" + % {"width": paper_width, "height": paper_height} + }): dvifile = TexManager().make_dvi( - r"\newgeometry{papersize={%(width)sin,%(height)sin}," - r"body={%(width)sin,%(height)sin}, margin={0in,0in}}""\n" - r"\begin{figure}" - r"\centering\leavevmode%(psfrags)s" - r"\includegraphics*[angle=%(angle)s]{%(epsfile)s}" + "\n" + r"\begin{figure}""\n" + r" \centering\leavevmode""\n" + r" %(psfrags)s""\n" + r" \includegraphics*[angle=%(angle)s]{%(epsfile)s}""\n" r"\end{figure}" % { - "width": paper_width, "height": paper_height, "psfrags": "\n".join(psfrags), "angle": 90 if orientation == 'landscape' else 0, "epsfile": pathlib.Path(tmpfile).resolve().as_posix(), diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index fc5498d910e8..2a48a13c4109 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -269,6 +269,12 @@ def _output(self): maxx = max(maxx, x + w) maxy = max(maxy, y + e) maxy_pure = max(maxy_pure, y) + if self._baseline_v is not None: + maxy_pure = self._baseline_v # This should normally be the case. + 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) if self.dpi is None: # special case for ease of debugging: output raw dvi coordinates @@ -296,9 +302,24 @@ 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 + # down + # push + # down, down + # push + # down (possibly multiple) + # push <= here, v is the baseline position. + # etc. + # (dviasm is useful to explore this structure.) + self._baseline_v = None while True: byte = self.file.read(1)[0] self._dtable[byte](self, byte) + if (self._baseline_v is None + and len(getattr(self, "stack", [])) == 3): + self._baseline_v = self.v if byte == 140: # end of page return True if self.state is _dvistate.post_post: # end of file diff --git a/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.pdf b/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.pdf index 8041641f9827..4ef375771d38 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.pdf and b/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png b/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png index 9a92f45e1e18..e4a9183612f5 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png and b/lib/matplotlib/tests/baseline_images/test_usetex/test_usetex.png differ diff --git a/lib/matplotlib/tests/test_usetex.py b/lib/matplotlib/tests/test_usetex.py index 8d8cf29d0b82..ec693288e932 100644 --- a/lib/matplotlib/tests/test_usetex.py +++ b/lib/matplotlib/tests/test_usetex.py @@ -1,3 +1,4 @@ +import numpy as np import pytest import platform @@ -7,31 +8,56 @@ from matplotlib.ticker import EngFormatter -@pytest.fixture(autouse=True) # All tests in this module use usetex. -def usetex(): - if not mpl.checkdep_usetex(True): - pytest.skip('Missing TeX of Ghostscript or dvipng') - mpl.rcParams['text.usetex'] = True +if not mpl.checkdep_usetex(True): + pytestmark = pytest.mark.skip('Missing TeX of Ghostscript or dvipng') -@image_comparison(baseline_images=['test_usetex'], - extensions=['pdf', 'png'], - tol={'aarch64': 2.868}.get(platform.machine(), 0.3)) +@image_comparison( + baseline_images=['test_usetex'], + extensions=['pdf', 'png'], + style="mpl20") def test_usetex(): + mpl.rcParams['text.usetex'] = True fig = plt.figure() ax = fig.add_subplot(111) - ax.text(0.1, 0.2, + kwargs = {"verticalalignment": "baseline", "size": 24, + "bbox": dict(pad=0, edgecolor="k", facecolor="none")} + ax.text(0.2, 0.7, # the \LaTeX macro exercises character sizing and placement, # \left[ ... \right\} draw some variable-height characters, # \sqrt and \frac draw horizontal rules, \mathrm changes the font r'\LaTeX\ $\left[\int\limits_e^{2e}' r'\sqrt\frac{\log^3 x}{x}\,\mathrm{d}x \right\}$', - fontsize=24) - ax.set_xticks([]) - ax.set_yticks([]) + **kwargs) + ax.text(0.2, 0.3, "lg", **kwargs) + ax.text(0.4, 0.3, r"$\frac{1}{2}\pi$", **kwargs) + ax.text(0.6, 0.3, "$p^{3^A}$", **kwargs) + ax.text(0.8, 0.3, "$p_{3_2}$", **kwargs) + for x in {t.get_position()[0] for t in ax.texts}: + ax.axvline(x) + for y in {t.get_position()[1] for t in ax.texts}: + ax.axhline(y) + ax.set_axis_off() + + +@check_figures_equal() +def test_empty(fig_test, fig_ref): + mpl.rcParams['text.usetex'] = True + fig_test.text(.5, .5, "% a comment") @check_figures_equal() def test_unicode_minus(fig_test, fig_ref): + mpl.rcParams['text.usetex'] = True fig_test.text(.5, .5, "$-$") fig_ref.text(.5, .5, "\N{MINUS SIGN}") + + +def test_mathdefault(): + plt.rcParams["axes.formatter.use_mathtext"] = True + fig = plt.figure() + fig.add_subplot().set_xlim(-1, 1) + # Check that \mathdefault commands generated by tickers don't cause + # problems when later switching usetex on. + mpl.rcParams['text.usetex'] = True + fig.canvas.draw() diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index 1e788a64c4cd..5406980bf108 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -192,6 +192,25 @@ def get_custom_preamble(self): """Return a string containing user additions to the tex preamble.""" return rcParams['text.latex.preamble'] + def _get_preamble(self): + unicode_preamble = "\n".join([ + r"\usepackage[utf8]{inputenc}", + r"\DeclareUnicodeCharacter{2212}{\ensuremath{-}}", + ]) if rcParams["text.latex.unicode"] else "" + 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, + unicode_preamble, + # Needs to come early so that the custom preamble can change the + # geometry, e.g. in convert_psfrags. + r"\usepackage[papersize=72in,body=70in,margin=1in]{geometry}", + self.get_custom_preamble(), + ]) + def make_tex(self, tex, fontsize): """ Generate a tex file to render the tex string at a specific font size. @@ -200,29 +219,22 @@ def make_tex(self, tex, fontsize): """ basefile = self.get_basefile(tex, fontsize) texfile = '%s.tex' % basefile - custom_preamble = self.get_custom_preamble() fontcmd = {'sans-serif': r'{\sffamily %s}', 'monospace': r'{\ttfamily %s}'}.get(self.font_family, r'{\rmfamily %s}') tex = fontcmd % tex - unicode_preamble = "\n".join([ - r"\usepackage[utf8]{inputenc}", - r"\DeclareUnicodeCharacter{2212}{\ensuremath{-}}", - ]) if rcParams["text.latex.unicode"] else "" - s = r""" -\documentclass{article} %s -%s -%s -\usepackage[papersize={72in,72in},body={70in,70in},margin={1in,1in}]{geometry} \pagestyle{empty} \begin{document} -\fontsize{%f}{%f}%s +%% 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._font_preamble, unicode_preamble, custom_preamble, - fontsize, fontsize * 1.25, tex) +""" % (self._get_preamble(), fontsize, fontsize * 1.25, tex) with open(texfile, 'wb') as fh: if rcParams['text.latex.unicode']: fh.write(s.encode('utf8')) @@ -250,27 +262,17 @@ def make_tex_preview(self, tex, fontsize): """ basefile = self.get_basefile(tex, fontsize) texfile = '%s.tex' % basefile - custom_preamble = self.get_custom_preamble() fontcmd = {'sans-serif': r'{\sffamily %s}', 'monospace': r'{\ttfamily %s}'}.get(self.font_family, r'{\rmfamily %s}') tex = fontcmd % tex - unicode_preamble = "\n".join([ - r"\usepackage[utf8]{inputenc}", - r"\DeclareUnicodeCharacter{2212}{\ensuremath{-}}", - ]) if rcParams["text.latex.unicode"] else "" - # newbox, setbox, immediate, etc. are used to find the box # extent of the rendered text. s = r""" -\documentclass{article} -%s -%s %s \usepackage[active,showbox,tightpage]{preview} -\usepackage[papersize={72in,72in},body={70in,70in},margin={1in,1in}]{geometry} %% we override the default showbox as it is treated as an error and makes %% the exit status not zero @@ -282,8 +284,7 @@ def make_tex_preview(self, tex, fontsize): {\fontsize{%f}{%f}%s} \end{preview} \end{document} -""" % (self._font_preamble, unicode_preamble, custom_preamble, - fontsize, fontsize * 1.25, tex) +""" % (self._get_preamble(), fontsize, fontsize * 1.25, tex) with open(texfile, 'wb') as fh: if rcParams['text.latex.unicode']: fh.write(s.encode('utf8')) @@ -391,9 +392,16 @@ def make_png(self, tex, fontsize, dpi): # see get_rgba for a discussion of the background if not os.path.exists(pngfile): dvifile = self.make_dvi(tex, fontsize) - self._run_checked_subprocess( - ["dvipng", "-bg", "Transparent", "-D", str(dpi), - "-T", "tight", "-o", pngfile, dvifile], tex) + 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").version != "1.16"): + cmd.insert(1, "--freetype0") + self._run_checked_subprocess(cmd, tex) return pngfile def get_grey(self, tex, fontsize=None, dpi=None): @@ -446,7 +454,7 @@ def get_text_width_height_descent(self, tex, fontsize, renderer=None): return width, height + depth, depth else: - # use dviread. It sometimes returns a wrong descent. + # use dviread. dvifile = self.make_dvi(tex, fontsize) with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi: page, = dvi diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index f29184ba55f8..c6b05288c17d 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -188,10 +188,6 @@ 'SymmetricalLogLocator', 'LogitLocator', 'OldAutoLocator') -def _mathdefault(s): - return '\\mathdefault{%s}' % s - - class _DummyAxis: def __init__(self, minpos=0): self.dataLim = mtransforms.Bbox.unit() @@ -664,14 +660,10 @@ def get_offset(self): sciNotStr = self.format_data(10 ** self.orderOfMagnitude) else: sciNotStr = '1e%d' % self.orderOfMagnitude - if self._useMathText: - if sciNotStr != '': - sciNotStr = r'\times%s' % _mathdefault(sciNotStr) - s = ''.join(('$', sciNotStr, _mathdefault(offsetStr), '$')) - elif self._usetex: + if self._useMathText or self._usetex: if sciNotStr != '': - sciNotStr = r'\times%s' % sciNotStr - s = ''.join(('$', sciNotStr, offsetStr, '$')) + sciNotStr = r'\times\mathdefault{%s}' % sciNotStr + s = r'$%s\mathdefault{%s}$' % (sciNotStr, offsetStr) else: s = ''.join((sciNotStr, offsetStr)) @@ -794,10 +786,8 @@ def _set_format(self): break sigfigs += 1 self.format = '%1.' + str(sigfigs) + 'f' - if self._usetex: - self.format = '$%s$' % self.format - elif self._useMathText: - self.format = '$%s$' % _mathdefault(self.format) + if self._usetex or self._useMathText: + self.format = r'$\mathdefault{%s}$' % self.format @cbook.deprecated("3.1") def pprint_val(self, x): @@ -1091,11 +1081,7 @@ class LogFormatterMathtext(LogFormatter): def _non_decade_format(self, sign_string, base, fx, usetex): 'Return string for non-decade locations' - if usetex: - return (r'$%s%s^{%.2f}$') % (sign_string, base, fx) - else: - return ('$%s$' % _mathdefault('%s%s^{%.2f}' % - (sign_string, base, fx))) + return r'$\mathdefault{%s%s^{%.2f}}$' % (sign_string, base, fx) def __call__(self, x, pos=None): """ @@ -1107,10 +1093,7 @@ def __call__(self, x, pos=None): min_exp = rcParams['axes.formatter.min_exponent'] if x == 0: # Symlog - if usetex: - return '$0$' - else: - return '$%s$' % _mathdefault('0') + return r'$\mathdefault{0}$' sign_string = '-' if x < 0 else '' x = abs(x) @@ -1135,18 +1118,12 @@ def __call__(self, x, pos=None): else: base = '%s' % b - if np.abs(fx) < min_exp: - if usetex: - return r'${0}{1:g}$'.format(sign_string, x) - else: - return '${0}$'.format(_mathdefault( - '{0}{1:g}'.format(sign_string, x))) + if abs(fx) < min_exp: + return r'$\mathdefault{%s%g}$' % (sign_string, x) elif not is_x_decade: return self._non_decade_format(sign_string, base, fx, usetex) - elif usetex: - return r'$%s%s^{%d}$' % (sign_string, base, fx) else: - return '$%s$' % _mathdefault('%s%s^{%d}' % (sign_string, base, fx)) + return r'$\mathdefault{%s%s^{%d}}$' % (sign_string, base, fx) class LogFormatterSciNotation(LogFormatterMathtext): @@ -1161,12 +1138,8 @@ def _non_decade_format(self, sign_string, base, fx, usetex): coeff = b ** fx / b ** exponent if is_close_to_int(coeff): coeff = round(coeff) - if usetex: - return (r'$%s%g\times%s^{%d}$') % \ - (sign_string, coeff, base, exponent) - else: - return ('$%s$' % _mathdefault(r'%s%g\times%s^{%d}' % - (sign_string, coeff, base, exponent))) + return r'$\mathdefault{%s%g\times%s^{%d}}$' \ + % (sign_string, coeff, base, exponent) class LogitFormatter(Formatter): @@ -1338,8 +1311,6 @@ def __call__(self, x, pos=None): return "" if x <= 0 or x >= 1: return "" - usetex = rcParams["text.usetex"] - if is_close_to_int(2 * x) and round(2 * x) == 1: s = self._one_half elif x < 0.5 and is_decade(x, rtol=1e-7): @@ -1354,9 +1325,7 @@ def __call__(self, x, pos=None): s = self._one_minus(self._format_value(1-x, 1-self.locs)) else: s = self._format_value(x, self.locs, sci_notation=False) - if usetex: - return "$%s$" % s - return "$%s$" % _mathdefault(s) + return r"$\mathdefault{%s}$" % s def format_data_short(self, value): """