Repository: python-pillow/Pillow

Issue:
GIF degrading after initial frame when import
I'm trying to create a programatic assembler for a variety of animated gifs. I want to do this by grabbing each of the parts (each individually animated) and layering them to create the final GIF. All parts apart from the background have transparency.

```python
from PIL import Image, ImageSequence, GifImagePlugin
import os


bg = Image.open('images/bg/Background Sky.gif')
body = Image.open('images/body/Body Blue.gif')
eyes = Image.open('images/eyes/Eyes Green.gif')
headwear = Image.open('images/headwear/Headwear Party Hat.gif')

frames = []
for idx, frame in enumerate(ImageSequence.Iterator(bg)):
    frame = Image.new('RGBA', (640, 640), color=(255,0,0,0))
    frame.paste(ImageSequence.Iterator(bg)[idx])
    frame.paste(ImageSequence.Iterator(body)[idx], mask=ImageSequence.Iterator(body)[idx].convert("RGBA"))
    frame.paste(ImageSequence.Iterator(eyes)[idx], mask=ImageSequence.Iterator(eyes)[idx].convert("RGBA"))
    frame.paste(ImageSequence.Iterator(headwear)[idx], mask=ImageSequence.Iterator(headwear)[idx].convert("RGBA"))
    
    frames.append(frame)

print(frames)

#frames[0].save('images/generated/output.gif', save_all=True, append_images=frames[0:])
frames[0].save('images/generated/output.gif', save_all=True, append_images=frames[1:], loop=0)
```

