Repository: python-pillow/Pillow

Issue:
Pillow no longer reads grayscale multi-frame gifs
The following breaks in pillow 9.0.1 but works fine if I downgrade to pillow 8.4.0. My guess is that the new loader that converts to RGB/RGBA when seeking doesn't account for the possibility of a grayscale GIF.

```python

from PIL import Image
import numpy as np

img = Image.open("gray_test.gif")
im1 = np.asarray(img)  # just showing that the first frame works
img.seek(2)  # break
```

Stack Trace:

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\Sebastian\AppData\Local\Programs\Python\Python38\lib\site-packages\PIL\GifImagePlugin.py", line 133, in seek
    self._seek(f)
  File "C:\Users\Sebastian\AppData\Local\Programs\Python\Python38\lib\site-packages\PIL\GifImagePlugin.py", line 151, in _seek
    self.load()
  File "C:\Users\Sebastian\AppData\Local\Programs\Python\Python38\lib\site-packages\PIL\ImageFile.py", line 264, in load
    self.load_end()
  File "C:\Users\Sebastian\AppData\Local\Programs\Python\Python38\lib\site-packages\PIL\GifImagePlugin.py", line 371, in load_end
    self.im.putpalettealpha(self._frame_transparency, 0)
ValueError: image has no palette
```

Picture for testing:

![gray_test](https://user-images.githubusercontent.com/4402489/155085936-fcbcab8d-2732-4b39-9160-8765866f832c.gif)




Discussion:
I've created PR #6086 to resolve this.

Resolving patch:
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -167,9 +167,15 @@ def _seek(self, frame):
         if self.__frame == 1:
             self.pyaccess = None
             if "transparency" in self.info:
-                self.mode = "RGBA"
-                self.im.putpalettealpha(self.info["transparency"], 0)
-                self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
+                if self.mode == "P":
+                    self.im.putpalettealpha(self.info["transparency"], 0)
+                    self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
+                    self.mode = "RGBA"
+                else:
+                    self.im = self.im.convert_transparent(
+                        "LA", self.info["transparency"]
+                    )
+                    self.mode = "LA"
 
                 del self.info["transparency"]
             else:
@@ -368,15 +374,18 @@ def load_end(self):
         if self.__frame == 0:
             return
         if self._frame_transparency is not None:
-            self.im.putpalettealpha(self._frame_transparency, 0)
-            frame_im = self.im.convert("RGBA")
+            if self.mode == "P":
+                self.im.putpalettealpha(self._frame_transparency, 0)
+                frame_im = self.im.convert("RGBA")
+            else:
+                frame_im = self.im.convert_transparent("LA", self._frame_transparency)
         else:
             frame_im = self.im.convert("RGB")
         frame_im = self._crop(frame_im, self.dispose_extent)
 
         self.im = self._prev_im
         self.mode = self.im.mode
-        if frame_im.mode == "RGBA":
+        if frame_im.mode in ("LA", "RGBA"):
             self.im.paste(frame_im, self.dispose_extent, frame_im)
         else:
             self.im.paste(frame_im, self.dispose_extent)
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -975,7 +975,9 @@ def convert_transparency(m, v):
         delete_trns = False
         # transparency handling
         if has_transparency:
-            if self.mode in ("1", "L", "I", "RGB") and mode == "RGBA":
+            if (self.mode in ("1", "L", "I") and mode in ("LA", "RGBA")) or (
+                self.mode == "RGB" and mode == "RGBA"
+            ):
                 # Use transparent conversion to promote from transparent
                 # color to an alpha channel.
                 new_im = self._new(
@@ -1564,8 +1566,8 @@ def paste(self, im, box=None, mask=None):
         also use color strings as supported by the ImageColor module.
 
         If a mask is given, this method updates only the regions
-        indicated by the mask.  You can use either "1", "L" or "RGBA"
-        images (in the latter case, the alpha band is used as mask).
+        indicated by the mask. You can use either "1", "L", "LA", "RGBA"
+        or "RGBa" images (if present, the alpha band is used as mask).
         Where the mask is 255, the given image is copied as is.  Where
         the mask is 0, the current value is preserved.  Intermediate
         values will mix the two images together, including their alpha
@@ -1613,7 +1615,7 @@ def paste(self, im, box=None, mask=None):
         elif isImageType(im):
             im.load()
             if self.mode != im.mode:
-                if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"):
+                if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"):
                     # should use an adapter for this!
                     im = im.convert(self.mode)
             im = im.im


Test patch:
diff --git a/Tests/images/no_palette_with_transparency.gif b/Tests/images/no_palette_with_transparency.gif
new file mode 100644
Binary files /dev/null and b/Tests/images/no_palette_with_transparency.gif 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
@@ -59,6 +59,17 @@ def test_invalid_file():
         GifImagePlugin.GifImageFile(invalid_file)
 
 
+def test_l_mode_transparency():
+    with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
+        assert im.mode == "L"
+        assert im.load()[0, 0] == 0
+        assert im.info["transparency"] == 255
+
+        im.seek(1)
+        assert im.mode == "LA"
+        assert im.load()[0, 0] == (0, 255)
+
+
 def test_optimize():
     def test_grayscale(optimize):
         im = Image.new("L", (1, 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
@@ -135,6 +135,10 @@ def test_trns_l(tmp_path):
 
     f = str(tmp_path / "temp.png")
 
+    im_la = im.convert("LA")
+    assert "transparency" not in im_la.info
+    im_la.save(f)
+
     im_rgb = im.convert("RGB")
     assert im_rgb.info["transparency"] == (128, 128, 128)  # undone
     im_rgb.save(f)
diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py
--- a/Tests/test_image_paste.py
+++ b/Tests/test_image_paste.py
@@ -67,6 +67,16 @@ def gradient_RGB(self):
             ],
         )
 
+    @cached_property
+    def gradient_LA(self):
+        return Image.merge(
+            "LA",
+            [
+                self.gradient_L,
+                self.gradient_L.transpose(Image.Transpose.ROTATE_90),
+            ],
+        )
+
     @cached_property
     def gradient_RGBA(self):
         return Image.merge(
@@ -145,6 +155,28 @@ def test_image_mask_L(self):
                 ],
             )
 
+    def test_image_mask_LA(self):
+        for mode in ("RGBA", "RGB", "L"):
+            im = Image.new(mode, (200, 200), "white")
+            im2 = getattr(self, "gradient_" + mode)
+
+            self.assert_9points_paste(
+                im,
+                im2,
+                self.gradient_LA,
+                [
+                    (128, 191, 255, 191),
+                    (112, 207, 206, 111),
+                    (128, 254, 128, 1),
+                    (208, 208, 239, 239),
+                    (192, 191, 191, 191),
+                    (207, 207, 112, 113),
+                    (255, 255, 255, 255),
+                    (239, 207, 207, 239),
+                    (255, 191, 128, 191),
+                ],
+            )
+
     def test_image_mask_RGBA(self):
         for mode in ("RGBA", "RGB", "L"):
             im = Image.new(mode, (200, 200), "white")