Repository: python-pillow/Pillow

Issue:
PA mode handling is wonky
Or. half baked. 

We 'support' a mode called `PA` which is palette with alpha, stored in 4 bytes per pixel. (FWIW, I think this all applies to `LA` as well)

`Image.putpalette` (https://github.com/python-pillow/Pillow/blob/master/PIL/Image.py#L1447) only allows for `L` or `P` images when adding a palette:

```
        if self.mode not in ("L", "P"):
            raise ValueError("illegal image mode")
```

`_imaging.c:_putpalette` Also only accepts\* `L` and `P`.  This code was in PIL:

```
    if (strcmp(self->image->mode, "L") != 0 && strcmp(self->image->mode, "P")) {
        PyErr_SetString(PyExc_ValueError, wrong_mode);
        return NULL;
```

\* or rather, it looks like it should but only actually accepts if `mode ==`P`and mode !='L'`

`TiffImagePlugin:_setup` (https://github.com/python-pillow/Pillow/blob/master/PIL/TiffImagePlugin.py#L1137) only includes the palette for  `P` images. 

```
        if self.mode == "P":
            palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
            self.palette = ImagePalette.raw("RGB;L", b"".join(palette))
```

All of this found while checking out #1597.



Discussion:
I've created PR #3728 to help this.

While I don't understand this comment, it does pass tests.

> _imaging.c:_putpalette Also only accepts* L and P. This code was in PIL:
> 
>     if (strcmp(self->image->mode, "L") != 0 && strcmp(self->image->mode, "P")) {
>         PyErr_SetString(PyExc_ValueError, wrong_mode);
>         return NULL;
> * or rather, it looks like it should but only actually accepts if mode ==Pand mode !='L'

Resolving patch:
diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py
--- a/src/PIL/ImImagePlugin.py
+++ b/src/PIL/ImImagePlugin.py
@@ -71,6 +71,7 @@
     "RYB3 image": ("RGB", "RYB;T"),
     # extensions
     "LA image": ("LA", "LA;L"),
+    "PA image": ("LA", "PA;L"),
     "RGBA image": ("RGBA", "RGBA;L"),
     "RGBX image": ("RGBX", "RGBX;L"),
     "CMYK image": ("CMYK", "CMYK;L"),
@@ -218,15 +219,16 @@ def _open(self):
                         linear = 0
                 else:
                     greyscale = 0
-            if self.mode == "L" or self.mode == "LA":
+            if self.mode in ["L", "LA", "P", "PA"]:
                 if greyscale:
                     if not linear:
                         self.lut = [i8(c) for c in palette[:256]]
                 else:
-                    if self.mode == "L":
+                    if self.mode in ["L", "P"]:
                         self.mode = self.rawmode = "P"
-                    elif self.mode == "LA":
-                        self.mode = self.rawmode = "PA"
+                    elif self.mode in ["LA", "PA"]:
+                        self.mode = "PA"
+                        self.rawmode = "PA;L"
                     self.palette = ImagePalette.raw("RGB;L", palette)
             elif self.mode == "RGB":
                 if not greyscale or not linear:
@@ -340,10 +342,10 @@ def _save(im, fp, filename):
         fp.write(("Name: %s\r\n" % filename).encode('ascii'))
     fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode('ascii'))
     fp.write(("File size (no of images): %d\r\n" % frames).encode('ascii'))
-    if im.mode == "P":
+    if im.mode in ["P", "PA"]:
         fp.write(b"Lut: 1\r\n")
     fp.write(b"\000" * (511-fp.tell()) + b"\032")
-    if im.mode == "P":
+    if im.mode in ["P", "PA"]:
         fp.write(im.im.getpalette("RGB", "RGB;L"))  # 768 bytes
     ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, 0, -1))])
 
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -715,7 +715,7 @@ def __setstate__(self, state):
         self.mode = mode
         self._size = size
         self.im = core.new(mode, size)
-        if mode in ("L", "P") and palette:
+        if mode in ("L", "LA", "P", "PA") and palette:
             self.putpalette(palette)
         self.frombytes(data)
 
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1358,7 +1358,7 @@ def _setup(self):
 
         # fixup palette descriptor
 
-        if self.mode == "P":
+        if self.mode in ["P", "PA"]:
             palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
             self.palette = ImagePalette.raw("RGB;L", b"".join(palette))
 
@@ -1484,7 +1484,7 @@ def _save(im, fp, filename):
 
     ifd[PHOTOMETRIC_INTERPRETATION] = photo
 
-    if im.mode == "P":
+    if im.mode in ["P", "PA"]:
         lut = im.im.getpalette("RGB", "RGB;L")
         ifd[COLORMAP] = tuple(i8(v) * 256 for v in lut)
     # data orientation


Test patch:
diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py
--- a/Tests/test_file_im.py
+++ b/Tests/test_file_im.py
@@ -48,7 +48,7 @@ def test_eoferror(self):
         im.seek(n_frames-1)
 
     def test_roundtrip(self):
-        for mode in ["RGB", "P"]:
+        for mode in ["RGB", "P", "PA"]:
             out = self.tempfile('temp.im')
             im = hopper(mode)
             im.save(out)
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -489,6 +489,16 @@ def test_tiled_planar_raw(self):
         self.assert_image_equal_tofile(im,
                                        "Tests/images/tiff_adobe_deflate.png")
 
+    def test_palette(self):
+        for mode in ["P", "PA"]:
+            outfile = self.tempfile("temp.tif")
+
+            im = hopper(mode)
+            im.save(outfile)
+
+            reloaded = Image.open(outfile)
+            self.assert_image_equal(im.convert("RGB"), reloaded.convert("RGB"))
+
     def test_tiff_save_all(self):
         import io
         import os
diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py
--- a/Tests/test_pickle.py
+++ b/Tests/test_pickle.py
@@ -75,6 +75,15 @@ def test_pickle_p_mode(self):
                                           protocol=protocol,
                                           test_file=test_file)
 
+    def test_pickle_pa_mode(self):
+        # Arrange
+        import pickle
+
+        # Act / Assert
+        for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
+            self.helper_pickle_string(pickle, protocol, mode="PA")
+            self.helper_pickle_file(pickle, protocol, mode="PA")
+
     def test_pickle_l_mode(self):
         # Arrange
         import pickle
@@ -84,6 +93,24 @@ def test_pickle_l_mode(self):
             self.helper_pickle_string(pickle, protocol, mode="L")
             self.helper_pickle_file(pickle, protocol, mode="L")
 
+    def test_pickle_la_mode_with_palette(self):
+        # Arrange
+        import pickle
+        im = Image.open('Tests/images/hopper.jpg')
+        filename = self.tempfile('temp.pkl')
+        im = im.convert("PA")
+
+        # Act / Assert
+        for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
+            im.mode = "LA"
+            with open(filename, 'wb') as f:
+                pickle.dump(im, f, protocol)
+            with open(filename, 'rb') as f:
+                loaded_im = pickle.load(f)
+
+            im.mode = "PA"
+            self.assertEqual(im, loaded_im)
+
     def test_cpickle_l_mode(self):
         # Arrange
         try: