Repository: python-pillow/Pillow

Issue:
Palettes with rawmode RGBA;15 don't work
### What did you do?

In an attempt to convert a PlayStation TIM image format to a bitmap, I turned to Pillow to handle image processing for me. This proved to be reasonably straightforward, but when operating on 8-bit palettized TIM files, I seem to be unable to preserve transparency. As per [THIS DOCUMENTATION](http://www.psxdev.net/forum/viewtopic.php?t=109) palette entries are RGBA5551 entries, but attempting to use this format with Pillow results in an exception (see the sample code below for more detail).

Since palettes with format `'RGBA;15'` (RGBA5551) are reported as supported, while both `'RGBA'` (RGBA8888) and `'RGB;15'` (RGB555) are accepted, I believe this might be a bug.

### What did you expect to happen?

I expected `image.putpalette(clut_data, 'RGBA;15')` to succeed and produce a correct image with the alpha channel used.

### What actually happened?

`image.putpalette(clut_data, 'RGBA;15')` throws `unrecognized raw mode`.

### What are your OS, Python and Pillow versions?

* OS: **Windows 10 21H2**
* Python: **3.10.0**
* Pillow: **9.0.1**, grabbed from `pip`

For a relatively small reproduction, refer to the attached `.zip` with a `.tim` file to convert and run the following example with `python code.py unpack title_gtmode.tim`. Notice `image.putpalette(clut_data, 'RGBA;15')` throws `unrecognized raw mode`, while running it with mode `RGB;15` works as expected and produces a correct image, albeit with transparency data discarded.
```python
import sys
import struct
import os
from PIL import Image

if len(sys.argv) < 3:
    exit

mode = sys.argv[1].lower()
if mode == 'unpack':
    clut_data = None
    image_data = None
    image_width = 0
    image_height = 0

    with open(sys.argv[2], 'rb') as tim:
        tag, version = struct.unpack('BB2x', tim.read(4))
        if tag == 0x10:
            if version != 0:
                sys.exit(f'Unknown TIM file version {version}!')
            
            flags = struct.unpack('B3x', tim.read(4))[0]
            bpp = flags & 3
            clp = (flags & 8) != 0

            if clp:
                # Parse CLUT
                length, x, y, width, height = struct.unpack('IHHHH', tim.read(12))
                clut_data = tim.read(length - 12)
            
            # Parse image
            length, x, y, image_width, image_height = struct.unpack('IHHHH', tim.read(12))
            image_data = tim.read(length - 12)
        
    if image_data:
        image = Image.frombytes('P', (image_width * 2, image_height), image_data, 'raw', 'P')
        image.putpalette(clut_data, 'RGBA;15') # 'unrecognized raw mode' exception here

        name = os.path.splitext(sys.argv[2])[0]
        image.save(name + '.png')
```
[title_gtmode.zip](https://github.com/python-pillow/Pillow/files/8010648/title_gtmode.zip)




Discussion:
Hi. You're calling `putpalette` with a `rawmode` argument of "RGBA;15".

As noted in https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.putpalette
> rawmode – The raw mode of the palette. Either “RGB”, “RGBA”, or a mode that can be transformed to “RGB” (e.g. “R”, “BGR;15”, “RGBA;L”).

I've created PR #6031 to add this missing transformation (in Pillow development terms, an "unpacker") from RGBA;15 to RGB.
Hey, thanks! That looks good.

However, does that mean it's impossible to preserve the transparency bit in palettes in my case? TIMs palettes have a single bit signifying if the color is transparent or not, so it should translate to alpha reasonably well when exporting, but I don't know if Pillow lets me do that.
I expect this code should let you make use of alpha.

```python
image = Image.frombytes('P', (image_width * 2, image_height), image_data, 'raw', 'P')
palette = ImagePalette.raw("RGBA;15", clut_data)
palette.mode = "RGBA"
image.palette = palette

name = os.path.splitext('title_gtmode.tim')[0]
image.save(name + '.png')
```

Do you have a palette containing transparency that you can test it on?
I tried the following example and it throws `unrecognized raw mode` inside `image.save`. An example image containing alphas was 4bpp and not 8bpp so I had to slightly update the code to add support for `P;4` - I also had to swap the nibbles around as else "right" and "left" pixels were in wrong order, and I don't think I can tell Pillow to swap them:

```python
import sys
import struct
import os
from PIL import Image, ImagePalette

if len(sys.argv) < 3:
    exit

mode = sys.argv[1].lower()
if mode == 'unpack':
    clut_data = None
    image_data = None
    image_width = 0
    image_height = 0

    with open(sys.argv[2], 'rb') as tim:
        tag, version = struct.unpack('BB2x', tim.read(4))
        if tag == 0x10:
            if version != 0:
                sys.exit(f'Unknown TIM file version {version}!')
            
            flags = struct.unpack('B3x', tim.read(4))[0]
            bpp = flags & 3
            clp = (flags & 8) != 0

            if clp:
                # Parse CLUT
                length, x, y, width, height = struct.unpack('IHHHH', tim.read(12))
                clut_data = tim.read(length - 12)
            
            # Parse image
            length, x, y, width, height = struct.unpack('IHHHH', tim.read(12))
            image_data = tim.read(length - 12)

            if bpp == 0:
                # 4bit, groups of 4 pixels
                image_width = width * 4
                rawmode = 'P;4'
                mode = 'P'

                # TODO: Is there a better way to do it in Pillow? Order of nibbles needs to be swapped
                image_data = bytes(map(lambda x: ((x & 0xF) << 4) | ((x >> 4) & 0xF), image_data))
            elif bpp == 1:
                # 8bit, groups of 2 pixels
                image_width = width * 2
                rawmode = 'P'
                mode = 'P'
            elif bpp == 2:
                # 16bit, each pixel separate
                image_width = width
                rawmode = 'RGB;15' # TODO: Alpha?
                mode = 'RGB'
            elif bpp == 3:
                # 24bit, 3-byte groups
                # TODO: Verify this
                image_width = (width * 3) / 2
                rawmode = 'RGB'
                mode = 'RGB'
            image_height = height
        
    if image_data:
        image = Image.frombytes(mode, (image_width, image_height), image_data, 'raw', rawmode)
        palette = ImagePalette.raw('RGBA;15', clut_data)
        palette.mode = 'RGBA'
        image.palette = palette

        name = os.path.splitext(sys.argv[2])[0]
        image.save(name + '.bmp')
```

[GT1_Trial_Mountain.zip](https://github.com/python-pillow/Pillow/files/8017900/GT1_Trial_Mountain.zip)

Resolving patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -821,12 +821,6 @@ def load(self):
         if self.im and self.palette and self.palette.dirty:
             # realize palette
             mode, arr = self.palette.getdata()
-            if mode == "RGBA":
-                mode = "RGB"
-                self.info["transparency"] = arr[3::4]
-                arr = bytes(
-                    value for (index, value) in enumerate(arr) if index % 4 != 3
-                )
             palette_length = self.im.putpalette(mode, arr)
             self.palette.dirty = 0
             self.palette.rawmode = None
@@ -837,8 +831,11 @@ def load(self):
                     self.im.putpalettealphas(self.info["transparency"])
                 self.palette.mode = "RGBA"
             else:
-                self.palette.mode = "RGB"
-                self.palette.palette = self.im.getpalette()[: palette_length * 3]
+                palette_mode = "RGBA" if mode.startswith("RGBA") else "RGB"
+                self.palette.mode = palette_mode
+                self.palette.palette = self.im.getpalette(palette_mode, palette_mode)[
+                    : palette_length * len(palette_mode)
+                ]
 
         if self.im:
             if cffi and USE_CFFI_ACCESS:
@@ -1761,8 +1758,8 @@ def putpalette(self, data, rawmode="RGB"):
         Alternatively, an 8-bit string may be used instead of an integer sequence.
 
         :param data: A palette sequence (either a list or a string).
-        :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a
-           mode that can be transformed to "RGB" (e.g. "R", "BGR;15", "RGBA;L").
+        :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode
+           that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L").
         """
         from . import ImagePalette
 


Test patch:
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
@@ -62,3 +62,16 @@ def test_putpalette_with_alpha_values():
         im.putpalette(palette_with_alpha_values, "RGBA")
 
         assert_image_equal(im.convert("RGBA"), expected)
+
+
+@pytest.mark.parametrize(
+    "mode, palette",
+    (
+        ("RGBA", (1, 2, 3, 4)),
+        ("RGBAX", (1, 2, 3, 4, 0)),
+    ),
+)
+def test_rgba_palette(mode, palette):
+    im = Image.new("P", (1, 1))
+    im.putpalette(palette, mode)
+    assert im.palette.colors == {(1, 2, 3, 4): 0}