This is the final output: 
![output](https://user-images.githubusercontent.com/33958787/141956668-a4d6d5c9-0039-46dc-b97c-9953b03681ac.gif)

This is the raw background: 
![Background Sky](https://user-images.githubusercontent.com/33958787/141956768-b3e67dc6-77a6-410d-9216-538f94d4a8cb.gif)


I have tried to set the frame to a copy of the frame within the for loop:
`frame = frame.copy()`

But this fails also.


Discussion:
Upon further investigation, it seems that non-animated parts of the gif are 'removed' on subsequent frames. 
You've provided "Background Sky.gif". Would you also be able to provide "Body Blue.gif", "Eyes Green.gif" and "Headwear Party Hat.gif"?
I realise they are probably not the core of the problem, but it would just be helpful to be able to easily run your sample code.
Let's simplify it...

```python
from PIL import Image, ImageSequence, GifImagePlugin

bg = Image.open('images/bg/Background Sky.gif')

frames = []
for idx, frame in enumerate(ImageSequence.Iterator(bg)):
    frame = Image.new('RGBA', (640, 640), color=(255,0,0,0))
    frame.paste(ImageSequence.Iterator(bg)[idx])
    
    frames.append(frame)

print(frames)

frames[0].save('images/generated/output.gif', save_all=True, append_images=frames[1:], loop=0)
```

The issue occurs on its own. Have tried an array of different ways, including tweaking the disposal. See below saves each frame individually but gives the same result:

```python
from PIL import Image, ImageSequence, GifImagePlugin
import os

bg = Image.open('images/bg/Background Starry Night.gif')

print(bg.info)
print(bg.palette)
print(bg.mode)
print(bg.format)
print(bg.load())

print(bg.n_frames)
bg.convert('RGBA')
bg.save('images/generated/outputmain.gif', format='GIF', save_all=True, disposal=2)
for idx in range(bg.n_frames):
    bg.seek(idx)
    bg.save('images/generated/output'+ str(idx) +'.png', disposal=2, optimize=False, interlace=False, include_color_table=True)
```


Resolving patch:
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -124,8 +124,7 @@ def seek(self, frame):
         if not self._seek_check(frame):
             return
         if frame < self.__frame:
-            if frame != 0:
-                self.im = None
+            self.im = None
             self._seek(0)
 
         last_frame = self.__frame
@@ -165,12 +164,21 @@ def _seek(self, frame):
                 pass
             self.__offset = 0
 
+        if self.__frame == 1:
+            self.pyaccess = None
+            if "transparency" in self.info:
+                self.mode = "RGBA"
+                self.im.putpalettealpha(self.info["transparency"], 0)
+                self.im = self.im.convert("RGBA", Image.FLOYDSTEINBERG)
+
+                del self.info["transparency"]
+            else:
+                self.mode = "RGB"
+                self.im = self.im.convert("RGB", Image.FLOYDSTEINBERG)
         if self.dispose:
             self.im.paste(self.dispose, self.dispose_extent)
 
-        from copy import copy
-
-        self.palette = copy(self.global_palette)
+        palette = None
 
         info = {}
         frame_transparency = None
@@ -246,7 +254,7 @@ def _seek(self, frame):
 
                 if flags & 128:
                     bits = (flags & 7) + 1
-                    self.palette = ImagePalette.raw("RGB", self.fp.read(3 << bits))
+                    palette = ImagePalette.raw("RGB", self.fp.read(3 << bits))
 
                 # image data
                 bits = self.fp.read(1)[0]
@@ -257,6 +265,15 @@ def _seek(self, frame):
                 pass
                 # raise OSError, "illegal GIF tag `%x`" % s[0]
 
+        frame_palette = palette or self.global_palette
+
+        def _rgb(color):
+            if frame_palette:
+                color = tuple(frame_palette.palette[color * 3 : color * 3 + 3])
+            else:
+                color = (color, color, color)
+            return color
+
         try:
             if self.disposal_method < 2:
                 # do not dispose or none specified
@@ -272,9 +289,13 @@ def _seek(self, frame):
 
                 # by convention, attempt to use transparency first
                 color = self.info.get("transparency", frame_transparency)
-                if color is None:
-                    color = self.info.get("background", 0)
-                self.dispose = Image.core.fill("P", dispose_size, color)
+                if color is not None:
+                    dispose_mode = "RGBA"
+                    color = _rgb(color) + (0,)
+                else:
+                    dispose_mode = "RGB"
+                    color = _rgb(self.info.get("background", 0))
+                self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
             else:
                 # replace with previous contents
                 if self.im:
@@ -286,24 +307,20 @@ def _seek(self, frame):
 
                     Image._decompression_bomb_check(dispose_size)
                     self.dispose = Image.core.fill(
-                        "P", dispose_size, frame_transparency
+                        "RGBA", dispose_size, _rgb(frame_transparency) + (0,)
                     )
         except AttributeError:
             pass
 
         if interlace is not None:
-            transparency = -1
-            if frame_transparency is not None:
-                if frame == 0:
-                    self.info["transparency"] = frame_transparency
-                else:
-                    transparency = frame_transparency
+            if frame == 0 and frame_transparency is not None:
+                self.info["transparency"] = frame_transparency
             self.tile = [
                 (
                     "gif",
                     (x0, y0, x1, y1),
                     self.__offset,
-                    (bits, interlace, transparency),
+                    (bits, interlace),
                 )
             ]
         else:
@@ -316,16 +333,54 @@ def _seek(self, frame):
             elif k in self.info:
                 del self.info[k]
 
-        self.mode = "L"
-        if self.palette:
-            self.mode = "P"
+        if frame == 0:
+            self.mode = "P" if frame_palette else "L"
+
+            if self.mode == "P" and not palette:
+                from copy import copy
+
+                palette = copy(self.global_palette)
+            self.palette = palette
+        else:
+            self._frame_palette = frame_palette
+            self._frame_transparency = frame_transparency
 
     def load_prepare(self):
-        if not self.im and "transparency" in self.info:
-            self.im = Image.core.fill(self.mode, self.size, self.info["transparency"])
+        if self.__frame == 0:
+            if "transparency" in self.info:
+                self.im = Image.core.fill(
+                    self.mode, self.size, self.info["transparency"]
+                )
+        else:
+            self._prev_im = self.im
+            if self._frame_palette:
+                self.mode = "P"
+                self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
+                self.im.putpalette(*self._frame_palette.getdata())
+                self._frame_palette = None
+            else:
+                self.mode = "L"
+                self.im = None
 
         super().load_prepare()
 
+    def load_end(self):
+        if self.__frame == 0:
+            return
+        if self._frame_transparency is not None:
+            self.im.putpalettealpha(self._frame_transparency, 0)
+            frame_im = self.im.convert("RGBA")
+        else:
+            frame_im = self.im.convert("RGB")
+        frame_im = self._crop(frame_im, self.dispose_extent)
+
+        self.im = self._prev_im
+        self.mode = self.im.mode
+        if frame_im.mode == "RGBA":
+            self.im.paste(frame_im, self.dispose_extent, frame_im)
+        else:
+            self.im.paste(frame_im, self.dispose_extent)
+
     def tell(self):
         return self.__frame
 
diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py
--- a/src/PIL/PdfImagePlugin.py
+++ b/src/PIL/PdfImagePlugin.py
@@ -135,7 +135,7 @@ def _save(im, fp, filename, save_all=False):
                 procset = "ImageB"  # grayscale
             elif im.mode == "P":
                 filter = "ASCIIHexDecode"
-                palette = im.im.getpalette("RGB")
+                palette = im.getpalette()
                 colorspace = [
                     PdfParser.PdfName("Indexed"),
                     PdfParser.PdfName("DeviceRGB"),


Test patch:
diff --git a/Tests/images/different_transparency_merged.gif b/Tests/images/different_transparency_merged.gif
deleted file mode 100644
Binary files a/Tests/images/different_transparency_merged.gif and /dev/null differ
diff --git a/Tests/images/different_transparency_merged.png b/Tests/images/different_transparency_merged.png
new file mode 100644
Binary files /dev/null and b/Tests/images/different_transparency_merged.png differ
diff --git a/Tests/images/dispose_bgnd_rgba.gif b/Tests/images/dispose_bgnd_rgba.gif
new file mode 100644
Binary files /dev/null and b/Tests/images/dispose_bgnd_rgba.gif differ
diff --git a/Tests/images/dispose_none_load_end_second.gif b/Tests/images/dispose_none_load_end_second.gif
deleted file mode 100644
Binary files a/Tests/images/dispose_none_load_end_second.gif and /dev/null differ
diff --git a/Tests/images/dispose_none_load_end_second.png b/Tests/images/dispose_none_load_end_second.png
new file mode 100644
Binary files /dev/null and b/Tests/images/dispose_none_load_end_second.png differ
diff --git a/Tests/images/dispose_prev_first_frame_seeked.gif b/Tests/images/dispose_prev_first_frame_seeked.gif
deleted file mode 100644
Binary files a/Tests/images/dispose_prev_first_frame_seeked.gif and /dev/null differ
diff --git a/Tests/images/dispose_prev_first_frame_seeked.png b/Tests/images/dispose_prev_first_frame_seeked.png
new file mode 100644
Binary files /dev/null and b/Tests/images/dispose_prev_first_frame_seeked.png differ
diff --git a/Tests/images/missing_background_first_frame.gif b/Tests/images/missing_background_first_frame.gif
deleted file mode 100644
Binary files a/Tests/images/missing_background_first_frame.gif and /dev/null differ
diff --git a/Tests/images/missing_background_first_frame.png b/Tests/images/missing_background_first_frame.png
new file mode 100644
Binary files /dev/null and b/Tests/images/missing_background_first_frame.png differ
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
@@ -163,6 +163,32 @@ def test_roundtrip_save_all(tmp_path):
         assert reread.n_frames == 5
 
 
+@pytest.mark.parametrize(
+    "path, mode",
+    (
+        ("Tests/images/dispose_bgnd.gif", "RGB"),
+        # Hexeditted copy of dispose_bgnd to add transparency
+        ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"),
+    ),
+)
+def test_loading_multiple_palettes(path, mode):
+    with Image.open(path) as im:
+        assert im.mode == "P"
+        first_frame_colors = im.palette.colors.keys()
+        original_color = im.convert("RGB").load()[0, 0]
+
+        im.seek(1)
+        assert im.mode == mode
+        if mode == "RGBA":
+            im = im.convert("RGB")
+
+        # Check a color only from the old palette
+        assert im.load()[0, 0] == original_color
+
+        # Check a color from the new palette
+        assert im.load()[24, 24] not in first_frame_colors
+
+
 def test_headers_saving_for_animated_gifs(tmp_path):
     important_headers = ["background", "version", "duration", "loop"]
     # Multiframe image
@@ -324,7 +350,7 @@ def test_dispose_none_load_end():
     with Image.open("Tests/images/dispose_none_load_end.gif") as img:
         img.seek(1)
 
-        assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.gif")
+        assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png")
 
 
 def test_dispose_background():
@@ -340,12 +366,16 @@ def test_dispose_background():
 def test_dispose_background_transparency():
     with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
         img.seek(2)
-        px = img.convert("RGBA").load()
+        px = img.load()
         assert px[35, 30][3] == 0
 
 
 def test_transparent_dispose():
-    expected_colors = [(2, 1, 2), (0, 1, 0), (2, 1, 2)]
+    expected_colors = [
+        (2, 1, 2),
+        ((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
+        ((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
+    ]
     with Image.open("Tests/images/transparent_dispose.gif") as img:
         for frame in range(3):
             img.seek(frame)
@@ -368,7 +398,7 @@ def test_dispose_previous_first_frame():
     with Image.open("Tests/images/dispose_prev_first_frame.gif") as im:
         im.seek(1)
         assert_image_equal_tofile(
-            im, "Tests/images/dispose_prev_first_frame_seeked.gif"
+            im, "Tests/images/dispose_prev_first_frame_seeked.png"
         )
 
 
@@ -508,7 +538,7 @@ def test_dispose2_background(tmp_path):
 
     with Image.open(out) as im:
         im.seek(1)
-        assert im.getpixel((0, 0)) == 0
+        assert im.getpixel((0, 0)) == (255, 0, 0)
 
 
 def test_transparency_in_second_frame():
@@ -517,9 +547,9 @@ def test_transparency_in_second_frame():
 
         # Seek to the second frame
         im.seek(im.tell() + 1)
-        assert im.info["transparency"] == 0
+        assert "transparency" not in im.info
 
-        assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.gif")
+        assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
 
 
 def test_no_transparency_in_second_frame():
@@ -926,4 +956,4 @@ def test_missing_background():
     # but the disposal method is "Restore to background color"
     with Image.open("Tests/images/missing_background.gif") as im:
         im.seek(1)
-        assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.gif")
+        assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png")