Repository: python-pillow/Pillow

Issue:
Broken background on transparent GIFs built from individual frames
The background of transparent GIFs constructed from Images seems to not quite see the transparent color correctly.

```py
from PIL import Image

with Image.open('test.gif') as im:
	images = []
	durations = []
	for i in range(im.n_frames):
		im.seek(i)
		im.convert('RGBA')
		im_temp = Image.new('RGBA',im.size,(0,0,0,0))
		im_temp.paste(im,(0,0))
		images.append(im_temp)
		durations.append(im.info['duration'])
	images[0].save(
		'test-out.gif', 
		interlace=True,
		save_all=True,
		append_images=images[1:],
		loop=0,
		duration=durations,
		disposal=2,
		transparency = 255,
		background = 255,
		optimize=False
	)
```
This should copy the image perfectly, but there are artifacts.
![Input](https://user-images.githubusercontent.com/59123926/157148158-e21b8ba6-bcfa-4c50-bcbc-3ddc41ed1b48.gif)
![Output](https://user-images.githubusercontent.com/59123926/157148265-a302bff2-5be0-40e0-9f4c-dbe8f97b12c4.gif)
[Here's the venv I used to test this, in a zip.](https://github.com/python-pillow/Pillow/files/8201983/pillowtest.zip)


Discussion:
I suspect that setting transparency and background to 255 is the issue. These values are indexes into the color palette but your palette only has 13 entries, not 255.  I fed the test input and output .gifs provided by @balt-is-you-and-shift into a small script I wrote, and here is the output:
```
% python3 mk_ani.py -i test.gif
PIL 9.0.1
"test.gif" size=(32, 32) mode=P is_ani=True #frames=8 loop=0 palette_size=13
0: disposal=2 transparency=0 duration=1000 background='idx=0 color=(0, 0, 0) #000000'
1: disposal=2 transparency=None duration=50 background='idx=0 color=(0, 0, 0) #000000'
2: disposal=2 transparency=None duration=70 background='idx=0 color=(0, 0, 0) #000000'
3: disposal=2 transparency=None duration=200 background='idx=0 color=(0, 0, 0) #000000'
4: disposal=2 transparency=None duration=100 background='idx=0 color=(0, 0, 0) #000000'
5: disposal=2 transparency=None duration=100 background='idx=0 color=(0, 0, 0) #000000'
6: disposal=2 transparency=None duration=100 background='idx=0 color=(0, 0, 0) #000000'
7: disposal=2 transparency=None duration=50 background='idx=0 color=(0, 0, 0) #000000'
% python3 mk_ani.py -i test-out.gif
PIL 9.0.1
"test-out.gif" size=(32, 32) mode=P is_ani=True #frames=8 loop=0 palette_size=13
0: disposal=2 transparency=255 duration=1000 background='idx=255 color=None None'
1: disposal=2 transparency=None duration=50 background='idx=255 color=None None'
2: disposal=2 transparency=None duration=70 background='idx=255 color=None None'
3: disposal=2 transparency=None duration=200 background='idx=255 color=None None'
4: disposal=2 transparency=None duration=100 background='idx=255 color=None None'
5: disposal=2 transparency=None duration=100 background='idx=255 color=None None'
6: disposal=2 transparency=None duration=100 background='idx=255 color=None None'
7: disposal=2 transparency=None duration=50 background='idx=255 color=None None'
```
When I copied the background and transparency from the source to the save() call, I think I got the results you desire:
```
% python3 mk_ani.py -i test-out-t0-bg-none.gif
PIL 9.0.1
"test-out-t0-bg-none.gif" size=(32, 32) mode=P is_ani=True #frames=8 loop=0 palette_size=13
0: disposal=2 transparency=0 duration=1000 background='idx=0 color=(0, 0, 0) #000000'
1: disposal=2 transparency=None duration=50 background='idx=0 color=(0, 0, 0) #000000'
2: disposal=2 transparency=None duration=70 background='idx=0 color=(0, 0, 0) #000000'
3: disposal=2 transparency=None duration=200 background='idx=0 color=(0, 0, 0) #000000'
4: disposal=2 transparency=None duration=100 background='idx=0 color=(0, 0, 0) #000000'
5: disposal=2 transparency=None duration=100 background='idx=0 color=(0, 0, 0) #000000'
6: disposal=2 transparency=None duration=100 background='idx=0 color=(0, 0, 0) #000000'
7: disposal=2 transparency=None duration=50 background='idx=0 color=(0, 0, 0) #000000'
```
![test-out-t0-bg-none](https://user-images.githubusercontent.com/33208090/157178443-0713496d-d0fb-4f54-98ad-a39ee2c85a42.gif)
Take a look at this. If I set the transparency using the key from the info dictionary, it works.

```python
from PIL import Image

with Image.open('test.gif') as im:
	images = []
	durations = []
	transparency = im.info["transparency"]
	for i in range(im.n_frames):
		im.seek(i)
		im.convert('RGBA')
		im_temp = Image.new('RGBA',im.size,(0,0,0,0))
		im_temp.paste(im,(0,0))
		images.append(im_temp)
		durations.append(im.info['duration'])
	images[0].save(
		'test-out.gif', 
		interlace=True,
		save_all=True,
		append_images=images[1:],
		loop=0,
		duration=durations,
		disposal=2,
		transparency = transparency,
		background = 255,
		optimize=False
	)
```
![test-out](https://user-images.githubusercontent.com/3112309/157183652-50170ee9-bf1b-4e83-a484-b646d38ca402.gif)

This wouldn't solve the issue well, bad example. I'll make a better example of the issue when I can.
[test-v2.zip](https://github.com/python-pillow/Pillow/files/8207292/test-v2.zip)
Not very copy-pastable, but it's a much better example of the problem at hand.
 
_Sorry for the multiple edits, I'm trying to get an example that best shows the issue._

```py
from PIL import Image
import json

with open('durations.json','rb') as f:
  durations = json.load(f)
images = []
for n in range(8):
  with Image.open(f'test{n}.png') as im:
    im = im.convert('RGBA').resize([n*8 for n in im.size],Image.NEAREST)
    images.append(im)
images[0].save( #GIFs have this issue
  'test.gif', 
  interlace=True,
  save_all=True,
  append_images=images[1:],
  loop=0,
  duration=durations,
  disposal=2,
  transparency = 255,
  background = 255,
  optimize=False
)
images[0].save( #APNGs don't have this issue, showing it's an issue with GIF saving
  'test.png', 
  format='PNG',
  save_all=True,
  append_images=images,
  default_image=True,
  loop=0,
  durations=durations
)
```
![test](https://user-images.githubusercontent.com/59123926/157268862-d96545ce-bc57-44e2-bea9-497f03d9f704.png) 
APNG
![test](https://user-images.githubusercontent.com/59123926/157268863-ea294ebd-c9f0-4b7b-820f-89a827e4c91e.gif) 
GIF
@resnbl @radarhere
Not sure why you are setting transparency=255 (or why PIL is not complaining about it!). According to the docs (https://pillow.readthedocs.io/en/latest/handbook/image-file-formats.html#saving), transparency is an "index", i.e. offset into the color palette indicating which "color" should denote transparent pixels. Your image palette does not contain 256 colors (only 13 are used), so you are not getting the proper results. For the test provided, using transparency=0 works.

The same applies to the background setting: it is an index into the color palette. Again, setting to 0 will get what you want for this. (@radarhere : your solution is missing this part as well.)

FYI: here is the color palette for your generated GIF:
```
Global palette:
  0 #000000 (0, 0, 0)
  1 #00E436 (0, 228, 54)
  2 #FFEC27 (255, 236, 39)
  3 #FF004D (255, 0, 77)
  4 #29ADFF (41, 173, 255)
  5 #00B543 (0, 181, 67)
  6 #FF6C24 (255, 108, 36)
  7 #BE1250 (190, 18, 80)
  8 #065AB5 (6, 90, 181)
  9 #A8E72E (168, 231, 46)
  10 #C2C3C7 (194, 195, 199)
  11 #F3EF7D (243, 239, 125)
  12 #FF6E59 (255, 110, 89)
```
If there is anything amiss with PIL, I think it is that the `.save()` method did not complain about the transparency and background arguments being outside the range of the generated palette table.

On a side note: I went through the same thing as you: generating an animated image from a list of images. In my solution, I chose a file naming convention over a .json durations file. I named my inputs "nn-dDDD-blah.png" where "nn" is the image order index (for easily sorting the output of a glob.glob('*.png') call), "DDD" is the duration in msecs., and "blah" is anything but was used for the default output filename "blah.{gif|png}". Just another way of doing things...

![test](https://user-images.githubusercontent.com/33208090/157293804-c9851207-87d4-4c3f-be20-6aa80fcbd021.gif)

See, the problem with that is that _setting transparency and background to 255 worked fine in previous versions, and setting them both to 0 won't work if the majority of the image isn't transparent._

Resolving patch:
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -424,7 +424,7 @@ def _close__fp(self):
 RAWMODE = {"1": "L", "L": "L", "P": "P"}
 
 
-def _normalize_mode(im, initial_call=False):
+def _normalize_mode(im):
     """
     Takes an image (or frame), returns an image in a mode that is appropriate
     for saving in a Gif.
@@ -432,31 +432,20 @@ def _normalize_mode(im, initial_call=False):
     It may return the original image, or it may return an image converted to
     palette or 'L' mode.
 
-    UNDONE: What is the point of mucking with the initial call palette, for
-    an image that shouldn't have a palette, or it would be a mode 'P' and
-    get returned in the RAWMODE clause.
-
     :param im: Image object
-    :param initial_call: Default false, set to true for a single frame.
     :returns: Image object
     """
     if im.mode in RAWMODE:
         im.load()
         return im
     if Image.getmodebase(im.mode) == "RGB":
-        if initial_call:
-            palette_size = 256
-            if im.palette:
-                palette_size = len(im.palette.getdata()[1]) // 3
-            im = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=palette_size)
-            if im.palette.mode == "RGBA":
-                for rgba in im.palette.colors.keys():
-                    if rgba[3] == 0:
-                        im.info["transparency"] = im.palette.colors[rgba]
-                        break
-            return im
-        else:
-            return im.convert("P")
+        im = im.convert("P", palette=Image.Palette.ADAPTIVE)
+        if im.palette.mode == "RGBA":
+            for rgba in im.palette.colors.keys():
+                if rgba[3] == 0:
+                    im.info["transparency"] = im.palette.colors[rgba]
+                    break
+        return im
     return im.convert("L")
 
 
@@ -514,7 +503,7 @@ def _normalize_palette(im, palette, info):
 
 
 def _write_single_frame(im, fp, palette):
-    im_out = _normalize_mode(im, True)
+    im_out = _normalize_mode(im)
     for k, v in im_out.info.items():
         im.encoderinfo.setdefault(k, v)
     im_out = _normalize_palette(im_out, palette, im.encoderinfo)
@@ -646,11 +635,14 @@ def get_interlace(im):
 def _write_local_header(fp, im, offset, flags):
     transparent_color_exists = False
     try:
-        transparency = im.encoderinfo["transparency"]
-    except KeyError:
+        if "transparency" in im.encoderinfo:
+            transparency = im.encoderinfo["transparency"]
+        else:
+            transparency = im.info["transparency"]
+        transparency = int(transparency)
+    except (KeyError, ValueError):
         pass
     else:
-        transparency = int(transparency)
         # optimize the block away if transparent color is not used
         transparent_color_exists = True
 


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
@@ -842,6 +842,17 @@ def test_rgb_transparency(tmp_path):
         assert "transparency" not in reloaded.info
 
 
+def test_rgba_transparency(tmp_path):
+    out = str(tmp_path / "temp.gif")
+
+    im = hopper("P")
+    im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])
+
+    with Image.open(out) as reloaded:
+        reloaded.seek(1)
+        assert_image_equal(hopper("P").convert("RGB"), reloaded)
+
+
 def test_bbox(tmp_path):
     out = str(tmp_path / "temp.gif")