Skip to content

Commit 63e2614

Browse files
tacaswellQuLogic
andcommitted
API: forbid unsafe settings and keywords in AbstractMovieWriter.grab_frame
Several of the keyword arguments (dpi, bbox_inches) to savefig will change the dimensions of the rendered figure or byte output (format). Changing these mid-stream in generating a movie will in a best case result in an animation that jumps around and in a worst case silently generate a completely corrupted movie. The rcparam savefig.bbox can have the same effect. The low level AbstractMovieWriter.grab_frame is now strict and will error if any possibly unsafe conditions are met. closes matplotlib#25608 Co-authored-by: Elliott Sales de Andrade <[email protected]>
1 parent a1f9e0f commit 63e2614

File tree

3 files changed

+76
-1
lines changed

3 files changed

+76
-1
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Reject size related keyword arguments to MovieWriter *grab_frame* method
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
Although we pass `.Figure.savefig` keyword arguments through the
5+
`.AbstractMovieWriter.grab_frame` some of the arguments will result in invalid
6+
output if passed. To be successfully stitched into a movie, each frame
7+
must be exactly the same size, thus *bbox_inches* and *dpi* are excluded.
8+
Additionally, the movie writers are opinionated about the format of each
9+
frame, so the *format* argument is also excluded. Passing these
10+
arguments will now raise `TypeError` for all writers (it already did so for some
11+
arguments and some writers). The *bbox_inches* argument is already ignored (with
12+
a warning) if passed to `.Animation.save`.
13+
14+
15+
Additionally, if :rc:`savefig.bbox` is set to ``'tight'``,
16+
`.AbstractMovieWriter.grab_frame` will now error. Previously this rcParam
17+
would be temporarily overridden (with a warning) in `.Animation.save`, it is
18+
now additionally overridden in `.AbstractMovieWriter.saving`.

lib/matplotlib/animation.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,13 @@ def grab_frame(self, **savefig_kwargs):
213213
Grab the image information from the figure and save as a movie frame.
214214
215215
All keyword arguments in *savefig_kwargs* are passed on to the
216-
`~.Figure.savefig` call that saves the figure.
216+
`~.Figure.savefig` call that saves the figure. However, several
217+
keyword arguments that are supported by `~.Figure.savefig` may not be
218+
passed as they are controlled by the MovieWriter:
219+
220+
- *dpi*, *bbox_inches*: These may not be passed because each frame of the
221+
animation much be exactly the same size in pixels.
222+
- *format*: This is controlled by the MovieWriter.
217223
"""
218224

219225
@abc.abstractmethod
@@ -351,6 +357,7 @@ def finish(self):
351357

352358
def grab_frame(self, **savefig_kwargs):
353359
# docstring inherited
360+
_validate_grabframe_kwargs(savefig_kwargs)
354361
_log.debug('MovieWriter.grab_frame: Grabbing frame.')
355362
# Readjust the figure size in case it has been changed by the user.
356363
# All frames must have the same size to save the movie correctly.
@@ -457,6 +464,7 @@ def _base_temp_name(self):
457464
def grab_frame(self, **savefig_kwargs):
458465
# docstring inherited
459466
# Creates a filename for saving using basename and counter.
467+
_validate_grabframe_kwargs(savefig_kwargs)
460468
path = Path(self._base_temp_name() % self._frame_counter)
461469
self._temp_paths.append(path) # Record the filename for later use.
462470
self._frame_counter += 1 # Ensures each created name is unique.
@@ -491,6 +499,7 @@ def setup(self, fig, outfile, dpi=None):
491499
self._frames = []
492500

493501
def grab_frame(self, **savefig_kwargs):
502+
_validate_grabframe_kwargs(savefig_kwargs)
494503
buf = BytesIO()
495504
self.fig.savefig(
496505
buf, **{**savefig_kwargs, "format": "rgba", "dpi": self.dpi})
@@ -747,6 +756,7 @@ def setup(self, fig, outfile, dpi=None, frame_dir=None):
747756
self._clear_temp = False
748757

749758
def grab_frame(self, **savefig_kwargs):
759+
_validate_grabframe_kwargs(savefig_kwargs)
750760
if self.embed_frames:
751761
# Just stop processing if we hit the limit
752762
if self._hit_limit:
@@ -1776,3 +1786,16 @@ def _draw_frame(self, framedata):
17761786
a.set_animated(self._blit)
17771787

17781788
save_count = _api.deprecate_privatize_attribute("3.7")
1789+
1790+
1791+
def _validate_grabframe_kwargs(savefig_kwargs):
1792+
if mpl.rcParams['savefig.bbox'] == 'tight':
1793+
raise ValueError(
1794+
f"{mpl.rcParams['savefig.bbox']=} must not be 'tight' as it "
1795+
"may cause frame size to vary, which is inappropriate for animation."
1796+
)
1797+
for k in ('dpi', 'bbox_inches', 'format'):
1798+
if k in savefig_kwargs:
1799+
raise TypeError(
1800+
f"grab_frame got an unexpected keyword argument {k!r}"
1801+
)

lib/matplotlib/tests/test_animation.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ def setup(self, fig, outfile, dpi, *args):
6262
self._count = 0
6363

6464
def grab_frame(self, **savefig_kwargs):
65+
from matplotlib.animation import _validate_grabframe_kwargs
66+
_validate_grabframe_kwargs(savefig_kwargs)
6567
self.savefig_kwargs = savefig_kwargs
6668
self._count += 1
6769

@@ -193,6 +195,38 @@ def test_save_animation_smoketest(tmpdir, writer, frame_format, output, anim):
193195
del anim
194196

195197

198+
@pytest.mark.parametrize('writer, frame_format, output', gen_writers())
199+
def test_grabframe(tmpdir, writer, frame_format, output):
200+
WriterClass = animation.writers[writer]
201+
202+
if frame_format is not None:
203+
plt.rcParams["animation.frame_format"] = frame_format
204+
205+
fig, ax = plt.subplots()
206+
207+
dpi = None
208+
codec = None
209+
if writer == 'ffmpeg':
210+
# Issue #8253
211+
fig.set_size_inches((10.85, 9.21))
212+
dpi = 100.
213+
codec = 'h264'
214+
215+
test_writer = WriterClass()
216+
# Use temporary directory for the file-based writers, which produce a file
217+
# per frame with known names.
218+
with tmpdir.as_cwd():
219+
with test_writer.saving(fig, output, dpi):
220+
# smoke test it works
221+
test_writer.grab_frame()
222+
for k in {'dpi', 'bbox_inches', 'format'}:
223+
with pytest.raises(
224+
TypeError,
225+
match=f"grab_frame got an unexpected keyword argument {k!r}"
226+
):
227+
test_writer.grab_frame(**{k: object()})
228+
229+
196230
@pytest.mark.parametrize('writer', [
197231
pytest.param(
198232
'ffmpeg', marks=pytest.mark.skipif(

0 commit comments

Comments
 (0)