diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index fa03f90d7ad8..03bc155c63a6 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -23,6 +23,7 @@ import six from six.moves import xrange, zip +import numpy as np import os import platform import sys @@ -62,9 +63,38 @@ def adjusted_figsize(w, h, dpi, n): + '''Compute figure size so that pixels are a multiple of n + + Parameters + ---------- + w, h : float + Size in inches + + dpi : float + The dpi + + n : int + The target multiple + + Returns + ------- + wnew, hnew : float + The new figure size in inches. + ''' + + # this maybe simplified if / when we adopt consistent rounding for + # pixel size across the whole library + def correct_roundoff(x, dpi, n): + if int(x*dpi) % n != 0: + if int(np.nextafter(x, np.inf)*dpi) % n == 0: + x = np.nextafter(x, np.inf) + elif int(np.nextafter(x, -np.inf)*dpi) % n == 0: + x = np.nextafter(x, -np.inf) + return x + wnew = int(w * dpi / n) * n / dpi hnew = int(h * dpi / n) * n / dpi - return wnew, hnew + return (correct_roundoff(wnew, dpi, n), correct_roundoff(hnew, dpi, n)) # A registry for available MovieWriter classes @@ -278,8 +308,11 @@ def _adjust_frame_size(self): verbose.report('figure size (inches) has been adjusted ' 'from %s x %s to %s x %s' % (wo, ho, w, h), level='helpful') + else: + w, h = self.fig.get_size_inches() verbose.report('frame size in pixels is %s x %s' % self.frame_size, level='debug') + return w, h def setup(self, fig, outfile, dpi=None): ''' @@ -301,7 +334,7 @@ def setup(self, fig, outfile, dpi=None): if dpi is None: dpi = self.fig.dpi self.dpi = dpi - self._adjust_frame_size() + self._w, self._h = self._adjust_frame_size() # Run here so that grab_frame() can write the data to a pipe. This # eliminates the need for temp files. @@ -337,6 +370,10 @@ def grab_frame(self, **savefig_kwargs): verbose.report('MovieWriter.grab_frame: Grabbing frame.', level='debug') try: + # re-adjust the figure size in case it has been changed by the + # user. We must ensure that every frame is the same size or + # the movie will not save correctly. + self.fig.set_size_inches(self._w, self._h) # Tell the figure to save its data to the sink, using the # frame format and dpi. self.fig.savefig(self._frame_sink(), format=self.frame_format, @@ -386,16 +423,21 @@ def isAvailable(cls): if not bin_path: return False try: - p = subprocess.Popen(bin_path, - shell=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - creationflags=subprocess_creation_flags) - p.communicate() - return True + p = subprocess.Popen( + bin_path, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + creationflags=subprocess_creation_flags) + return cls._handle_subprocess(p) except OSError: return False + @classmethod + def _handle_subprocess(cls, process): + process.communicate() + return True + class FileMovieWriter(MovieWriter): '''`MovieWriter` for writing to individual files and stitching at the end. @@ -570,10 +612,18 @@ def output_args(self): return args + ['-y', self.outfile] + @classmethod + def _handle_subprocess(cls, process): + _, err = process.communicate() + # Ubuntu 12.04 ships a broken ffmpeg binary which we shouldn't use + if 'Libav' in err.decode(): + return False + return True + # Combine FFMpeg options with pipe-based writing @writers.register('ffmpeg') -class FFMpegWriter(MovieWriter, FFMpegBase): +class FFMpegWriter(FFMpegBase, MovieWriter): '''Pipe-based ffmpeg writer. Frames are streamed directly to ffmpeg via a pipe and written in a single @@ -594,7 +644,7 @@ def _args(self): # Combine FFMpeg options with temp file-based writing @writers.register('ffmpeg_file') -class FFMpegFileWriter(FileMovieWriter, FFMpegBase): +class FFMpegFileWriter(FFMpegBase, FileMovieWriter): '''File-based ffmpeg writer. Frames are written to temporary files on disk and then stitched diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py index 5f1d8a5ebf8c..017727016fe0 100644 --- a/lib/matplotlib/tests/test_animation.py +++ b/lib/matplotlib/tests/test_animation.py @@ -145,6 +145,14 @@ def test_save_animation_smoketest(tmpdir, writer, extension): ax.set_xlim(0, 10) ax.set_ylim(-1, 1) + dpi = None + codec = None + if writer == 'ffmpeg': + # Issue #8253 + fig.set_size_inches((10.85, 9.21)) + dpi = 100. + codec = 'h264' + def init(): line.set_data([], []) return line, @@ -160,7 +168,8 @@ def animate(i): with tmpdir.as_cwd(): anim = animation.FuncAnimation(fig, animate, init_func=init, frames=5) try: - anim.save('movie.' + extension, fps=30, writer=writer, bitrate=500) + anim.save('movie.' + extension, fps=30, writer=writer, bitrate=500, + dpi=dpi, codec=codec) except UnicodeDecodeError: pytest.xfail("There can be errors in the numpy import stack, " "see issues #1891 and #2679")