You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

403 lines
13 KiB
Python

import datetime
from io import BytesIO
import os
import shutil
import numpy as np
from packaging.version import parse as parse_version
import pytest
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.testing import _has_tex_package, _check_for_pgf
from matplotlib.testing.exceptions import ImageComparisonFailure
from matplotlib.testing.compare import compare_images
from matplotlib.backends.backend_pgf import PdfPages
from matplotlib.testing.decorators import (
_image_directories, check_figures_equal, image_comparison)
from matplotlib.testing._markers import (
needs_ghostscript, needs_pgf_lualatex, needs_pgf_pdflatex,
needs_pgf_xelatex)
baseline_dir, result_dir = _image_directories(lambda: 'dummy func')
def compare_figure(fname, savefig_kwargs={}, tol=0):
actual = os.path.join(result_dir, fname)
plt.savefig(actual, **savefig_kwargs)
expected = os.path.join(result_dir, "expected_%s" % fname)
shutil.copyfile(os.path.join(baseline_dir, fname), expected)
err = compare_images(expected, actual, tol=tol)
if err:
raise ImageComparisonFailure(err)
@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$")
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():
plt.figure()
x = np.linspace(0, 1, 15)
# line plot
plt.plot(x, x ** 2, "b-")
# marker
plt.plot(x, 1 - x**2, "g>")
# filled paths and patterns
plt.fill_between([0., .4], [.4, 0.], hatch='//', facecolor="lightgray",
edgecolor="red")
plt.fill([3, 3, .8, .8, 3], [2, -2, -2, 0, 2], "b")
# text and typesetting
plt.plot([0.9], [0.5], "ro", markersize=3)
plt.text(0.9, 0.5, 'unicode (ü, °, \N{Section Sign}) and math ($\\mu_i = x_i^2$)',
ha='right', fontsize=20)
plt.ylabel('sans-serif, blue, $\\frac{\\sqrt{x}}{y^2}$..',
family='sans-serif', color='blue')
plt.text(1, 1, 'should be clipped as default clip_box is Axes bbox',
fontsize=20, clip_on=True)
plt.xlim(0, 1)
plt.ylim(0, 1)
# test compiling a figure to pdf with xelatex
@needs_pgf_xelatex
@pytest.mark.backend('pgf')
@image_comparison(['pgf_xelatex.pdf'], style='default')
def test_xelatex():
rc_xelatex = {'font.family': 'serif',
'pgf.rcfonts': False}
mpl.rcParams.update(rc_xelatex)
create_figure()
try:
_old_gs_version = \
mpl._get_executable_info('gs').version < parse_version('9.50')
except mpl.ExecutableNotFoundError:
_old_gs_version = True
# test compiling a figure to pdf with pdflatex
@needs_pgf_pdflatex
@pytest.mark.skipif(not _has_tex_package('type1ec'), reason='needs type1ec.sty')
@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.71 if _old_gs_version else 0)
def test_pdflatex():
rc_pdflatex = {'font.family': 'serif',
'pgf.rcfonts': False,
'pgf.texsystem': 'pdflatex',
'pgf.preamble': ('\\usepackage[utf8x]{inputenc}'
'\\usepackage[T1]{fontenc}')}
mpl.rcParams.update(rc_pdflatex)
create_figure()
# test updating the rc parameters for each figure
@needs_pgf_xelatex
@needs_pgf_pdflatex
@mpl.style.context('default')
@pytest.mark.backend('pgf')
def test_rcupdate():
rc_sets = [{'font.family': 'sans-serif',
'font.size': 30,
'figure.subplot.left': .2,
'lines.markersize': 10,
'pgf.rcfonts': False,
'pgf.texsystem': 'xelatex'},
{'font.family': 'monospace',
'font.size': 10,
'figure.subplot.left': .1,
'lines.markersize': 20,
'pgf.rcfonts': False,
'pgf.texsystem': 'pdflatex',
'pgf.preamble': ('\\usepackage[utf8x]{inputenc}'
'\\usepackage[T1]{fontenc}'
'\\usepackage{sfmath}')}]
tol = [0, 13.2] if _old_gs_version else [0, 0]
for i, rc_set in enumerate(rc_sets):
with mpl.rc_context(rc_set):
for substring, pkg in [('sfmath', 'sfmath'), ('utf8x', 'ucs')]:
if (substring in mpl.rcParams['pgf.preamble']
and not _has_tex_package(pkg)):
pytest.skip(f'needs {pkg}.sty')
create_figure()
compare_figure(f'pgf_rcupdate{i + 1}.pdf', tol=tol[i])
# test backend-side clipping, since large numbers are not supported by TeX
@needs_pgf_xelatex
@mpl.style.context('default')
@pytest.mark.backend('pgf')
def test_pathclip():
np.random.seed(19680801)
mpl.rcParams.update({'font.family': 'serif', 'pgf.rcfonts': False})
fig, axs = plt.subplots(1, 2)
axs[0].plot([0., 1e100], [0., 1e100])
axs[0].set_xlim(0, 1)
axs[0].set_ylim(0, 1)
axs[1].scatter([0, 1], [1, 1])
axs[1].hist(np.random.normal(size=1000), bins=20, range=[-10, 10])
axs[1].set_xscale('log')
fig.savefig(BytesIO(), format="pdf") # No image comparison.
# test mixed mode rendering
@needs_pgf_xelatex
@pytest.mark.backend('pgf')
@image_comparison(['pgf_mixedmode.pdf'], style='default')
def test_mixedmode():
mpl.rcParams.update({'font.family': 'serif', 'pgf.rcfonts': False})
Y, X = np.ogrid[-1:1:40j, -1:1:40j]
plt.pcolor(X**2 + Y**2).set_rasterized(True)
# test bbox_inches clipping
@needs_pgf_xelatex
@mpl.style.context('default')
@pytest.mark.backend('pgf')
def test_bbox_inches():
mpl.rcParams.update({'font.family': 'serif', 'pgf.rcfonts': False})
fig, (ax1, ax2) = plt.subplots(1, 2)
ax1.plot(range(5))
ax2.plot(range(5))
plt.tight_layout()
bbox = ax1.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
compare_figure('pgf_bbox_inches.pdf', savefig_kwargs={'bbox_inches': bbox},
tol=0)
@mpl.style.context('default')
@pytest.mark.backend('pgf')
@pytest.mark.parametrize('system', [
pytest.param('lualatex', marks=[needs_pgf_lualatex]),
pytest.param('pdflatex', marks=[needs_pgf_pdflatex]),
pytest.param('xelatex', marks=[needs_pgf_xelatex]),
])
def test_pdf_pages(system):
rc_pdflatex = {
'font.family': 'serif',
'pgf.rcfonts': False,
'pgf.texsystem': system,
}
mpl.rcParams.update(rc_pdflatex)
fig1, ax1 = plt.subplots()
ax1.plot(range(5))
fig1.tight_layout()
fig2, ax2 = plt.subplots(figsize=(3, 2))
ax2.plot(range(5))
fig2.tight_layout()
path = os.path.join(result_dir, f'pdfpages_{system}.pdf')
md = {
'Author': 'me',
'Title': 'Multipage PDF with pgf',
'Subject': 'Test page',
'Keywords': 'test,pdf,multipage',
'ModDate': datetime.datetime(
1968, 8, 1, tzinfo=datetime.timezone(datetime.timedelta(0))),
'Trapped': 'Unknown'
}
with PdfPages(path, metadata=md) as pdf:
pdf.savefig(fig1)
pdf.savefig(fig2)
pdf.savefig(fig1)
assert pdf.get_pagecount() == 3
@mpl.style.context('default')
@pytest.mark.backend('pgf')
@pytest.mark.parametrize('system', [
pytest.param('lualatex', marks=[needs_pgf_lualatex]),
pytest.param('pdflatex', marks=[needs_pgf_pdflatex]),
pytest.param('xelatex', marks=[needs_pgf_xelatex]),
])
def test_pdf_pages_metadata_check(monkeypatch, system):
# Basically the same as test_pdf_pages, but we keep it separate to leave
# pikepdf as an optional dependency.
pikepdf = pytest.importorskip('pikepdf')
monkeypatch.setenv('SOURCE_DATE_EPOCH', '0')
mpl.rcParams.update({'pgf.texsystem': system})
fig, ax = plt.subplots()
ax.plot(range(5))
md = {
'Author': 'me',
'Title': 'Multipage PDF with pgf',
'Subject': 'Test page',
'Keywords': 'test,pdf,multipage',
'ModDate': datetime.datetime(
1968, 8, 1, tzinfo=datetime.timezone(datetime.timedelta(0))),
'Trapped': 'True'
}
path = os.path.join(result_dir, f'pdfpages_meta_check_{system}.pdf')
with PdfPages(path, metadata=md) as pdf:
pdf.savefig(fig)
with pikepdf.Pdf.open(path) as pdf:
info = {k: str(v) for k, v in pdf.docinfo.items()}
# Not set by us, so don't bother checking.
if '/PTEX.FullBanner' in info:
del info['/PTEX.FullBanner']
if '/PTEX.Fullbanner' in info:
del info['/PTEX.Fullbanner']
# Some LaTeX engines ignore this setting, and state themselves as producer.
producer = info.pop('/Producer')
assert producer == f'Matplotlib pgf backend v{mpl.__version__}' or (
system == 'lualatex' and 'LuaTeX' in producer)
assert info == {
'/Author': 'me',
'/CreationDate': 'D:19700101000000Z',
'/Creator': f'Matplotlib v{mpl.__version__}, https://matplotlib.org',
'/Keywords': 'test,pdf,multipage',
'/ModDate': 'D:19680801000000Z',
'/Subject': 'Test page',
'/Title': 'Multipage PDF with pgf',
'/Trapped': '/True',
}
@needs_pgf_xelatex
def test_multipage_keep_empty(tmp_path):
# An empty pdf deletes itself afterwards.
fn = tmp_path / "a.pdf"
with PdfPages(fn) as pdf:
pass
assert not fn.exists()
# Test pdf files with content, they should never be deleted.
fn = tmp_path / "b.pdf"
with PdfPages(fn) as pdf:
pdf.savefig(plt.figure())
assert fn.exists()
@needs_pgf_xelatex
def test_tex_restart_after_error():
fig = plt.figure()
fig.suptitle(r"\oops")
with pytest.raises(ValueError):
fig.savefig(BytesIO(), format="pgf")
fig = plt.figure() # start from scratch
fig.suptitle(r"this is ok")
fig.savefig(BytesIO(), format="pgf")
@needs_pgf_xelatex
def test_bbox_inches_tight():
fig, ax = plt.subplots()
ax.imshow([[0, 1], [2, 3]])
fig.savefig(BytesIO(), format="pdf", backend="pgf", bbox_inches="tight")
@needs_pgf_xelatex
@needs_ghostscript
def test_png_transparency(): # Actually, also just testing that png works.
buf = BytesIO()
plt.figure().savefig(buf, format="png", backend="pgf", transparent=True)
buf.seek(0)
t = plt.imread(buf)
assert (t[..., 3] == 0).all() # fully transparent.
@needs_pgf_xelatex
def test_unknown_font(caplog):
with caplog.at_level("WARNING"):
mpl.rcParams["font.family"] = "this-font-does-not-exist"
plt.figtext(.5, .5, "hello, world")
plt.savefig(BytesIO(), format="pgf")
assert "Ignoring unknown font: this-font-does-not-exist" in [
r.getMessage() for r in caplog.records]
@check_figures_equal(extensions=["pdf"])
@pytest.mark.parametrize("texsystem", ("pdflatex", "xelatex", "lualatex"))
@pytest.mark.backend("pgf")
def test_minus_signs_with_tex(fig_test, fig_ref, texsystem):
if not _check_for_pgf(texsystem):
pytest.skip(texsystem + ' + pgf is required')
mpl.rcParams["pgf.texsystem"] = texsystem
fig_test.text(.5, .5, "$-1$")
fig_ref.text(.5, .5, "$\N{MINUS SIGN}1$")
@pytest.mark.backend("pgf")
def test_sketch_params():
fig, ax = plt.subplots(figsize=(3, 3))
ax.set_xticks([])
ax.set_yticks([])
ax.set_frame_on(False)
handle, = ax.plot([0, 1])
handle.set_sketch_params(scale=5, length=30, randomness=42)
with BytesIO() as fd:
fig.savefig(fd, format='pgf')
buf = fd.getvalue().decode()
baseline = r"""\pgfpathmoveto{\pgfqpoint{0.375000in}{0.300000in}}%
\pgfpathlineto{\pgfqpoint{2.700000in}{2.700000in}}%
\usepgfmodule{decorations}%
\usepgflibrary{decorations.pathmorphing}%
\pgfkeys{/pgf/decoration/.cd, """ \
r"""segment length = 0.150000in, amplitude = 0.100000in}%
\pgfmathsetseed{42}%
\pgfdecoratecurrentpath{random steps}%
\pgfusepath{stroke}%"""
# \pgfdecoratecurrentpath must be after the path definition and before the
# path is used (\pgfusepath)
assert baseline in buf
# test to make sure that the document font size is set consistently (see #26892)
@needs_pgf_xelatex
@pytest.mark.skipif(
not _has_tex_package('unicode-math'), reason='needs unicode-math.sty'
)
@pytest.mark.backend('pgf')
@image_comparison(['pgf_document_font_size.pdf'], style='default', remove_text=True)
def test_document_font_size():
mpl.rcParams.update({
'pgf.texsystem': 'xelatex',
'pgf.rcfonts': False,
'pgf.preamble': r'\usepackage{unicode-math}',
})
plt.figure()
plt.plot([],
label=r'$this is a very very very long math label a \times b + 10^{-3}$ '
r'and some text'
)
plt.plot([],
label=r'\normalsize the document font size is \the\fontdimen6\font'
)
plt.legend()