Repository: python-pillow/Pillow

Issue:
ImagePalette's palette argument expects different alignment since 8.3.0
It seems that `PIL.ImagePalette.ImagePalette`'s `palette` argument no longer expects a palette aligned by channels, but one color after the other, i.e. `RGBRGBRGB` rather than `RRRGGGBBB` as previously.

I assume this is an unintentional breaking change as it is neither specifically mentioned in the patch notes, nor was the documentation updated. I could not find any PR or bug report on this, either (unless I missed it - I am sorry if that's the case).

If this is a bug, I hope it can actually become a feature as I personally prefer the new ordering.

Example:
```python
import PIL.Image
import PIL.ImagePalette

p = [0, 0, 0,
     255, 0, 0,
     0, 255, 0]

palette = PIL.ImagePalette.ImagePalette(mode='RGB', palette=p, size=len(p))

image = PIL.Image.new('P', (100, 100), 1)  # fill image with second color in the palette
image.putpalette(palette)

image.show()  # Gives a blue image on 8.2.0 and a red image since 8.3.0

assert image.getpalette()[:len(p)] == p, 'This assert succeeds in 8.3.0'
```


Cheers!


* OS: Arch Linux
* Python: 3.9
* Pillow: 8.3.0+



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
@@ -396,15 +396,7 @@ def _normalize_palette(im, palette, info):
         if isinstance(palette, (bytes, bytearray, list)):
             source_palette = bytearray(palette[:768])
         if isinstance(palette, ImagePalette.ImagePalette):
-            source_palette = bytearray(
-                itertools.chain.from_iterable(
-                    zip(
-                        palette.palette[:256],
-                        palette.palette[256:512],
-                        palette.palette[512:768],
-                    )
-                )
-            )
+            source_palette = bytearray(palette.palette)
 
     if im.mode == "P":
         if not source_palette:
diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py
--- a/src/PIL/ImagePalette.py
+++ b/src/PIL/ImagePalette.py
@@ -25,12 +25,12 @@ class ImagePalette:
     """
     Color palette for palette mapped images
 
-    :param mode: The mode to use for the Palette. See:
+    :param mode: The mode to use for the palette. See:
         :ref:`concept-modes`. Defaults to "RGB"
     :param palette: An optional palette. If given, it must be a bytearray,
-        an array or a list of ints between 0-255. The list must be aligned
-        by channel (All R values must be contiguous in the list before G
-        and B values.) Defaults to 0 through 255 per channel.
+        an array or a list of ints between 0-255. The list must consist of
+        all channels for one color followed by the next color (e.g. RGBRGBRGB).
+        Defaults to an empty palette.
     :param size: An optional palette size. If given, an error is raised
         if ``palette`` is not of equal length.
     """
@@ -204,9 +204,9 @@ def make_gamma_lut(exp):
 
 
 def negative(mode="RGB"):
-    palette = list(range(256))
+    palette = list(range(256 * len(mode)))
     palette.reverse()
-    return ImagePalette(mode, palette * len(mode))
+    return ImagePalette(mode, [i // len(mode) for i in palette])
 
 
 def random(mode="RGB"):
@@ -219,15 +219,13 @@ def random(mode="RGB"):
 
 
 def sepia(white="#fff0c0"):
-    r, g, b = ImageColor.getrgb(white)
-    r = make_linear_lut(0, r)
-    g = make_linear_lut(0, g)
-    b = make_linear_lut(0, b)
-    return ImagePalette("RGB", r + g + b)
+    bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)]
+    return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)])
 
 
 def wedge(mode="RGB"):
-    return ImagePalette(mode, list(range(256)) * len(mode))
+    palette = list(range(256 * len(mode)))
+    return ImagePalette(mode, [i // len(mode) for i in palette])
 
 
 def load(filename):


Test patch:
diff --git a/Tests/images/palette_negative.png b/Tests/images/palette_negative.png
new file mode 100644
Binary files /dev/null and b/Tests/images/palette_negative.png differ
diff --git a/Tests/images/palette_sepia.png b/Tests/images/palette_sepia.png
new file mode 100644
Binary files /dev/null and b/Tests/images/palette_sepia.png differ
diff --git a/Tests/images/palette_wedge.png b/Tests/images/palette_wedge.png
new file mode 100644
Binary files /dev/null and b/Tests/images/palette_wedge.png differ
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
@@ -833,7 +833,7 @@ def test_palette_save_ImagePalette(tmp_path):
 
     with Image.open(out) as reloaded:
         im.putpalette(palette)
-        assert_image_equal(reloaded, im)
+        assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
 
 
 def test_save_I(tmp_path):
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
@@ -2,7 +2,7 @@
 
 from PIL import Image, ImagePalette
 
-from .helper import assert_image_equal, hopper
+from .helper import assert_image_equal, assert_image_equal_tofile, hopper
 
 
 def test_putpalette():
@@ -36,9 +36,15 @@ def palette(mode):
 def test_imagepalette():
     im = hopper("P")
     im.putpalette(ImagePalette.negative())
+    assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png")
+
     im.putpalette(ImagePalette.random())
+
     im.putpalette(ImagePalette.sepia())
+    assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_sepia.png")
+
     im.putpalette(ImagePalette.wedge())
+    assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png")
 
 
 def test_putpalette_with_alpha_values():