diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index c261cfb97bb..dbbd3bf9de9 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -158,6 +158,9 @@ def check(colors, size, expected_palette_length): assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) # These do optimize the palette + check(256, 511, 256) + check(255, 511, 255) + check(129, 511, 129) check(128, 511, 128) check(64, 511, 64) check(4, 511, 4) @@ -167,11 +170,6 @@ def check(colors, size, expected_palette_length): check(64, 513, 256) check(4, 513, 256) - # Other limits that don't optimize the palette - check(129, 511, 256) - check(255, 511, 256) - check(256, 511, 256) - def test_optimize_full_l(): im = Image.frombytes("L", (16, 16), bytes(range(256))) @@ -180,6 +178,19 @@ def test_optimize_full_l(): assert im.mode == "L" +def test_optimize_if_palette_can_be_reduced_by_half(): + with Image.open("Tests/images/test.colors.gif") as im: + # Reduce dimensions because original is too big for _get_optimize() + im = im.resize((591, 443)) + im_rgb = im.convert("RGB") + + for (optimize, colors) in ((False, 256), (True, 8)): + out = BytesIO() + im_rgb.save(out, "GIF", optimize=optimize) + with Image.open(out) as reloaded: + assert len(reloaded.palette.palette) // 3 == colors + + def test_roundtrip(tmp_path): out = str(tmp_path / "temp.gif") im = hopper() @@ -982,8 +993,8 @@ def im_generator(ims): def test_transparent_optimize(tmp_path): # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses # transparency. - # Need a palette that isn't using the 0 color, and one that's > 128 items where the - # transparent color is actually the top palette entry to trigger the bug. + # Need a palette that isn't using the 0 color, + # where the transparent color is actually the top palette entry to trigger the bug. data = bytes(range(1, 254)) palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) @@ -993,10 +1004,10 @@ def test_transparent_optimize(tmp_path): im.putpalette(palette) out = str(tmp_path / "temp.gif") - im.save(out, transparency=253) - with Image.open(out) as reloaded: + im.save(out, transparency=im.getpixel((252, 0))) - assert reloaded.info["transparency"] == 253 + with Image.open(out) as reloaded: + assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) def test_rgb_transparency(tmp_path): diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 3469199cad3..ce9fb5dd46c 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -824,9 +824,18 @@ def _get_optimize(im, info): if count: used_palette_colors.append(i) - if optimise or ( - len(used_palette_colors) <= 128 - and max(used_palette_colors) > len(used_palette_colors) + if optimise or max(used_palette_colors) >= len(used_palette_colors): + return used_palette_colors + + num_palette_colors = len(im.palette.palette) // Image.getmodebands( + im.palette.mode + ) + current_palette_size = 1 << (num_palette_colors - 1).bit_length() + if ( + # check that the palette would become smaller when saved + len(used_palette_colors) <= current_palette_size // 2 + # check that the palette is not already the smallest possible size + and current_palette_size > 2 ): return used_palette_colors