From 6bb6944ac72c3b99beb8ddfab98e09032c0482ae Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 8 Sep 2021 15:33:23 -0400 Subject: [PATCH] Backport PR #20518: Support sketch_params in pgf backend --- lib/matplotlib/artist.py | 3 +++ lib/matplotlib/backends/backend_pgf.py | 24 +++++++++++++++++++++ lib/matplotlib/tests/test_backend_pgf.py | 27 ++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 152c0ea33ff5..185ec79ca8d8 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -690,6 +690,9 @@ def set_sketch_params(self, scale=None, length=None, randomness=None): The scale factor by which the length is shrunken or expanded (default 16.0) + The PGF backend uses this argument as an RNG seed and not as + described above. Using the same seed yields the same random shape. + .. ACCEPTS: (scale: float, length: float, randomness: float) """ if scale is None: diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 749da559662c..2fa8c3251b12 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -600,6 +600,30 @@ def _print_pgf_path(self, gc, path, transform, rgbFace=None): r"{\pgfqpoint{%fin}{%fin}}" % coords) + # apply pgf decorators + sketch_params = gc.get_sketch_params() if gc else None + if sketch_params is not None: + # Only "length" directly maps to "segment length" in PGF's API. + # PGF uses "amplitude" to pass the combined deviation in both x- + # and y-direction, while matplotlib only varies the length of the + # wiggle along the line ("randomness" and "length" parameters) + # and has a separate "scale" argument for the amplitude. + # -> Use "randomness" as PRNG seed to allow the user to force the + # same shape on multiple sketched lines + scale, length, randomness = sketch_params + if scale is not None: + # make matplotlib and PGF rendering visually similar + length *= 0.5 + scale *= 2 + # PGF guarantees that repeated loading is a no-op + writeln(self.fh, r"\usepgfmodule{decorations}") + writeln(self.fh, r"\usepgflibrary{decorations.pathmorphing}") + writeln(self.fh, r"\pgfkeys{/pgf/decoration/.cd, " + f"segment length = {(length * f):f}in, " + f"amplitude = {(scale * f):f}in}}") + writeln(self.fh, f"\\pgfmathsetseed{{{int(randomness)}}}") + writeln(self.fh, r"\pgfdecoratecurrentpath{random steps}") + def _pgf_path_draw(self, stroke=True, fill=False): actions = [] if stroke: diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index a463c96e61fc..9b5b0b28ee3f 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -337,3 +337,30 @@ def test_minus_signs_with_tex(fig_test, fig_ref, texsystem): 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