Skip to content

Commit e0e346d

Browse files
committed
Pillow animation writer.
1 parent bd75712 commit e0e346d

File tree

4 files changed

+84
-16
lines changed

4 files changed

+84
-16
lines changed

doc/api/animation_api.rst

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,20 @@ Examples
153153
Writer Classes
154154
==============
155155

156-
The provided writers fall into two broad categories: pipe-based and
157-
file-based. The pipe-based writers stream the captured frames over a
158-
pipe to an external process. The pipe-based variants tend to be more
159-
performant, but may not work on all systems.
156+
The provided writers fall into a few broad categories.
157+
158+
The Pillow writer relies on the Pillow library to write the animation, keeping
159+
all data in memory.
160+
161+
.. autosummary::
162+
:toctree: _as_gen
163+
:nosignatures:
164+
165+
PillowWriter
166+
167+
The pipe-based writers stream the captured frames over a pipe to an external
168+
process. The pipe-based variants tend to be more performant, but may not work
169+
on all systems.
160170

161171
.. autosummary::
162172
:toctree: _as_gen
@@ -166,9 +176,9 @@ performant, but may not work on all systems.
166176
ImageMagickFileWriter
167177
AVConvWriter
168178

169-
Alternatively the file-based writers save temporary files for each
170-
frame which are stitched into a single file at the end. Although
171-
slower, these writers can be easier to debug.
179+
The file-based writers save temporary files for each frame which are stitched
180+
into a single file at the end. Although slower, these writers can be easier to
181+
debug.
172182

173183
.. autosummary::
174184
:toctree: _as_gen
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Writing animations with Pillow
2+
``````````````````````````````
3+
It is now possible to use Pillow as an animation writer. Supported output
4+
formats are currently gif (Pillow>=3.4) and webp (Pillow>=5.0). Use e.g. as ::
5+
6+
from __future__ import division
7+
8+
from matplotlib import pyplot as plt
9+
from matplotlib.animation import FuncAnimation, PillowWriter
10+
11+
fig, ax = plt.subplots()
12+
line, = plt.plot([0, 1])
13+
14+
def animate(i):
15+
line.set_ydata([0, i / 20])
16+
return [line]
17+
18+
anim = FuncAnimation(fig, animate, 20, blit=True)
19+
anim.save("movie.gif", writer=PillowWriter(fps=24))
20+
plt.show()

lib/matplotlib/animation.py

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import abc
2727
import contextlib
28+
from io import BytesIO
2829
import itertools
2930
import logging
3031
import os
@@ -43,10 +44,8 @@
4344

4445
if six.PY2:
4546
from base64 import encodestring as encodebytes
46-
from cStringIO import StringIO as BytesIO
4747
else:
4848
from base64 import encodebytes
49-
from io import BytesIO
5049

5150

5251
_log = logging.getLogger(__name__)
@@ -575,6 +574,45 @@ def cleanup(self):
575574
os.remove(fname)
576575

577576

577+
@writers.register('pillow')
578+
class PillowWriter(MovieWriter):
579+
@classmethod
580+
def isAvailable(cls):
581+
try:
582+
import PIL
583+
except ImportError:
584+
return False
585+
return True
586+
587+
def __init__(self, *args, **kwargs):
588+
if kwargs.get("extra_args") is None:
589+
kwargs["extra_args"] = ()
590+
super(PillowWriter, self).__init__(*args, **kwargs)
591+
592+
def setup(self, fig, outfile, dpi=None):
593+
self._frames = []
594+
self._outfile = outfile
595+
self._dpi = dpi
596+
self._fig = fig
597+
598+
def grab_frame(self, **savefig_kwargs):
599+
from PIL import Image
600+
buf = BytesIO()
601+
self._fig.savefig(buf, **dict(savefig_kwargs, format="rgba"))
602+
renderer = self._fig.canvas.get_renderer()
603+
# Using frombuffer / getbuffer may be slightly more efficient, but
604+
# Py3-only.
605+
self._frames.append(Image.frombytes(
606+
"RGBA",
607+
(int(renderer.width), int(renderer.height)),
608+
buf.getvalue()))
609+
610+
def finish(self):
611+
self._frames[0].save(
612+
self._outfile, save_all=True, append_images=self._frames[1:],
613+
duration=int(1000 / self.fps))
614+
615+
578616
# Base class of ffmpeg information. Has the config keys and the common set
579617
# of arguments that controls the *output* side of things.
580618
class FFMpegBase(object):
@@ -1138,8 +1176,8 @@ class to use, such as 'ffmpeg'. If ``None``, defaults to
11381176

11391177
if 'bbox_inches' in savefig_kwargs:
11401178
_log.warning("Warning: discarding the 'bbox_inches' argument in "
1141-
"'savefig_kwargs' as it may cause frame size "
1142-
"to vary, which is inappropriate for animation.")
1179+
"'savefig_kwargs' as it may cause frame size "
1180+
"to vary, which is inappropriate for animation.")
11431181
savefig_kwargs.pop('bbox_inches')
11441182

11451183
# Create a new sequence of frames for saved data. This is different
@@ -1150,16 +1188,15 @@ class to use, such as 'ffmpeg'. If ``None``, defaults to
11501188
# allow for this non-existent use case or find a way to make it work.
11511189
with rc_context():
11521190
if rcParams['savefig.bbox'] == 'tight':
1153-
_log.info("Disabling savefig.bbox = 'tight', as it "
1154-
"may cause frame size to vary, which "
1155-
"is inappropriate for animation.")
1191+
_log.info("Disabling savefig.bbox = 'tight', as it may cause "
1192+
"frame size to vary, which is inappropriate for "
1193+
"animation.")
11561194
rcParams['savefig.bbox'] = None
11571195
with writer.saving(self._fig, filename, dpi):
11581196
for anim in all_anim:
11591197
# Clear the initial frame
11601198
anim._init_draw()
1161-
for data in zip(*[a.new_saved_frame_seq()
1162-
for a in all_anim]):
1199+
for data in zip(*[a.new_saved_frame_seq() for a in all_anim]):
11631200
for anim, d in zip(all_anim, data):
11641201
# TODO: See if turning off blit is really necessary
11651202
anim._draw_next_frame(d, blit=False)

lib/matplotlib/tests/test_animation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def isAvailable(self):
117117
('avconv_file', 'movie.mp4'),
118118
('imagemagick', 'movie.gif'),
119119
('imagemagick_file', 'movie.gif'),
120+
('pillow', 'movie.gif'),
120121
('html', 'movie.html'),
121122
('null', 'movie.null')
122123
]

0 commit comments

Comments
 (0)