| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402 |
- 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()
|