Files

259 lines
8.8 KiB
Python

import re
from tempfile import TemporaryFile
import numpy as np
from packaging.version import parse as parse_version
import pytest
import matplotlib as mpl
from matplotlib import dviread
from matplotlib.testing import _has_tex_package
from matplotlib.testing.decorators import check_figures_equal, image_comparison
from matplotlib.testing._markers import needs_usetex
import matplotlib.pyplot as plt
pytestmark = needs_usetex
@image_comparison(
baseline_images=['test_usetex'],
extensions=['pdf', 'png'],
style="mpl20")
def test_usetex():
mpl.rcParams['text.usetex'] = True
fig, ax = plt.subplots()
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\}$',
**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(extensions=['png', 'pdf', 'svg'])
def test_empty(fig_test, fig_ref):
mpl.rcParams['text.usetex'] = True
fig_test.text(.5, .5, "% a comment")
@check_figures_equal(extensions=['png', 'pdf', 'svg'])
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()
@image_comparison(['eqnarray.png'], style='mpl20')
def test_multiline_eqnarray():
text = (
r'\begin{eqnarray*}'
r'foo\\'
r'bar\\'
r'baz\\'
r'\end{eqnarray*}'
)
fig = plt.figure(figsize=(1, 1))
fig.text(0.5, 0.5, text, usetex=True,
horizontalalignment='center', verticalalignment='center')
@pytest.mark.parametrize("fontsize", [8, 10, 12])
def test_minus_no_descent(fontsize):
# Test special-casing of minus descent in DviFont._height_depth_of, by
# checking that overdrawing a 1 and a -1 results in an overall height
# equivalent to drawing either of them separately.
mpl.style.use("mpl20")
mpl.rcParams['font.size'] = fontsize
heights = {}
fig = plt.figure()
for vals in [(1,), (-1,), (-1, 1)]:
fig.clear()
for x in vals:
fig.text(.5, .5, f"${x}$", usetex=True)
fig.canvas.draw()
# The following counts the number of non-fully-blank pixel rows.
heights[vals] = ((np.array(fig.canvas.buffer_rgba())[..., 0] != 255)
.any(axis=1).sum())
assert len({*heights.values()}) == 1
@pytest.mark.parametrize('pkg', ['xcolor', 'chemformula'])
def test_usetex_packages(pkg):
if not _has_tex_package(pkg):
pytest.skip(f'{pkg} is not available')
mpl.rcParams['text.usetex'] = True
fig = plt.figure()
text = fig.text(0.5, 0.5, "Some text 0123456789")
fig.canvas.draw()
mpl.rcParams['text.latex.preamble'] = (
r'\PassOptionsToPackage{dvipsnames}{xcolor}\usepackage{%s}' % pkg)
fig = plt.figure()
text2 = fig.text(0.5, 0.5, "Some text 0123456789")
fig.canvas.draw()
np.testing.assert_array_equal(text2.get_window_extent(),
text.get_window_extent())
@pytest.mark.parametrize(
"preamble",
[r"\usepackage[full]{textcomp}", r"\usepackage{underscore}"],
)
def test_latex_pkg_already_loaded(preamble):
plt.rcParams["text.latex.preamble"] = preamble
fig = plt.figure()
fig.text(.5, .5, "hello, world", usetex=True)
fig.canvas.draw()
def test_usetex_with_underscore():
plt.rcParams["text.usetex"] = True
df = {"a_b": range(5)[::-1], "c": range(5)}
fig, ax = plt.subplots()
ax.plot("c", "a_b", data=df)
ax.legend()
ax.text(0, 0, "foo_bar", usetex=True)
plt.draw()
@pytest.mark.flaky(reruns=3) # Tends to hit a TeX cache lock on AppVeyor.
@pytest.mark.parametrize("fmt", ["pdf", "svg"])
def test_missing_psfont(fmt, monkeypatch):
"""An error is raised if a TeX font lacks a Type-1 equivalent"""
monkeypatch.setattr(
dviread.PsfontsMap, '__getitem__',
lambda self, k: dviread.PsFont(
texname=b'texfont', psname=b'Some Font',
effects=None, encoding=None, filename=None))
mpl.rcParams['text.usetex'] = True
fig, ax = plt.subplots()
ax.text(0.5, 0.5, 'hello')
with TemporaryFile() as tmpfile, pytest.raises(ValueError):
fig.savefig(tmpfile, format=fmt)
def test_pdf_type1_font_subsetting():
"""Test that fonts in PDF output are properly subset."""
pikepdf = pytest.importorskip("pikepdf")
mpl.rcParams["text.usetex"] = True
mpl.rcParams["text.latex.preamble"] = r"\usepackage{amssymb}"
fig, ax = plt.subplots()
ax.text(0.2, 0.7, r"$\int_{-\infty}^{\aleph}\sqrt{\alpha\beta\gamma}\mathrm{d}x$")
ax.text(0.2, 0.5, r"$\mathfrak{x}\circledcirc\mathfrak{y}\in\mathbb{R}$")
with TemporaryFile() as tmpfile:
fig.savefig(tmpfile, format="pdf")
tmpfile.seek(0)
pdf = pikepdf.Pdf.open(tmpfile)
length = {}
page = pdf.pages[0]
for font_name, font in page.Resources.Font.items():
assert font.Subtype == "/Type1", (
f"Font {font_name}={font} is not a Type 1 font"
)
# Subsetted font names have a 6-character tag followed by a '+'
base_font = str(font["/BaseFont"]).removeprefix("/")
assert re.match(r"^[A-Z]{6}\+", base_font), (
f"Font {font_name}={base_font} lacks a subset indicator tag"
)
assert "/FontFile" in font.FontDescriptor, (
f"Type 1 font {font_name}={base_font} is not embedded"
)
_, original_name = base_font.split("+", 1)
length[original_name] = len(bytes(font["/FontDescriptor"]["/FontFile"]))
print("Embedded font stream lengths:", length)
# We should have several fonts, each much smaller than the original.
# I get under 10kB on my system for each font, but allow 15kB in case
# of differences in the font files.
assert {
'CMEX10',
'CMMI12',
'CMR12',
'CMSY10',
'CMSY8',
'EUFM10',
'MSAM10',
'MSBM10',
}.issubset(length), "Missing expected fonts in the PDF"
for font_name, length in length.items():
assert length < 15_000, (
f"Font {font_name}={length} is larger than expected"
)
# For comparison, lengths without subsetting on my system:
# 'CMEX10': 29686
# 'CMMI12': 36176
# 'CMR12': 32157
# 'CMSY10': 32004
# 'CMSY8': 32061
# 'EUFM10': 20546
# 'MSAM10': 31199
# 'MSBM10': 34129
try:
_old_gs_version = mpl._get_executable_info('gs').version < parse_version('9.55')
except mpl.ExecutableNotFoundError:
_old_gs_version = True
@image_comparison(baseline_images=['rotation'], extensions=['eps', 'pdf', 'png', 'svg'],
style='mpl20', tol=3.91 if _old_gs_version else 0)
def test_rotation():
mpl.rcParams['text.usetex'] = True
fig = plt.figure()
ax = fig.add_axes((0, 0, 1, 1))
ax.set(xlim=(-0.5, 5), xticks=[], ylim=(-0.5, 3), yticks=[], frame_on=False)
text = {val: val[0] for val in ['top', 'center', 'bottom', 'left', 'right']}
text['baseline'] = 'B'
text['center_baseline'] = 'C'
for i, va in enumerate(['top', 'center', 'bottom', 'baseline', 'center_baseline']):
for j, ha in enumerate(['left', 'center', 'right']):
for k, angle in enumerate([0, 90, 180, 270]):
k //= 2
x = i + k / 2
y = j + k / 2
ax.plot(x, y, '+', c=f'C{k}', markersize=20, markeredgewidth=0.5)
# 'My' checks full height letters plus descenders.
ax.text(x, y, f"$\\mathrm{{My {text[ha]}{text[va]} {angle}}}$",
rotation=angle, horizontalalignment=ha, verticalalignment=va)
def test_unicode_sizing():
tp = mpl.textpath.TextToPath()
scale1 = tp.get_glyphs_tex(mpl.font_manager.FontProperties(), "W")[0][0][3]
scale2 = tp.get_glyphs_tex(mpl.font_manager.FontProperties(), r"\textwon")[0][0][3]
assert scale1 == scale2