Repository: python-pillow/Pillow

Issue:
Possible missing "PA" check
https://github.com/python-pillow/Pillow/blob/5a087ccbb9b48cba04276fd1b7d0cec93488b7c3/src/PIL/PyAccess.py#L59-L62

This code saves a reference to the image palette, but only for "P" mode images. I think it should probably check for "PA" here too. This saved palette is used later in this class, but again only for "P", not "PA".

https://github.com/python-pillow/Pillow/blob/5a087ccbb9b48cba04276fd1b7d0cec93488b7c3/src/PIL/PyAccess.py#L91-L97

I haven't checked if this palette reference is used outside of this class, though it is supposed to be private so hopefully not.


Discussion:

Resolving patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -1839,7 +1839,7 @@ def putpixel(self, xy, value):
         Modifies the pixel at the given position. The color is given as
         a single numerical value for single-band images, and a tuple for
         multi-band images. In addition to this, RGB and RGBA tuples are
-        accepted for P images.
+        accepted for P and PA images.
 
         Note that this method is relatively slow.  For more extensive changes,
         use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw`
@@ -1864,12 +1864,17 @@ def putpixel(self, xy, value):
             return self.pyaccess.putpixel(xy, value)
 
         if (
-            self.mode == "P"
+            self.mode in ("P", "PA")
             and isinstance(value, (list, tuple))
             and len(value) in [3, 4]
         ):
-            # RGB or RGBA value for a P image
+            # RGB or RGBA value for a P or PA image
+            if self.mode == "PA":
+                alpha = value[3] if len(value) == 4 else 255
+                value = value[:3]
             value = self.palette.getcolor(value, self)
+            if self.mode == "PA":
+                value = (value, alpha)
         return self.im.putpixel(xy, value)
 
     def remap_palette(self, dest_map, source_palette=None):
diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py
--- a/src/PIL/PyAccess.py
+++ b/src/PIL/PyAccess.py
@@ -58,7 +58,7 @@ def __init__(self, img, readonly=False):
 
         # Keep pointer to im object to prevent dereferencing.
         self._im = img.im
-        if self._im.mode == "P":
+        if self._im.mode in ("P", "PA"):
             self._palette = img.palette
 
         # Debugging is polluting test traces, only useful here
@@ -89,12 +89,17 @@ def __setitem__(self, xy, color):
         (x, y) = self.check_xy((x, y))
 
         if (
-            self._im.mode == "P"
+            self._im.mode in ("P", "PA")
             and isinstance(color, (list, tuple))
             and len(color) in [3, 4]
         ):
-            # RGB or RGBA value for a P image
+            # RGB or RGBA value for a P or PA image
+            if self._im.mode == "PA":
+                alpha = color[3] if len(color) == 4 else 255
+                color = color[:3]
             color = self._palette.getcolor(color, self._img)
+            if self._im.mode == "PA":
+                color = (color, alpha)
 
         return self.set_pixel(x, y, color)
 


Test patch:
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -215,11 +215,14 @@ def test_signedness(self, mode):
         self.check(mode, 2**15 + 1)
         self.check(mode, 2**16 - 1)
 
+    @pytest.mark.parametrize("mode", ("P", "PA"))
     @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
-    def test_p_putpixel_rgb_rgba(self, color):
-        im = Image.new("P", (1, 1), 0)
+    def test_p_putpixel_rgb_rgba(self, mode, color):
+        im = Image.new(mode, (1, 1))
         im.putpixel((0, 0), color)
-        assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0)
+
+        alpha = color[3] if len(color) == 4 and mode == "PA" else 255
+        assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
 
 
 @pytest.mark.skipif(cffi is None, reason="No CFFI")
@@ -340,12 +343,15 @@ def test_reference_counting(self):
                 # pixels can contain garbage if image is released
                 assert px[i, 0] == 0
 
-    def test_p_putpixel_rgb_rgba(self):
-        for color in [(255, 0, 0), (255, 0, 0, 255)]:
-            im = Image.new("P", (1, 1), 0)
+    @pytest.mark.parametrize("mode", ("P", "PA"))
+    def test_p_putpixel_rgb_rgba(self, mode):
+        for color in [(255, 0, 0), (255, 0, 0, 127)]:
+            im = Image.new(mode, (1, 1))
             access = PyAccess.new(im, False)
             access.putpixel((0, 0), color)
-            assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0)
+
+            alpha = color[3] if len(color) == 4 and mode == "PA" else 255
+            assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha)
 
 
 class TestImagePutPixelError(AccessTest):