diff --git a/src/roboticstoolbox/backends/PyPlot/PyPlot.py b/src/roboticstoolbox/backends/PyPlot/PyPlot.py index 3d4f042ed..1e6e34b97 100644 --- a/src/roboticstoolbox/backends/PyPlot/PyPlot.py +++ b/src/roboticstoolbox/backends/PyPlot/PyPlot.py @@ -512,7 +512,8 @@ def getframe(self): # render the frame and save as a PIL image in the list canvas = self.fig.canvas - return _pil("RGB", canvas.get_width_height(), canvas.tostring_rgb()) + image = _pil("RGBA", canvas.get_width_height(), bytes(canvas.buffer_rgba())) + return image.convert("RGB") def _push_inline_frame(self): # Push a snapshot into notebook output for inline animation. diff --git a/src/roboticstoolbox/robot/BaseRobot.py b/src/roboticstoolbox/robot/BaseRobot.py index 97008c378..b732f5c50 100644 --- a/src/roboticstoolbox/robot/BaseRobot.py +++ b/src/roboticstoolbox/robot/BaseRobot.py @@ -2058,6 +2058,9 @@ def plot( env = self._get_graphical_backend(backend) + if movie is not None: + from roboticstoolbox.backends.PyPlot import PyPlot + launch_kwargs = {} for key in ("render_mode", "inline_every_n", "inline_format", "inline_dpi"): if key in kwargs: diff --git a/tests/test_PyPlot.py b/tests/test_PyPlot.py index 9e3abc4b3..55a8bf3b4 100644 --- a/tests/test_PyPlot.py +++ b/tests/test_PyPlot.py @@ -69,6 +69,36 @@ def test_launch_rejects_2d_axes(self): env.launch(fig=fig, ax=ax) plt.close(fig) + def test_plot_movie(self): + # robot.plot(..., movie=...) used to crash outright: BaseRobot.plot() + # referenced PyPlot in an isinstance check with no import anywhere in + # the module (NameError), and getframe() called the long-removed + # matplotlib Agg canvas method tostring_rgb() (AttributeError on + # modern matplotlib). Covers both bugs end-to-end via a real saved + # GIF, not just "no exception raised". + import tempfile + import os + from PIL import Image + + panda = rp.models.Panda() + qt = rp.jtraj(panda.qr, panda.qz, 3) + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "movie.gif") + env = panda.plot(qt.q, backend="pyplot", movie=path) + env.close() + + self.assertTrue(os.path.exists(path)) + with Image.open(path) as img: + n_frames = 0 + try: + while True: + img.seek(n_frames) + n_frames += 1 + except EOFError: + pass + self.assertEqual(n_frames, 3) + def test_options_scalar_override(self): # Issue #418: options={"jointaxislength": ...} (a plain scalar # default, unlike the dict-valued color/linewidth options) used to