From d522e0a5c03f87b653ab25a23116d9f685aa0c1e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 10 Sep 2024 18:50:06 +1000 Subject: [PATCH] Improved handling of RGBA palettes when saving GIF images --- Tests/test_file_gif.py | 18 ++++++++++++++++++ src/PIL/GifImagePlugin.py | 17 ++++++++++++++--- src/PIL/Image.py | 5 ++++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 571fe1b9a..16c8466f3 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1429,3 +1429,21 @@ def test_saving_rgba(tmp_path: Path) -> None: with Image.open(out) as reloaded: reloaded_rgba = reloaded.convert("RGBA") assert reloaded_rgba.load()[0, 0][3] == 0 + + +def test_optimizing_p_rgba(tmp_path: Path) -> None: + out = str(tmp_path / "temp.gif") + + im1 = Image.new("P", (100, 100)) + d = ImageDraw.Draw(im1) + d.ellipse([(40, 40), (60, 60)], fill=1) + data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254 + im1.putpalette(data, "RGBA") + + im2 = Image.new("P", (100, 100)) + im2.putpalette(data, "RGBA") + + im1.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reloaded: + assert reloaded.n_frames == 2 diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index f206fbb9c..57c291792 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -553,7 +553,9 @@ def _normalize_palette( if im.mode == "P": if not source_palette: - source_palette = im.im.getpalette("RGB")[:768] + im_palette = im.getpalette(None) + assert im_palette is not None + source_palette = bytearray(im_palette) else: # L-mode if not source_palette: source_palette = bytearray(i // 3 for i in range(768)) @@ -629,7 +631,10 @@ def _write_single_frame( def _getbbox( base_im: Image.Image, im_frame: Image.Image ) -> tuple[Image.Image, tuple[int, int, int, int] | None]: - if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im): + palette_bytes = [ + bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame) + ] + if palette_bytes[0] != palette_bytes[1]: im_frame = im_frame.convert("RGBA") base_im = base_im.convert("RGBA") delta = ImageChops.subtract_modulo(im_frame, base_im) @@ -984,7 +989,13 @@ def _get_palette_bytes(im: Image.Image) -> bytes: :param im: Image object :returns: Bytes, len<=768 suitable for inclusion in gif header """ - return bytes(im.palette.palette) if im.palette else b"" + if not im.palette: + return b"" + + palette = bytes(im.palette.palette) + if im.palette.mode == "RGBA": + palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3)) + return palette def _get_background( diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e6d9047f5..a7a158d35 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1066,7 +1066,7 @@ class Image: trns_im = new(self.mode, (1, 1)) if self.mode == "P": assert self.palette is not None - trns_im.putpalette(self.palette) + trns_im.putpalette(self.palette, self.palette.mode) if isinstance(t, tuple): err = "Couldn't allocate a palette color for transparency" assert trns_im.palette is not None @@ -2182,6 +2182,9 @@ class Image: source_palette = self.im.getpalette(palette_mode, palette_mode) else: # L-mode source_palette = bytearray(i // 3 for i in range(768)) + elif len(source_palette) > 768: + bands = 4 + palette_mode = "RGBA" palette_bytes = b"" new_positions = [0] * 256