Repository: python-pillow/Pillow

Issue:
Allow use of generators when saving GIF with append_images
### What did you do?

I was trying to save a GIF using ``save_all=True`` and ``append_images=frames``, with ``frames`` being a generator function, not a list.

### What did you expect to happen?

I expected the frames to be saved in the GIF file.

### What actually happened?

I got bitten by Pillow with the following exception:

```
Traceback (most recent call last):
  File "C:\Users\Fabien\Desktop\gif_resize_pillow.py", line 24, in <module>
    om.save("out.gif", save_all=True, append_images=frames)
  File "C:\Python36\lib\site-packages\PIL\Image.py", line 1893, in save
    save_handler(self, fp, filename)
  File "C:\Python36\lib\site-packages\PIL\GifImagePlugin.py", line 451, in _save_all
    _save(im, fp, filename, save_all=True)
  File "C:\Python36\lib\site-packages\PIL\GifImagePlugin.py", line 463, in _save
    if not save_all or not _write_multiple_frames(im, fp, palette):
  File "C:\Python36\lib\site-packages\PIL\GifImagePlugin.py", line 396, in _write_multiple_frames
    for imSequence in [im]+im.encoderinfo.get("append_images", []):
TypeError: can only concatenate list (not "generator") to list
```

Problem come from [this line](https://github.com/python-pillow/Pillow/blob/master/PIL/GifImagePlugin.py#L398) in ``GifImagePlugin.py``, in the function ``_write_multiple_frames``:

``for imSequence in [im]+im.encoderinfo.get("append_images", []):``

Instead of using list concatenation, it should use ``itertools.chain``:

``for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):``

This allow the use of generator functions, but also tuples with ``append_images``.

### What versions of Pillow and Python are you using?

Pillow (4.3.0)

Example script triggering the bug:

```python
from PIL import Image, ImageSequence

size = 320, 240
im = Image.open("in.gif")
frames = ImageSequence.Iterator(im)

def thumbnails(frames):
    for frame in frames:
        thumbnail = frame.copy()
        thumbnail.thumbnail(size, Image.ANTIALIAS)
        yield thumbnail

frames = thumbnails(frames)

om = next(frames) # Handle first frame separately
om.info = im.info # Copy sequence info
om.save("out.gif", save_all=True, append_images=frames) # Boom!
```



Discussion:

Resolving patch:
diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py
--- a/PIL/GifImagePlugin.py
+++ b/PIL/GifImagePlugin.py
@@ -395,7 +395,7 @@ def _write_multiple_frames(im, fp, palette):
 
     im_frames = []
     frame_count = 0
-    for imSequence in [im]+im.encoderinfo.get("append_images", []):
+    for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
         for im_frame in ImageSequence.Iterator(imSequence):
             # a copy is required here since seek can still mutate the image
             im_frame = _normalize_mode(im_frame.copy())
diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py
--- a/PIL/TiffImagePlugin.py
+++ b/PIL/TiffImagePlugin.py
@@ -1785,13 +1785,13 @@ def _save_all(im, fp, filename):
     encoderinfo = im.encoderinfo.copy()
     encoderconfig = im.encoderconfig
     append_images = encoderinfo.get("append_images", [])
-    if not hasattr(im, "n_frames") and not len(append_images):
+    if not hasattr(im, "n_frames") and not append_images:
         return _save(im, fp, filename)
 
     cur_idx = im.tell()
     try:
         with AppendingTiffWriter(fp) as tf:
-            for ims in [im]+append_images:
+            for ims in itertools.chain([im], append_images):
                 ims.encoderinfo = encoderinfo
                 ims.encoderconfig = encoderconfig
                 if not hasattr(ims, "n_frames"):


Test patch:
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -410,8 +410,18 @@ def test_append_images(self):
 
         # Test appending single frame images
         im = Image.new('RGB', (100, 100), '#f00')
-        ims = [Image.new('RGB', (100, 100), color) for color in ['#0f0', '#00f']]
-        im.save(out, save_all=True, append_images=ims)
+        ims = [Image.new('RGB', (100, 100), color) for color
+               in ['#0f0', '#00f']]
+        im.copy().save(out, save_all=True, append_images=ims)
+
+        reread = Image.open(out)
+        self.assertEqual(reread.n_frames, 3)
+
+        # Tests appending using a generator
+        def imGenerator(ims):
+            for im in ims:
+                yield im
+        im.save(out, save_all=True, append_images=imGenerator(ims))
 
         reread = Image.open(out)
         self.assertEqual(reread.n_frames, 3)
diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py
--- a/Tests/test_file_pdf.py
+++ b/Tests/test_file_pdf.py
@@ -75,7 +75,17 @@ def test_save_all(self):
         self.assertGreater(os.path.getsize(outfile), 0)
 
         # Append images
-        im.save(outfile, save_all=True, append_images=[hopper()])
+        ims = [hopper()]
+        im.copy().save(outfile, save_all=True, append_images=ims)
+
+        self.assertTrue(os.path.isfile(outfile))
+        self.assertGreater(os.path.getsize(outfile), 0)
+
+        # Test appending using a generator
+        def imGenerator(ims):
+            for im in ims:
+                yield im
+        im.save(outfile, save_all=True, append_images=imGenerator(ims))
 
         self.assertTrue(os.path.isfile(outfile))
         self.assertGreater(os.path.getsize(outfile), 0)
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -470,7 +470,18 @@ def test_tiff_save_all(self):
         im = Image.new('RGB', (100, 100), '#f00')
         ims = [Image.new('RGB', (100, 100), color) for color
                in ['#0f0', '#00f']]
-        im.save(mp, format="TIFF", save_all=True, append_images=ims)
+        im.copy().save(mp, format="TIFF", save_all=True, append_images=ims)
+
+        mp.seek(0, os.SEEK_SET)
+        reread = Image.open(mp)
+        self.assertEqual(reread.n_frames, 3)
+
+        # Test appending using a generator
+        def imGenerator(ims):
+            for im in ims:
+                yield im
+        mp = io.BytesIO()
+        im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims))
 
         mp.seek(0, os.SEEK_SET)
         reread = Image.open(mp)