Repository: python-pillow/Pillow

Issue:
Incorrect transparency after converting indexed image to RGB
### What did you do?

I use Pillow to convert images on my web site on-demand to .webp format.  It works great! Except for one of two images on this page: https://nedbatchelder.com/text/cardcube.html  The first image is strangely pixellated.  The second looks perfect.

### What did you expect to happen?

The webp conversion should produce the same pixels as the original .png files.

### What actually happened?

Unfortunate graininess.

### What are your OS, Python and Pillow versions?

* OS: MacOS
* Python: 3.9.17
* Pillow: 10.0.0

I tried converting the two .png files into a variety of formats.  The .gif and .bmp look identical to the .png.  The .webp shows the results I see on my site:

```py
from pathlib import Path
from PIL import Image

for n in [1, 2]:
    for fmt in ["WEBP", "GIF", "BMP"]:
        inpath = Path(f"cardcube_step{n}.png")
        outpath = inpath.with_suffix("." + fmt.lower())
        im = Image.open(inpath)
        im.save(outpath, fmt, lossless=True, quality=80, method=6)
```

Here are the two .png files:

![cardcube_step1](https://github.com/python-pillow/Pillow/assets/23789/82b3f28d-6afb-4e02-a536-28483a962a59)
![cardcube_step2](https://github.com/python-pillow/Pillow/assets/23789/00659ca9-a707-40fa-88c2-09b2cc7d1a4b)

Here are the .webp files I get:

[webp.zip](https://github.com/python-pillow/Pillow/files/12063803/webp.zip)



Discussion:
The error is not related to webp, this is because of transparency chunk in the original PNG file:

```python
from pathlib import Path
from PIL import Image

for n in [1, 2]:
    inpath = Path(f"cardcube_step{n}.png")
    im = Image.open(inpath)
    print(im, im.info)
    for mode in ["RGB", "L"]:
        outpath = inpath.with_suffix(f".{mode}.png")
        im.convert(mode).save(outpath)
```

```
<PIL.PngImagePlugin.PngImageFile image mode=P size=473x267 at 0x104D3DB80> {'transparency': 0, 'dpi': (999.9472, 999.9472)}
<PIL.PngImagePlugin.PngImageFile image mode=P size=473x300 at 0x104D3DC10> {'dpi': (999.9725999999999, 999.9725999999999)}
```
Some research about the image:

```python
In [1]: from PIL import Image

In [2]: im = Image.open('cardcube_step1.png')

In [3]: im.info
Out[3]: {'transparency': 0, 'dpi': (999.9472, 999.9472)}

In [4]: im.palette.mode, len(im.palette.palette)
Out[4]: ('RGB', 90)

In [6]: " ".join(map(str, im.histogram()))
Out[6]: '0 492 169 255 261 392 10906 193 368 340 259 429 232 85 489 193 488 181 304 289 420 226 397 271 287 338 105299 243 297 330 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1858'

In [7]: im.convert('RGB').info
Out[7]: {'transparency': (0, 0, 0), 'dpi': (999.9472, 999.9472)}
```

So, this image with palette of 30 elements. The index of transparent pixels is 0, but there are no pixels in the image with such values. Instead, there are 1858 pixels with values outside of the palette (value 255), which interpreted as black.

The problem is that after converting the image from P to RGB mode, pixels outside of the palette turns black, but transparent item in the `im.info` is also converted to black, since the first element in the palette is also black.

There is no easy fix since after converting the image to RGB we can't distinguish true black pixels from transparent black pixels. One of the possible solutions could be deny transparency for `RGB` mode at all since this mode doesn't mean any transparency, although I don't know how this applicable and backward compatible.
Thanks for the analysis.  Why does it convert properly to gif or bmp, but not to webp?
It's because it doesn't convert anything for bmp or gif, those formats support indexed images, like png.

Resolving patch:
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -419,9 +419,11 @@ class BLPEncoder(ImageFile.PyEncoder):
     def _write_palette(self):
         data = b""
         palette = self.im.getpalette("RGBA", "RGBA")
-        for i in range(256):
+        for i in range(len(palette) // 4):
             r, g, b, a = palette[i * 4 : (i + 1) * 4]
             data += struct.pack("<4B", b, g, r, a)
+        while len(data) < 256 * 4:
+            data += b"\x00" * 4
         return data
 
     def encode(self, bufsize):
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -1069,7 +1069,7 @@ def convert_transparency(m, v):
         if mode == "P" and palette != Palette.ADAPTIVE:
             from . import ImagePalette
 
-            new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
+            new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB"))
         if delete_trns:
             # crash fail if we leave a bytes transparency in an rgb/l mode.
             del new_im.info["transparency"]


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
@@ -205,14 +205,14 @@ def test_optimize_full_l():
 
 
 def test_optimize_if_palette_can_be_reduced_by_half():
-    with Image.open("Tests/images/test.colors.gif") as im:
-        # Reduce dimensions because original is too big for _get_optimize()
-        im = im.resize((591, 443))
-    im_rgb = im.convert("RGB")
+    im = Image.new("P", (8, 1))
+    im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150))
+    for i in range(8):
+        im.putpixel((i, 0), (i + 1, 0, 0))
 
     for optimize, colors in ((False, 256), (True, 8)):
         out = BytesIO()
-        im_rgb.save(out, "GIF", optimize=optimize)
+        im.save(out, "GIF", optimize=optimize)
         with Image.open(out) as reloaded:
             assert len(reloaded.palette.palette) // 3 == colors
 
@@ -1172,18 +1172,17 @@ def test_palette_save_L(tmp_path):
 
 
 def test_palette_save_P(tmp_path):
-    # Pass in a different palette, then construct what the image would look like.
-    # Forcing a non-straight grayscale palette.
-
-    im = hopper("P")
-    palette = bytes(255 - i // 3 for i in range(768))
+    im = Image.new("P", (1, 2))
+    im.putpixel((0, 1), 1)
 
     out = str(tmp_path / "temp.gif")
-    im.save(out, palette=palette)
+    im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))
 
     with Image.open(out) as reloaded:
-        im.putpalette(palette)
-        assert_image_equal(reloaded, im)
+        reloaded_rgb = reloaded.convert("RGB")
+
+        assert reloaded_rgb.getpixel((0, 0)) == (1, 2, 3)
+        assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)
 
 
 def test_palette_save_duplicate_entries(tmp_path):
