Skip to content

Commit ecc25ca

Browse files
committed
Tweak pgf escapes.
- We don't need to escape underscores manually, but can rely on the underscore package like we already do for usetex. - We don't actually escape dollars (we parse them as math delimiters first). - Slightly tweak error message generation. - Move escaping tests before the big `create_figure` definition, which is used for further tests below.
1 parent 165df1b commit ecc25ca

File tree

2 files changed

+31
-20
lines changed

2 files changed

+31
-20
lines changed

lib/matplotlib/backends/backend_pgf.py

+14-9
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ def get_preamble():
5858

5959
def _get_preamble():
6060
"""Prepare a LaTeX preamble based on the rcParams configuration."""
61-
preamble = [mpl.rcParams["pgf.preamble"]]
61+
preamble = [
62+
mpl.rcParams["pgf.preamble"],
63+
]
6264
if mpl.rcParams["pgf.texsystem"] != "pdflatex":
6365
preamble.append("\\usepackage{fontspec}")
6466
if mpl.rcParams["pgf.rcfonts"]:
@@ -70,6 +72,8 @@ def _get_preamble():
7072
path = pathlib.Path(fm.findfont(family))
7173
preamble.append(r"\%s{%s}[Path=\detokenize{%s/}]" % (
7274
command, path.name, path.parent.as_posix()))
75+
preamble.append(mpl.texmanager._usepackage_if_not_loaded(
76+
"underscore", option="strings")) # Documented as "must come last".
7377
return "\n".join(preamble)
7478

7579

@@ -84,9 +88,8 @@ def _get_preamble():
8488
_NO_ESCAPE = r"(?<!\\)(?:\\\\)*"
8589
_split_math = re.compile(_NO_ESCAPE + r"\$").split
8690
_replace_escapetext = functools.partial(
87-
# When the next character is _, ^, $, or % (not preceded by an escape),
88-
# insert a backslash.
89-
re.compile(_NO_ESCAPE + "(?=[_^$%])").sub, "\\\\")
91+
# When the next character is an unescaped % or ^, insert a backslash.
92+
re.compile(_NO_ESCAPE + "(?=[%^])").sub, "\\\\")
9093
_replace_mathdefault = functools.partial(
9194
# Replace \mathdefault (when not preceded by an escape) by empty string.
9295
re.compile(_NO_ESCAPE + r"(\\mathdefault)").sub, "")
@@ -106,7 +109,7 @@ def _tex_escape(text):
106109
``$`` with ``\(\displaystyle %s\)``. Escaped math separators (``\$``)
107110
are ignored.
108111
109-
The following characters are escaped in text segments: ``_^$%``
112+
The following characters are escaped in text segments: ``^%``
110113
"""
111114
# Sometimes, matplotlib adds the unknown command \mathdefault.
112115
# Not using \mathnormal instead since this looks odd for the latex cm font.
@@ -358,14 +361,16 @@ def _get_box_metrics(self, tex):
358361
try:
359362
answer = self._expect_prompt()
360363
except LatexError as err:
361-
raise ValueError("Error measuring {!r}\nLaTeX Output:\n{}"
364+
# Here and below, use '{}' instead of {!r} to avoid doubling all
365+
# backslashes.
366+
raise ValueError("Error measuring {}\nLaTeX Output:\n{}"
362367
.format(tex, err.latex_output)) from err
363368
try:
364369
# Parse metrics from the answer string. Last line is prompt, and
365370
# next-to-last-line is blank line from \typeout.
366371
width, height, offset = answer.splitlines()[-3].split(",")
367372
except Exception as err:
368-
raise ValueError("Error measuring {!r}\nLaTeX Output:\n{}"
373+
raise ValueError("Error measuring {}\nLaTeX Output:\n{}"
369374
.format(tex, answer)) from err
370375
w, h, o = float(width[:-2]), float(height[:-2]), float(offset[:-2])
371376
# The height returned from LaTeX goes from base to top;
@@ -864,8 +869,8 @@ def print_pdf(self, fname_or_fh, *, metadata=None, **kwargs):
864869
r"\documentclass[12pt]{minimal}",
865870
r"\usepackage[papersize={%fin,%fin}, margin=0in]{geometry}"
866871
% (w, h),
867-
_get_preamble(),
868872
r"\usepackage{pgf}",
873+
_get_preamble(),
869874
r"\begin{document}",
870875
r"\centering",
871876
r"\input{figure.pgf}",
@@ -975,8 +980,8 @@ def _write_header(self, width_inches, height_inches):
975980
r"\documentclass[12pt]{minimal}",
976981
r"\usepackage[papersize={%fin,%fin}, margin=0in]{geometry}"
977982
% (width_inches, height_inches),
978-
_get_preamble(),
979983
r"\usepackage{pgf}",
984+
_get_preamble(),
980985
r"\setlength{\parindent}{0pt}",
981986
r"\begin{document}%",
982987
])

lib/matplotlib/tests/test_backend_pgf.py

+17-11
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ def compare_figure(fname, savefig_kwargs={}, tol=0):
3333
raise ImageComparisonFailure(err)
3434

3535

36+
@pytest.mark.parametrize('plain_text, escaped_text', [
37+
(r'quad_sum: $\sum x_i^2$', r'quad_sum: \(\displaystyle \sum x_i^2\)'),
38+
('% not a comment', r'\% not a comment'),
39+
('^not', r'\^not'),
40+
])
41+
def test_tex_escape(plain_text, escaped_text):
42+
assert _tex_escape(plain_text) == escaped_text
43+
44+
45+
@needs_pgf_xelatex
46+
@pytest.mark.backend('pgf')
47+
def test_tex_special_chars(tmp_path):
48+
fig = plt.figure()
49+
fig.text(.5, .5, "_^ $a_b^c$")
50+
fig.savefig(tmp_path / "test.pdf") # Should not error.
51+
52+
3653
def create_figure():
3754
plt.figure()
3855
x = np.linspace(0, 1, 15)
@@ -59,17 +76,6 @@ def create_figure():
5976
plt.ylim(0, 1)
6077

6178

62-
@pytest.mark.parametrize('plain_text, escaped_text', [
63-
(r'quad_sum: $\sum x_i^2$', r'quad\_sum: \(\displaystyle \sum x_i^2\)'),
64-
(r'no \$splits \$ here', r'no \$splits \$ here'),
65-
('with_underscores', r'with\_underscores'),
66-
('% not a comment', r'\% not a comment'),
67-
('^not', r'\^not'),
68-
])
69-
def test_tex_escape(plain_text, escaped_text):
70-
assert _tex_escape(plain_text) == escaped_text
71-
72-
7379
# test compiling a figure to pdf with xelatex
7480
@needs_pgf_xelatex
7581
@pytest.mark.backend('pgf')

0 commit comments

Comments
 (0)