Skip to content

Remove need to detect math mode in pgf strings #23442

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 28 additions & 36 deletions lib/matplotlib/backends/backend_pgf.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@
@_api.caching_module_getattr
class __getattr__:
NO_ESCAPE = _api.deprecated("3.6", obj_type="")(
property(lambda self: _NO_ESCAPE))
property(lambda self: r"(?<!\\)(?:\\\\)*"))
re_mathsep = _api.deprecated("3.6", obj_type="")(
property(lambda self: _split_math.__self__))
property(lambda self: r"(?<!\\)(?:\\\\)*\$"))


@_api.deprecated("3.6")
Expand All @@ -58,7 +58,15 @@ def get_preamble():

def _get_preamble():
"""Prepare a LaTeX preamble based on the rcParams configuration."""
preamble = [mpl.rcParams["pgf.preamble"]]
preamble = [
# Remove Matplotlib's custom command \mathdefault. (Not using
# \mathnormal instead since this looks odd with Computer Modern.)
r"\def\mathdefault#1{#1}",
# Use displaystyle for all math.
r"\everymath=\expandafter{\the\everymath\displaystyle}",
# Allow pgf.preamble to override the above definitions.
mpl.rcParams["pgf.preamble"],
]
if mpl.rcParams["pgf.texsystem"] != "pdflatex":
preamble.append("\\usepackage{fontspec}")
if mpl.rcParams["pgf.rcfonts"]:
Expand All @@ -83,16 +91,6 @@ def _get_preamble():
mpl_in_to_pt = 1. / mpl_pt_to_in


_NO_ESCAPE = r"(?<!\\)(?:\\\\)*"
_split_math = re.compile(_NO_ESCAPE + r"\$").split
_replace_escapetext = functools.partial(
# When the next character is an unescaped % or ^, insert a backslash.
re.compile(_NO_ESCAPE + "(?=[%^])").sub, "\\\\")
_replace_mathdefault = functools.partial(
# Replace \mathdefault (when not preceded by an escape) by empty string.
re.compile(_NO_ESCAPE + r"(\\mathdefault)").sub, "")


@_api.deprecated("3.6")
def common_texification(text):
return _tex_escape(text)
Expand All @@ -102,28 +100,8 @@ def _tex_escape(text):
r"""
Do some necessary and/or useful substitutions for texts to be included in
LaTeX documents.

This distinguishes text-mode and math-mode by replacing the math separator
``$`` with ``\(\displaystyle %s\)``. Escaped math separators (``\$``)
are ignored.

The following characters are escaped in text segments: ``^%``
"""
# Sometimes, matplotlib adds the unknown command \mathdefault.
# Not using \mathnormal instead since this looks odd for the latex cm font.
text = _replace_mathdefault(text)
text = text.replace("\N{MINUS SIGN}", r"\ensuremath{-}")
# split text into normaltext and inline math parts
parts = _split_math(text)
for i, s in enumerate(parts):
if not i % 2:
# textmode replacements
s = _replace_escapetext(s)
else:
# mathmode replacements
s = r"\(\displaystyle %s\)" % s
parts[i] = s
return "".join(parts)
return text.replace("\N{MINUS SIGN}", r"\ensuremath{-}")


@_api.deprecated("3.6")
Expand Down Expand Up @@ -168,7 +146,17 @@ def _escape_and_apply_props(s, prop):
commands.append(r"\bfseries")

commands.append(r"\selectfont")
return "".join(commands) + " " + _tex_escape(s)
return (
"{"
+ "".join(commands)
+ r"\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}"
# It should normally be enough to set the catcode of % to 12 ("normal
# character"); this works on TeXLive 2021 but not on 2018, so we just
# make it active too.
+ r"\catcode`\%=\active\def%{\%}"
+ _tex_escape(s)
+ "}"
)


def _metadata_to_str(key, value):
Expand Down Expand Up @@ -357,7 +345,11 @@ def _get_box_metrics(self, tex):
"""
# This method gets wrapped in __init__ for per-instance caching.
self._stdin_writeln( # Send textbox to TeX & request metrics typeout.
r"\sbox0{%s}\typeout{\the\wd0,\the\ht0,\the\dp0}" % tex)
# \sbox doesn't handle catcode assignments inside its argument,
# so repeat the assignment of the catcode of "^" and "%" outside.
r"{\catcode`\^=\active\catcode`\%%=\active\sbox0{%s}"
r"\typeout{\the\wd0,\the\ht0,\the\dp0}}"
% tex)
try:
answer = self._expect_prompt()
except LatexError as err:
Expand Down
22 changes: 9 additions & 13 deletions lib/matplotlib/tests/test_backend_pgf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import matplotlib.pyplot as plt
from matplotlib.testing import _has_tex_package, _check_for_pgf
from matplotlib.testing.compare import compare_images, ImageComparisonFailure
from matplotlib.backends.backend_pgf import PdfPages, _tex_escape
from matplotlib.backends.backend_pgf import PdfPages
from matplotlib.testing.decorators import (
_image_directories, check_figures_equal, image_comparison)
from matplotlib.testing._markers import (
Expand All @@ -33,21 +33,17 @@ def compare_figure(fname, savefig_kwargs={}, tol=0):
raise ImageComparisonFailure(err)


@pytest.mark.parametrize('plain_text, escaped_text', [
(r'quad_sum: $\sum x_i^2$', r'quad_sum: \(\displaystyle \sum x_i^2\)'),
('% not a comment', r'\% not a comment'),
('^not', r'\^not'),
])
def test_tex_escape(plain_text, escaped_text):
assert _tex_escape(plain_text) == escaped_text


@needs_pgf_xelatex
@needs_ghostscript
@pytest.mark.backend('pgf')
def test_tex_special_chars(tmp_path):
fig = plt.figure()
fig.text(.5, .5, "_^ $a_b^c$")
fig.savefig(tmp_path / "test.pdf") # Should not error.
fig.text(.5, .5, "%_^ $a_b^c$")
buf = BytesIO()
fig.savefig(buf, format="png", backend="pgf")
buf.seek(0)
t = plt.imread(buf)
assert not (t == 1).all() # The leading "%" didn't eat up everything.


def create_figure():
Expand Down Expand Up @@ -99,7 +95,7 @@ def test_xelatex():
@pytest.mark.skipif(not _has_tex_package('ucs'), reason='needs ucs.sty')
@pytest.mark.backend('pgf')
@image_comparison(['pgf_pdflatex.pdf'], style='default',
tol=11.7 if _old_gs_version else 0)
tol=11.71 if _old_gs_version else 0)
def test_pdflatex():
if os.environ.get('APPVEYOR'):
pytest.xfail("pdflatex test does not work on appveyor due to missing "
Expand Down