diff --git a/Tests/test_image.py b/Tests/test_image.py
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -632,8 +632,8 @@ def test_remap_palette(self):
                 im.remap_palette(None)
 
     def test_remap_palette_transparency(self):
-        im = Image.new("P", (1, 2))
-        im.putpixel((0, 1), 1)
+        im = Image.new("P", (1, 2), (0, 0, 0))
+        im.putpixel((0, 1), (255, 0, 0))
         im.info["transparency"] = 0
 
         im_remapped = im.remap_palette([1, 0])
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -117,11 +117,11 @@ def test_trns_p(tmp_path):
     f = str(tmp_path / "temp.png")
 
     im_l = im.convert("L")
-    assert im_l.info["transparency"] == 1  # undone
+    assert im_l.info["transparency"] == 0
     im_l.save(f)
 
     im_rgb = im.convert("RGB")
-    assert im_rgb.info["transparency"] == (0, 1, 2)  # undone
+    assert im_rgb.info["transparency"] == (0, 0, 0)
     im_rgb.save(f)
 
 
diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py
--- a/Tests/test_image_putpalette.py
+++ b/Tests/test_image_putpalette.py
@@ -84,3 +84,14 @@ def test_rgba_palette(mode, palette):
     im.putpalette(palette, mode)
     assert im.getpalette() == [1, 2, 3]
     assert im.palette.colors == {(1, 2, 3, 4): 0}
+
+
+def test_empty_palette():
+    im = Image.new("P", (1, 1))
+    assert im.getpalette() == []
+
+
+def test_undefined_palette_index():
+    im = Image.new("P", (1, 1), 3)
+    im.putpalette((1, 2, 3))
+    assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0)