Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e80cd21

Browse files
authored
Merge pull request #25631 from tacaswell/api/restrict_grabframe
API: forbid unsafe savefig kwargs to AbstractMovieWriter.grab_frame
2 parents 49f657a + 1d4ab69 commit e80cd21

File tree

4 files changed

+90
-16
lines changed

4 files changed

+90
-16
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: 36 additions & 13 deletions
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
@@ -227,12 +233,18 @@ def saving(self, fig, outfile, dpi, *args, **kwargs):
227233
228234
``*args, **kw`` are any parameters that should be passed to `setup`.
229235
"""
236+
if mpl.rcParams['savefig.bbox'] == 'tight':
237+
_log.info("Disabling savefig.bbox = 'tight', as it may cause "
238+
"frame size to vary, which is inappropriate for "
239+
"animation.")
240+
230241
# This particular sequence is what contextlib.contextmanager wants
231242
self.setup(fig, outfile, dpi, *args, **kwargs)
232-
try:
233-
yield self
234-
finally:
235-
self.finish()
243+
with mpl.rc_context({'savefig.bbox': None}):
244+
try:
245+
yield self
246+
finally:
247+
self.finish()
236248

237249

238250
class MovieWriter(AbstractMovieWriter):
@@ -351,6 +363,7 @@ def finish(self):
351363

352364
def grab_frame(self, **savefig_kwargs):
353365
# docstring inherited
366+
_validate_grabframe_kwargs(savefig_kwargs)
354367
_log.debug('MovieWriter.grab_frame: Grabbing frame.')
355368
# Readjust the figure size in case it has been changed by the user.
356369
# All frames must have the same size to save the movie correctly.
@@ -457,6 +470,7 @@ def _base_temp_name(self):
457470
def grab_frame(self, **savefig_kwargs):
458471
# docstring inherited
459472
# Creates a filename for saving using basename and counter.
473+
_validate_grabframe_kwargs(savefig_kwargs)
460474
path = Path(self._base_temp_name() % self._frame_counter)
461475
self._temp_paths.append(path) # Record the filename for later use.
462476
self._frame_counter += 1 # Ensures each created name is unique.
@@ -491,6 +505,7 @@ def setup(self, fig, outfile, dpi=None):
491505
self._frames = []
492506

493507
def grab_frame(self, **savefig_kwargs):
508+
_validate_grabframe_kwargs(savefig_kwargs)
494509
buf = BytesIO()
495510
self.fig.savefig(
496511
buf, **{**savefig_kwargs, "format": "rgba", "dpi": self.dpi})
@@ -747,6 +762,7 @@ def setup(self, fig, outfile, dpi=None, frame_dir=None):
747762
self._clear_temp = False
748763

749764
def grab_frame(self, **savefig_kwargs):
765+
_validate_grabframe_kwargs(savefig_kwargs)
750766
if self.embed_frames:
751767
# Just stop processing if we hit the limit
752768
if self._hit_limit:
@@ -1051,10 +1067,6 @@ def func(current_frame: int, total_frames: int) -> Any
10511067
# TODO: Right now, after closing the figure, saving a movie won't work
10521068
# since GUI widgets are gone. Either need to remove extra code to
10531069
# allow for this non-existent use case or find a way to make it work.
1054-
if mpl.rcParams['savefig.bbox'] == 'tight':
1055-
_log.info("Disabling savefig.bbox = 'tight', as it may cause "
1056-
"frame size to vary, which is inappropriate for "
1057-
"animation.")
10581070

10591071
facecolor = savefig_kwargs.get('facecolor',
10601072
mpl.rcParams['savefig.facecolor'])
@@ -1070,10 +1082,8 @@ def _pre_composite_to_white(color):
10701082
# canvas._is_saving = True makes the draw_event animation-starting
10711083
# callback a no-op; canvas.manager = None prevents resizing the GUI
10721084
# widget (both are likewise done in savefig()).
1073-
with mpl.rc_context({'savefig.bbox': None}), \
1074-
writer.saving(self._fig, filename, dpi), \
1075-
cbook._setattr_cm(self._fig.canvas,
1076-
_is_saving=True, manager=None):
1085+
with writer.saving(self._fig, filename, dpi), \
1086+
cbook._setattr_cm(self._fig.canvas, _is_saving=True, manager=None):
10771087
for anim in all_anim:
10781088
anim._init_draw() # Clear the initial frame
10791089
frame_number = 0
@@ -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/mpl-data/matplotlibrc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -690,9 +690,8 @@
690690
#savefig.edgecolor: auto # figure edge color when saving
691691
#savefig.format: png # {png, ps, pdf, svg}
692692
#savefig.bbox: standard # {tight, standard}
693-
# 'tight' is incompatible with pipe-based animation
694-
# backends (e.g. 'ffmpeg') but will work with those
695-
# based on temporary files (e.g. 'ffmpeg_file')
693+
# 'tight' is incompatible with generating frames
694+
# for animation
696695
#savefig.pad_inches: 0.1 # padding to be used, when bbox is set to 'tight'
697696
#savefig.directory: ~ # default directory in savefig dialog, gets updated after
698697
# interactive saves, unless set to the empty string (i.e.

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)