From 128ed189e58e2679ad5cdbc22937a3109282e340 Mon Sep 17 00:00:00 2001 From: Ray Gardner Date: Sat, 18 Jun 2022 18:07:58 -0600 Subject: [PATCH 1/4] Improve test in _get_optimize() Palette can be optimized if number of colors can be reduced by half or more. --- Tests/test_file_gif.py | 15 +++++++++++++++ src/PIL/GifImagePlugin.py | 7 ++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index c261cfb97bb..2bfaef4ee77 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -180,6 +180,21 @@ 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 because original is too big for _get_optimize() + im = im.resize((591, 443)) + imrgb = im.convert("RGB") + out = BytesIO() + imrgb.save(out, "GIF", optimize=False) + with Image.open(out) as reloaded: + assert len(reloaded.palette.palette) // 3 == 256 + out = BytesIO() + imrgb.save(out, "GIF", optimize=True) + with Image.open(out) as reloaded: + assert len(reloaded.palette.palette) // 3 == 8 + + def test_roundtrip(tmp_path): out = str(tmp_path / "temp.gif") im = hopper() diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 3469199cad3..f58146c7ed3 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -824,9 +824,14 @@ def _get_optimize(im, info): if count: used_palette_colors.append(i) + num_palette_colors = len(im.palette.palette) // 4 if im.palette.mode == 'RGBA' else len(im.palette.palette) // 3 + # Round up to power of 2 but at least 4 + num_palette_colors = max(4, 1 << (num_palette_colors - 1).bit_length()) if optimise or ( + len(used_palette_colors) <= 128 - and max(used_palette_colors) > len(used_palette_colors) + and max(used_palette_colors) >= len(used_palette_colors) + or len(used_palette_colors) <= num_palette_colors // 2 ): return used_palette_colors From f656711c80d039e60f86923b05dff4866815f2e2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 19 Jun 2022 00:20:25 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/GifImagePlugin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index f58146c7ed3..b30ed1728e3 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -824,11 +824,14 @@ def _get_optimize(im, info): if count: used_palette_colors.append(i) - num_palette_colors = len(im.palette.palette) // 4 if im.palette.mode == 'RGBA' else len(im.palette.palette) // 3 + num_palette_colors = ( + len(im.palette.palette) // 4 + if im.palette.mode == "RGBA" + else len(im.palette.palette) // 3 + ) # Round up to power of 2 but at least 4 num_palette_colors = max(4, 1 << (num_palette_colors - 1).bit_length()) if optimise or ( - len(used_palette_colors) <= 128 and max(used_palette_colors) >= len(used_palette_colors) or len(used_palette_colors) <= num_palette_colors // 2 From 709744432a2a290170d2b01e4c39fe2789f505f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 19 Jun 2022 16:47:50 +1000 Subject: [PATCH 3/4] Optimise palettes with more than 128 colors --- Tests/test_file_gif.py | 32 ++++++++++++++------------------ src/PIL/GifImagePlugin.py | 15 ++++++--------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 2bfaef4ee77..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))) @@ -182,17 +180,15 @@ def test_optimize_full_l(): def test_optimize_if_palette_can_be_reduced_by_half(): with Image.open("Tests/images/test.colors.gif") as im: - # Reduce because original is too big for _get_optimize() + # Reduce dimensions because original is too big for _get_optimize() im = im.resize((591, 443)) - imrgb = im.convert("RGB") - out = BytesIO() - imrgb.save(out, "GIF", optimize=False) - with Image.open(out) as reloaded: - assert len(reloaded.palette.palette) // 3 == 256 + im_rgb = im.convert("RGB") + + for (optimize, colors) in ((False, 256), (True, 8)): out = BytesIO() - imrgb.save(out, "GIF", optimize=True) + im_rgb.save(out, "GIF", optimize=optimize) with Image.open(out) as reloaded: - assert len(reloaded.palette.palette) // 3 == 8 + assert len(reloaded.palette.palette) // 3 == colors def test_roundtrip(tmp_path): @@ -997,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) @@ -1008,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 b30ed1728e3..dd659c95905 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -824,18 +824,15 @@ def _get_optimize(im, info): if count: used_palette_colors.append(i) - num_palette_colors = ( - len(im.palette.palette) // 4 - if im.palette.mode == "RGBA" - else len(im.palette.palette) // 3 + 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 ) # Round up to power of 2 but at least 4 num_palette_colors = max(4, 1 << (num_palette_colors - 1).bit_length()) - if optimise or ( - len(used_palette_colors) <= 128 - and max(used_palette_colors) >= len(used_palette_colors) - or len(used_palette_colors) <= num_palette_colors // 2 - ): + if len(used_palette_colors) <= num_palette_colors // 2: return used_palette_colors From 847ad8c512cf51c83740c8da621c8205fe693aa4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 26 Jun 2022 20:58:36 +1000 Subject: [PATCH 4/4] Clarify check that palette is not already at its smallest --- src/PIL/GifImagePlugin.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index dd659c95905..ce9fb5dd46c 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -830,9 +830,13 @@ def _get_optimize(im, info): num_palette_colors = len(im.palette.palette) // Image.getmodebands( im.palette.mode ) - # Round up to power of 2 but at least 4 - num_palette_colors = max(4, 1 << (num_palette_colors - 1).bit_length()) - if len(used_palette_colors) <= num_palette_colors // 2: + 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