Repository: python-pillow/Pillow

Issue:
Attempting to save a RGB image with transparency color as a GIF crashes when transparency color can't be added to palette
Attempting to save a RGB image `img` with a transparency color as a GIF can crash under very rare circumstances. Namely:
 - The result of adaptively quantizing the image must contain a full palette of the maximum 256 colors
 - The transparency color (`img.info["transparency"]`) must not be in this full palette

For example, this could happen when loading a GIF with transparency, converting it to RGB, thumbnailing it, and then attempting to save it.

It may be worth noting that I encountered this issue when upgrading the pillow dependency in an existing project - I'm guessing it might be a regression from the fix for #2803 .

### What did you do?

Ran previously working legacy code that opened a GIF, converted it to RGB, thumbnailed it, and then attempted to save it as a GIF again

### What did you expect to happen?

Saving should have worked

### What actually happened?

```
<env>/lib/python3.11/site-packages/PIL/Image.py:1056: UserWarning: Couldn't allocate palette entry for transparency
  warnings.warn("Couldn't allocate palette entry for transparency")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<env>/lib/python3.11/site-packages/PIL/Image.py", line 2432, in save
    save_handler(self, fp, filename)
  File "<env>/lib/python3.11/site-packages/PIL/GifImagePlugin.py", line 665, in _save
    _write_single_frame(im, fp, palette)
  File "<env>/lib/python3.11/site-packages/PIL/GifImagePlugin.py", line 559, in _write_single_frame
    _write_local_header(fp, im, (0, 0), flags)
  File "<env>/lib/python3.11/site-packages/PIL/GifImagePlugin.py", line 690, in _write_local_header
    transparency = int(transparency)
                   ^^^^^^^^^^^^^^^^^
```

Yes, this is a dead ringer for #2803 , but it's a different (if related) issue - it's still present in 9.5.0.

### What are your OS, Python and Pillow versions?

* OS: Linux
* Python: 3.11.x
* Pillow: 9.5.0

This code doesn't actually load a GIF - I'm not sure I'm allowed to share the image that the issue occurred with, and actually constructing a GIF file that triggers the issue is somewhat difficult. However, this code triggers the same code path.

### Reproducing

```python

import io
from PIL import Image

img = Image.new("RGB", (16, 16))

for ii in range(256):
    img.putpixel((ii//16, ii%16), (ii, ii, ii))

img.info["transparency"] = (0, 255, 0)
img.save(io.BytesIO(), format="GIF")

```
Note that setting `img.info["transparency"]` to a value that is present in the palette - like `(0, 0, 0)` - will not cause the issue.

### Code path

I wanted to make sure that this report is accurate, especially since a very similar issue already exists, so I actually made sure to understand exactly what is happening. This should save quite some time understanding the cause of the issue.

The GIF plugin normalizes RGB images to P before saving:
https://github.com/python-pillow/Pillow/blob/9.5.x/src/PIL/GifImagePlugin.py#L484

The conversion code does its best to convert the transparency as well. It even seemingly guards against this very issue, by deleting the transparency info in the converted image:
https://github.com/python-pillow/Pillow/blob/9.5.x/src/PIL/Image.py#L1051

However, when the frame is written, this being deleted means that it is also not written to the `encoderinfo` of the original image:
https://github.com/python-pillow/Pillow/blob/9.5.x/src/PIL/GifImagePlugin.py#L548

And then, when the local header is written, the original RGB image is passed to the function responsible for that for some reason:
https://github.com/python-pillow/Pillow/blob/9.5.x/src/PIL/GifImagePlugin.py#L559

And finally, this means that there is no `transparency` in `im.encoderinfo` here. So we fall back to the original image, where this info exists, but as a tuple. And while `ValueError` is guarded against here, `TypeError` isn't.
https://github.com/python-pillow/Pillow/blob/9.5.x/src/PIL/GifImagePlugin.py#L687


Discussion:

Resolving patch:
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -683,11 +683,7 @@ def get_interlace(im):
 def _write_local_header(fp, im, offset, flags):
     transparent_color_exists = False
     try:
-        if "transparency" in im.encoderinfo:
-            transparency = im.encoderinfo["transparency"]
-        else:
-            transparency = im.info["transparency"]
-        transparency = int(transparency)
+        transparency = int(im.encoderinfo["transparency"])
     except (KeyError, ValueError):
         pass
     else:


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
@@ -1086,6 +1086,21 @@ def test_transparent_optimize(tmp_path):
         assert reloaded.info["transparency"] == reloaded.getpixel((252, 0))
 
 
+def test_removed_transparency(tmp_path):
+    out = str(tmp_path / "temp.gif")
+    im = Image.new("RGB", (256, 1))
+
+    for x in range(256):
+        im.putpixel((x, 0), (x, 0, 0))
+
+    im.info["transparency"] = (255, 255, 255)
+    with pytest.warns(UserWarning):
+        im.save(out)
+
+    with Image.open(out) as reloaded:
+        assert "transparency" not in reloaded.info
+
+
 def test_rgb_transparency(tmp_path):
     out = str(tmp_path / "temp.gif")