{
    "file_diffs": [
        {
            "old_file_content": "import warnings\nfrom io import BytesIO\n\nimport pytest\n\nfrom PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, ImageSequence, features\n\nfrom .helper import (\n    assert_image_equal,\n    assert_image_equal_tofile,\n    assert_image_similar,\n    hopper,\n    is_pypy,\n    netpbm_available,\n)\n\n# sample gif stream\nTEST_GIF = \"Tests/images/hopper.gif\"\n\nwith open(TEST_GIF, \"rb\") as f:\n    data = f.read()\n\n\ndef test_sanity():\n    with Image.open(TEST_GIF) as im:\n        im.load()\n        assert im.mode == \"P\"\n        assert im.size == (128, 128)\n        assert im.format == \"GIF\"\n        assert im.info[\"version\"] == b\"GIF89a\"\n\n\n@pytest.mark.skipif(is_pypy(), reason=\"Requires CPython\")\ndef test_unclosed_file():\n    def open():\n        im = Image.open(TEST_GIF)\n        im.load()\n\n    pytest.warns(ResourceWarning, open)\n\n\ndef test_closed_file():\n    with warnings.catch_warnings():\n        im = Image.open(TEST_GIF)\n        im.load()\n        im.close()\n\n\ndef test_seek_after_close():\n    im = Image.open(\"Tests/images/iss634.gif\")\n    im.load()\n    im.close()\n\n    with pytest.raises(ValueError):\n        im.is_animated\n    with pytest.raises(ValueError):\n        im.n_frames\n    with pytest.raises(ValueError):\n        im.seek(1)\n\n\ndef test_context_manager():\n    with warnings.catch_warnings():\n        with Image.open(TEST_GIF) as im:\n            im.load()\n\n\ndef test_invalid_file():\n    invalid_file = \"Tests/images/flower.jpg\"\n\n    with pytest.raises(SyntaxError):\n        GifImagePlugin.GifImageFile(invalid_file)\n\n\ndef test_l_mode_transparency():\n    with Image.open(\"Tests/images/no_palette_with_transparency.gif\") as im:\n        assert im.mode == \"L\"\n        assert im.load()[0, 0] == 128\n        assert im.info[\"transparency\"] == 255\n\n        im.seek(1)\n        assert im.mode == \"L\"\n        assert im.load()[0, 0] == 128\n\n\ndef test_l_mode_after_rgb():\n    with Image.open(\"Tests/images/no_palette_after_rgb.gif\") as im:\n        im.seek(1)\n        assert im.mode == \"RGB\"\n\n        im.seek(2)\n        assert im.mode == \"RGB\"\n\n\ndef test_palette_not_needed_for_second_frame():\n    with Image.open(\"Tests/images/palette_not_needed_for_second_frame.gif\") as im:\n        im.seek(1)\n        assert_image_similar(im, hopper(\"L\").convert(\"RGB\"), 8)\n\n\ndef test_strategy():\n    with Image.open(\"Tests/images/iss634.gif\") as im:\n        expected_rgb_always = im.convert(\"RGB\")\n\n    with Image.open(\"Tests/images/chi.gif\") as im:\n        expected_rgb_always_rgba = im.convert(\"RGBA\")\n\n        im.seek(1)\n        expected_different = im.convert(\"RGB\")\n\n    try:\n        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS\n        with Image.open(\"Tests/images/iss634.gif\") as im:\n            assert im.mode == \"RGB\"\n            assert_image_equal(im, expected_rgb_always)\n\n        with Image.open(\"Tests/images/chi.gif\") as im:\n            assert im.mode == \"RGBA\"\n            assert_image_equal(im, expected_rgb_always_rgba)\n\n        GifImagePlugin.LOADING_STRATEGY = (\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY\n        )\n        # Stay in P mode with only a global palette\n        with Image.open(\"Tests/images/chi.gif\") as im:\n            assert im.mode == \"P\"\n\n            im.seek(1)\n            assert im.mode == \"P\"\n            assert_image_equal(im.convert(\"RGB\"), expected_different)\n\n        # Change to RGB mode when a frame has an individual palette\n        with Image.open(\"Tests/images/iss634.gif\") as im:\n            assert im.mode == \"P\"\n\n            im.seek(1)\n            assert im.mode == \"RGB\"\n    finally:\n        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST\n\n\ndef test_optimize():\n    def test_grayscale(optimize):\n        im = Image.new(\"L\", (1, 1), 0)\n        filename = BytesIO()\n        im.save(filename, \"GIF\", optimize=optimize)\n        return len(filename.getvalue())\n\n    def test_bilevel(optimize):\n        im = Image.new(\"1\", (1, 1), 0)\n        test_file = BytesIO()\n        im.save(test_file, \"GIF\", optimize=optimize)\n        return len(test_file.getvalue())\n\n    assert test_grayscale(0) == 799\n    assert test_grayscale(1) == 43\n    assert test_bilevel(0) == 799\n    assert test_bilevel(1) == 799\n\n\ndef test_optimize_correctness():\n    # 256 color Palette image, posterize to > 128 and < 128 levels\n    # Size bigger and smaller than 512x512\n    # Check the palette for number of colors allocated.\n    # Check for correctness after conversion back to RGB\n    def check(colors, size, expected_palette_length):\n        # make an image with empty colors in the start of the palette range\n        im = Image.frombytes(\n            \"P\", (colors, colors), bytes(range(256 - colors, 256)) * colors\n        )\n        im = im.resize((size, size))\n        outfile = BytesIO()\n        im.save(outfile, \"GIF\")\n        outfile.seek(0)\n        with Image.open(outfile) as reloaded:\n            # check palette length\n            palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v)\n            assert expected_palette_length == palette_length\n\n            assert_image_equal(im.convert(\"RGB\"), reloaded.convert(\"RGB\"))\n\n    # These do optimize the palette\n    check(256, 511, 256)\n    check(255, 511, 255)\n    check(129, 511, 129)\n    check(128, 511, 128)\n    check(64, 511, 64)\n    check(4, 511, 4)\n\n    # These don't optimize the palette\n    check(128, 513, 256)\n    check(64, 513, 256)\n    check(4, 513, 256)\n\n\ndef test_optimize_full_l():\n    im = Image.frombytes(\"L\", (16, 16), bytes(range(256)))\n    test_file = BytesIO()\n    im.save(test_file, \"GIF\", optimize=True)\n    assert im.mode == \"L\"\n\n\ndef test_optimize_if_palette_can_be_reduced_by_half():\n    with Image.open(\"Tests/images/test.colors.gif\") as im:\n        # Reduce dimensions because original is too big for _get_optimize()\n        im = im.resize((591, 443))\n    im_rgb = im.convert(\"RGB\")\n\n    for (optimize, colors) in ((False, 256), (True, 8)):\n        out = BytesIO()\n        im_rgb.save(out, \"GIF\", optimize=optimize)\n        with Image.open(out) as reloaded:\n            assert len(reloaded.palette.palette) // 3 == colors\n\n\ndef test_roundtrip(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im = hopper()\n    im.save(out)\n    with Image.open(out) as reread:\n\n        assert_image_similar(reread.convert(\"RGB\"), im, 50)\n\n\ndef test_roundtrip2(tmp_path):\n    # see https://github.com/python-pillow/Pillow/issues/403\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(TEST_GIF) as im:\n        im2 = im.copy()\n        im2.save(out)\n    with Image.open(out) as reread:\n\n        assert_image_similar(reread.convert(\"RGB\"), hopper(), 50)\n\n\ndef test_roundtrip_save_all(tmp_path):\n    # Single frame image\n    out = str(tmp_path / \"temp.gif\")\n    im = hopper()\n    im.save(out, save_all=True)\n    with Image.open(out) as reread:\n\n        assert_image_similar(reread.convert(\"RGB\"), im, 50)\n\n    # Multiframe image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\n        out = str(tmp_path / \"temp.gif\")\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 5\n\n\n@pytest.mark.parametrize(\n    \"path, mode\",\n    (\n        (\"Tests/images/dispose_bgnd.gif\", \"RGB\"),\n        # Hexeditted copy of dispose_bgnd to add transparency\n        (\"Tests/images/dispose_bgnd_rgba.gif\", \"RGBA\"),\n    ),\n)\ndef test_loading_multiple_palettes(path, mode):\n    with Image.open(path) as im:\n        assert im.mode == \"P\"\n        first_frame_colors = im.palette.colors.keys()\n        original_color = im.convert(\"RGB\").load()[0, 0]\n\n        im.seek(1)\n        assert im.mode == mode\n        if mode == \"RGBA\":\n            im = im.convert(\"RGB\")\n\n        # Check a color only from the old palette\n        assert im.load()[0, 0] == original_color\n\n        # Check a color from the new palette\n        assert im.load()[24, 24] not in first_frame_colors\n\n\ndef test_headers_saving_for_animated_gifs(tmp_path):\n    important_headers = [\"background\", \"version\", \"duration\", \"loop\"]\n    # Multiframe image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\n\n        info = im.info.copy()\n\n        out = str(tmp_path / \"temp.gif\")\n        im.save(out, save_all=True)\n    with Image.open(out) as reread:\n\n        for header in important_headers:\n            assert info[header] == reread.info[header]\n\n\ndef test_palette_handling(tmp_path):\n    # see https://github.com/python-pillow/Pillow/issues/513\n\n    with Image.open(TEST_GIF) as im:\n        im = im.convert(\"RGB\")\n\n        im = im.resize((100, 100), Image.Resampling.LANCZOS)\n        im2 = im.convert(\"P\", palette=Image.Palette.ADAPTIVE, colors=256)\n\n        f = str(tmp_path / \"temp.gif\")\n        im2.save(f, optimize=True)\n\n    with Image.open(f) as reloaded:\n\n        assert_image_similar(im, reloaded.convert(\"RGB\"), 10)\n\n\ndef test_palette_434(tmp_path):\n    # see https://github.com/python-pillow/Pillow/issues/434\n\n    def roundtrip(im, *args, **kwargs):\n        out = str(tmp_path / \"temp.gif\")\n        im.copy().save(out, *args, **kwargs)\n        reloaded = Image.open(out)\n\n        return reloaded\n\n    orig = \"Tests/images/test.colors.gif\"\n    with Image.open(orig) as im:\n\n        with roundtrip(im) as reloaded:\n            assert_image_similar(im, reloaded, 1)\n        with roundtrip(im, optimize=True) as reloaded:\n            assert_image_similar(im, reloaded, 1)\n\n        im = im.convert(\"RGB\")\n        # check automatic P conversion\n        with roundtrip(im) as reloaded:\n            reloaded = reloaded.convert(\"RGB\")\n            assert_image_equal(im, reloaded)\n\n\n@pytest.mark.skipif(not netpbm_available(), reason=\"Netpbm not available\")\ndef test_save_netpbm_bmp_mode(tmp_path):\n    with Image.open(TEST_GIF) as img:\n        img = img.convert(\"RGB\")\n\n        tempfile = str(tmp_path / \"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        with Image.open(tempfile) as reloaded:\n            assert_image_similar(img, reloaded.convert(\"RGB\"), 0)\n\n\n@pytest.mark.skipif(not netpbm_available(), reason=\"Netpbm not available\")\ndef test_save_netpbm_l_mode(tmp_path):\n    with Image.open(TEST_GIF) as img:\n        img = img.convert(\"L\")\n\n        tempfile = str(tmp_path / \"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        with Image.open(tempfile) as reloaded:\n            assert_image_similar(img, reloaded.convert(\"L\"), 0)\n\n\ndef test_seek():\n    with Image.open(\"Tests/images/dispose_none.gif\") as img:\n        frame_count = 0\n        try:\n            while True:\n                frame_count += 1\n                img.seek(img.tell() + 1)\n        except EOFError:\n            assert frame_count == 5\n\n\ndef test_seek_info():\n    with Image.open(\"Tests/images/iss634.gif\") as im:\n        info = im.info.copy()\n\n        im.seek(1)\n        im.seek(0)\n\n        assert im.info == info\n\n\ndef test_seek_rewind():\n    with Image.open(\"Tests/images/iss634.gif\") as im:\n        im.seek(2)\n        im.seek(1)\n\n        with Image.open(\"Tests/images/iss634.gif\") as expected:\n            expected.seek(1)\n            assert_image_equal(im, expected)\n\n\n@pytest.mark.parametrize(\n    \"path, n_frames\",\n    (\n        (TEST_GIF, 1),\n        (\"Tests/images/comment_after_last_frame.gif\", 2),\n        (\"Tests/images/iss634.gif\", 42),\n    ),\n)\ndef test_n_frames(path, n_frames):\n    # Test is_animated before n_frames\n    with Image.open(path) as im:\n        assert im.is_animated == (n_frames != 1)\n\n    # Test is_animated after n_frames\n    with Image.open(path) as im:\n        assert im.n_frames == n_frames\n        assert im.is_animated == (n_frames != 1)\n\n\ndef test_no_change():\n    # Test n_frames does not change the image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\n        im.seek(1)\n        expected = im.copy()\n        assert im.n_frames == 5\n        assert_image_equal(im, expected)\n\n    # Test is_animated does not change the image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\n        im.seek(3)\n        expected = im.copy()\n        assert im.is_animated\n        assert_image_equal(im, expected)\n\n    with Image.open(\"Tests/images/comment_after_only_frame.gif\") as im:\n        expected = Image.new(\"P\", (1, 1))\n        assert not im.is_animated\n        assert_image_equal(im, expected)\n\n\ndef test_eoferror():\n    with Image.open(TEST_GIF) as im:\n        n_frames = im.n_frames\n\n        # Test seeking past the last frame\n        with pytest.raises(EOFError):\n            im.seek(n_frames)\n        assert im.tell() < n_frames\n\n        # Test that seeking to the last frame does not raise an error\n        im.seek(n_frames - 1)\n\n\ndef test_first_frame_transparency():\n    with Image.open(\"Tests/images/first_frame_transparency.gif\") as im:\n        px = im.load()\n        assert px[0, 0] == im.info[\"transparency\"]\n\n\ndef test_dispose_none():\n    with Image.open(\"Tests/images/dispose_none.gif\") as img:\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                assert img.disposal_method == 1\n        except EOFError:\n            pass\n\n\ndef test_dispose_none_load_end():\n    # Test image created with:\n    #\n    # im = Image.open(\"transparent.gif\")\n    # im_rotated = im.rotate(180)\n    # im.save(\"dispose_none_load_end.gif\",\n    #         save_all=True, append_images=[im_rotated], disposal=[1,2])\n    with Image.open(\"Tests/images/dispose_none_load_end.gif\") as img:\n        img.seek(1)\n\n        assert_image_equal_tofile(img, \"Tests/images/dispose_none_load_end_second.png\")\n\n\ndef test_dispose_background():\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as img:\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                assert img.disposal_method == 2\n        except EOFError:\n            pass\n\n\ndef test_dispose_background_transparency():\n    with Image.open(\"Tests/images/dispose_bgnd_transparency.gif\") as img:\n        img.seek(2)\n        px = img.load()\n        assert px[35, 30][3] == 0\n\n\n@pytest.mark.parametrize(\n    \"loading_strategy, expected_colors\",\n    (\n        (\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,\n            (\n                (2, 1, 2),\n                ((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),\n                ((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),\n            ),\n        ),\n        (\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,\n            (\n                (2, 1, 2),\n                (0, 1, 0),\n                (2, 1, 2),\n            ),\n        ),\n    ),\n)\ndef test_transparent_dispose(loading_strategy, expected_colors):\n    GifImagePlugin.LOADING_STRATEGY = loading_strategy\n    try:\n        with Image.open(\"Tests/images/transparent_dispose.gif\") as img:\n            for frame in range(3):\n                img.seek(frame)\n                for x in range(3):\n                    color = img.getpixel((x, 0))\n                    assert color == expected_colors[frame][x]\n    finally:\n        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST\n\n\ndef test_dispose_previous():\n    with Image.open(\"Tests/images/dispose_prev.gif\") as img:\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                assert img.disposal_method == 3\n        except EOFError:\n            pass\n\n\ndef test_dispose_previous_first_frame():\n    with Image.open(\"Tests/images/dispose_prev_first_frame.gif\") as im:\n        im.seek(1)\n        assert_image_equal_tofile(\n            im, \"Tests/images/dispose_prev_first_frame_seeked.png\"\n        )\n\n\ndef test_previous_frame_loaded():\n    with Image.open(\"Tests/images/dispose_none.gif\") as img:\n        img.load()\n        img.seek(1)\n        img.load()\n        img.seek(2)\n        with Image.open(\"Tests/images/dispose_none.gif\") as img_skipped:\n            img_skipped.seek(2)\n            assert_image_equal(img_skipped, img)\n\n\ndef test_save_dispose(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im_list = [\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#111\"),\n        Image.new(\"L\", (100, 100), \"#222\"),\n    ]\n    for method in range(0, 4):\n        im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)\n        with Image.open(out) as img:\n            for _ in range(2):\n                img.seek(img.tell() + 1)\n                assert img.disposal_method == method\n\n    # Check per frame disposal\n    im_list[0].save(\n        out,\n        save_all=True,\n        append_images=im_list[1:],\n        disposal=tuple(range(len(im_list))),\n    )\n\n    with Image.open(out) as img:\n\n        for i in range(2):\n            img.seek(img.tell() + 1)\n            assert img.disposal_method == i + 1\n\n\ndef test_dispose2_palette(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # Four colors: white, grey, black, red\n    circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]\n\n    im_list = []\n    for circle in circles:\n        # Red background\n        img = Image.new(\"RGB\", (100, 100), (255, 0, 0))\n\n        # Circle in center of each frame\n        d = ImageDraw.Draw(img)\n        d.ellipse([(40, 40), (60, 60)], fill=circle)\n\n        im_list.append(img)\n\n    im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)\n\n    with Image.open(out) as img:\n        for i, circle in enumerate(circles):\n            img.seek(i)\n            rgb_img = img.convert(\"RGB\")\n\n            # Check top left pixel matches background\n            assert rgb_img.getpixel((0, 0)) == (255, 0, 0)\n\n            # Center remains red every frame\n            assert rgb_img.getpixel((50, 50)) == circle\n\n\ndef test_dispose2_diff(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # 4 frames: red/blue, red/red, blue/blue, red/blue\n    circles = [\n        ((255, 0, 0, 255), (0, 0, 255, 255)),\n        ((255, 0, 0, 255), (255, 0, 0, 255)),\n        ((0, 0, 255, 255), (0, 0, 255, 255)),\n        ((255, 0, 0, 255), (0, 0, 255, 255)),\n    ]\n\n    im_list = []\n    for i in range(len(circles)):\n        # Transparent BG\n        img = Image.new(\"RGBA\", (100, 100), (255, 255, 255, 0))\n\n        # Two circles per frame\n        d = ImageDraw.Draw(img)\n        d.ellipse([(0, 30), (40, 70)], fill=circles[i][0])\n        d.ellipse([(60, 30), (100, 70)], fill=circles[i][1])\n\n        im_list.append(img)\n\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0\n    )\n\n    with Image.open(out) as img:\n        for i, colours in enumerate(circles):\n            img.seek(i)\n            rgb_img = img.convert(\"RGBA\")\n\n            # Check left circle is correct colour\n            assert rgb_img.getpixel((20, 50)) == colours[0]\n\n            # Check right circle is correct colour\n            assert rgb_img.getpixel((80, 50)) == colours[1]\n\n            # Check BG is correct colour\n            assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0)\n\n\ndef test_dispose2_background(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im_list = []\n\n    im = Image.new(\"P\", (100, 100))\n    d = ImageDraw.Draw(im)\n    d.rectangle([(50, 0), (100, 100)], fill=\"#f00\")\n    d.rectangle([(0, 0), (50, 100)], fill=\"#0f0\")\n    im_list.append(im)\n\n    im = Image.new(\"P\", (100, 100))\n    d = ImageDraw.Draw(im)\n    d.rectangle([(0, 0), (100, 50)], fill=\"#f00\")\n    d.rectangle([(0, 50), (100, 100)], fill=\"#0f0\")\n    im_list.append(im)\n\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1\n    )\n\n    with Image.open(out) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (255, 0, 0)\n\n\ndef test_transparency_in_second_frame(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/different_transparency.gif\") as im:\n        assert im.info[\"transparency\"] == 0\n\n        # Seek to the second frame\n        im.seek(im.tell() + 1)\n        assert \"transparency\" not in im.info\n\n        assert_image_equal_tofile(im, \"Tests/images/different_transparency_merged.png\")\n\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reread:\n        reread.seek(reread.tell() + 1)\n        assert_image_equal_tofile(\n            reread, \"Tests/images/different_transparency_merged.png\"\n        )\n\n\ndef test_no_transparency_in_second_frame():\n    with Image.open(\"Tests/images/iss634.gif\") as img:\n        # Seek to the second frame\n        img.seek(img.tell() + 1)\n        assert \"transparency\" not in img.info\n\n        # All transparent pixels should be replaced with the color from the first frame\n        assert img.histogram()[255] == 0\n\n\ndef test_remapped_transparency(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"P\", (1, 2))\n    im2 = im.copy()\n\n    # Add transparency at a higher index\n    # so that it will be optimized to a lower index\n    im.putpixel((0, 1), 5)\n    im.info[\"transparency\"] = 5\n    im.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reloaded:\n        assert reloaded.info[\"transparency\"] == reloaded.getpixel((0, 1))\n\n\ndef test_duration(tmp_path):\n    duration = 1000\n\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n\n    # Check that the argument has priority over the info settings\n    im.info[\"duration\"] = 100\n    im.save(out, duration=duration)\n\n    with Image.open(out) as reread:\n        assert reread.info[\"duration\"] == duration\n\n\ndef test_multiple_duration(tmp_path):\n    duration_list = [1000, 2000, 3000]\n\n    out = str(tmp_path / \"temp.gif\")\n    im_list = [\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#111\"),\n        Image.new(\"L\", (100, 100), \"#222\"),\n    ]\n\n    # Duration as list\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], duration=duration_list\n    )\n    with Image.open(out) as reread:\n\n        for duration in duration_list:\n            assert reread.info[\"duration\"] == duration\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n    # Duration as tuple\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list)\n    )\n    with Image.open(out) as reread:\n\n        for duration in duration_list:\n            assert reread.info[\"duration\"] == duration\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n\ndef test_roundtrip_info_duration(tmp_path):\n    duration_list = [100, 500, 500]\n\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/transparent_dispose.gif\") as im:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(im)\n        ] == duration_list\n\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reloaded:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(reloaded)\n        ] == duration_list\n\n\ndef test_roundtrip_info_duration_combined(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/duplicate_frame.gif\") as im:\n        assert [frame.info[\"duration\"] for frame in ImageSequence.Iterator(im)] == [\n            1000,\n            1000,\n            1000,\n        ]\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reloaded:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(reloaded)\n        ] == [1000, 2000]\n\n\ndef test_identical_frames(tmp_path):\n    duration_list = [1000, 1500, 2000, 4000]\n\n    out = str(tmp_path / \"temp.gif\")\n    im_list = [\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#111\"),\n    ]\n\n    # Duration as list\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], duration=duration_list\n    )\n    with Image.open(out) as reread:\n\n        # Assert that the first three frames were combined\n        assert reread.n_frames == 2\n\n        # Assert that the new duration is the total of the identical frames\n        assert reread.info[\"duration\"] == 4500\n\n\n@pytest.mark.parametrize(\n    \"duration\", ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500)\n)\ndef test_identical_frames_to_single_frame(duration, tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im_list = [\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#000\"),\n    ]\n\n    im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)\n    with Image.open(out) as reread:\n        # Assert that all frames were combined\n        assert reread.n_frames == 1\n\n        # Assert that the new duration is the total of the identical frames\n        assert reread.info[\"duration\"] == 8500\n\n\ndef test_number_of_loops(tmp_path):\n    number_of_loops = 2\n\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.save(out, loop=number_of_loops)\n    with Image.open(out) as reread:\n        assert reread.info[\"loop\"] == number_of_loops\n\n    # Check that even if a subsequent GIF frame has the number of loops specified,\n    # only the value from the first frame is used\n    with Image.open(\"Tests/images/duplicate_number_of_loops.gif\") as im:\n        assert im.info[\"loop\"] == 2\n\n        im.seek(1)\n        assert im.info[\"loop\"] == 2\n\n\ndef test_background(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"background\"] = 1\n    im.save(out)\n    with Image.open(out) as reread:\n\n        assert reread.info[\"background\"] == im.info[\"background\"]\n\n    if features.check(\"webp\") and features.check(\"webp_anim\"):\n        with Image.open(\"Tests/images/hopper.webp\") as im:\n            assert isinstance(im.info[\"background\"], tuple)\n            im.save(out)\n\n\ndef test_comment(tmp_path):\n    with Image.open(TEST_GIF) as im:\n        assert im.info[\"comment\"] == b\"File written by Adobe Photoshop\\xa8 4.0\"\n\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"comment\"] = b\"Test comment text\"\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"comment\"] == im.info[\"comment\"]\n\n    im.info[\"comment\"] = \"Test comment text\"\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"comment\"] == im.info[\"comment\"].encode()\n\n        # Test that GIF89a is used for comments\n        assert reread.info[\"version\"] == b\"GIF89a\"\n\n\ndef test_comment_over_255(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    comment = b\"Test comment text\"\n    while len(comment) < 256:\n        comment += comment\n    im.info[\"comment\"] = comment\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"comment\"] == comment\n\n        # Test that GIF89a is used for comments\n        assert reread.info[\"version\"] == b\"GIF89a\"\n\n\ndef test_zero_comment_subblocks():\n    with Image.open(\"Tests/images/hopper_zero_comment_subblocks.gif\") as im:\n        assert_image_equal_tofile(im, TEST_GIF)\n\n\ndef test_read_multiple_comment_blocks():\n    with Image.open(\"Tests/images/multiple_comments.gif\") as im:\n        # Multiple comment blocks in a frame are separated not concatenated\n        assert im.info[\"comment\"] == b\"Test comment 1\\nTest comment 2\"\n\n\ndef test_empty_string_comment(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/chi.gif\") as im:\n        assert \"comment\" in im.info\n\n        # Empty string comment should suppress existing comment\n        im.save(out, save_all=True, comment=\"\")\n\n    with Image.open(out) as reread:\n        for frame in ImageSequence.Iterator(reread):\n            assert \"comment\" not in frame.info\n\n\ndef test_retain_comment_in_subsequent_frames(tmp_path):\n    # Test that a comment block at the beginning is kept\n    with Image.open(\"Tests/images/chi.gif\") as im:\n        for frame in ImageSequence.Iterator(im):\n            assert frame.info[\"comment\"] == b\"Created with GIMP\"\n\n    with Image.open(\"Tests/images/second_frame_comment.gif\") as im:\n        assert \"comment\" not in im.info\n\n        # Test that a comment in the middle is read\n        im.seek(1)\n        assert im.info[\"comment\"] == b\"Comment in the second frame\"\n\n        # Test that it is still present in a later frame\n        im.seek(2)\n        assert im.info[\"comment\"] == b\"Comment in the second frame\"\n\n        # Test that rewinding removes the comment\n        im.seek(0)\n        assert \"comment\" not in im.info\n\n    # Test that a saved image keeps the comment\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/dispose_prev.gif\") as im:\n        im.save(out, save_all=True, comment=\"Test\")\n\n    with Image.open(out) as reread:\n        for frame in ImageSequence.Iterator(reread):\n            assert frame.info[\"comment\"] == b\"Test\"\n\n\ndef test_version(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    def assert_version_after_save(im, version):\n        im.save(out)\n        with Image.open(out) as reread:\n            assert reread.info[\"version\"] == version\n\n    # Test that GIF87a is used by default\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    assert_version_after_save(im, b\"GIF87a\")\n\n    # Test setting the version to 89a\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"version\"] = b\"89a\"\n    assert_version_after_save(im, b\"GIF89a\")\n\n    # Test that adding a GIF89a feature changes the version\n    im.info[\"transparency\"] = 1\n    assert_version_after_save(im, b\"GIF89a\")\n\n    # Test that a GIF87a image is also saved in that format\n    with Image.open(\"Tests/images/test.colors.gif\") as im:\n        assert_version_after_save(im, b\"GIF87a\")\n\n        # Test that a GIF89a image is also saved in that format\n        im.info[\"version\"] = b\"GIF89a\"\n        assert_version_after_save(im, b\"GIF87a\")\n\n\ndef test_append_images(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # Test appending single frame images\n    im = Image.new(\"RGB\", (100, 100), \"#f00\")\n    ims = [Image.new(\"RGB\", (100, 100), color) for color in [\"#0f0\", \"#00f\"]]\n    im.copy().save(out, save_all=True, append_images=ims)\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 3\n\n    # Tests appending using a generator\n    def im_generator(ims):\n        yield from ims\n\n    im.save(out, save_all=True, append_images=im_generator(ims))\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 3\n\n    # Tests appending single and multiple frame images\n    with Image.open(\"Tests/images/dispose_none.gif\") as im:\n        with Image.open(\"Tests/images/dispose_prev.gif\") as im2:\n            im.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 10\n\n\ndef test_transparent_optimize(tmp_path):\n    # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses\n    # transparency.\n    # Need a palette that isn't using the 0 color,\n    # where the transparent color is actually the top palette entry to trigger the bug.\n\n    data = bytes(range(1, 254))\n    palette = ImagePalette.ImagePalette(\"RGB\", list(range(256)) * 3)\n\n    im = Image.new(\"L\", (253, 1))\n    im.frombytes(data)\n    im.putpalette(palette)\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, transparency=im.getpixel((252, 0)))\n\n    with Image.open(out) as reloaded:\n        assert reloaded.info[\"transparency\"] == reloaded.getpixel((252, 0))\n\n\ndef test_rgb_transparency(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # Single frame\n    im = Image.new(\"RGB\", (1, 1))\n    im.info[\"transparency\"] = (255, 0, 0)\n    im.save(out)\n\n    with Image.open(out) as reloaded:\n        assert \"transparency\" in reloaded.info\n\n    # Multiple frames\n    im = Image.new(\"RGB\", (1, 1))\n    im.info[\"transparency\"] = b\"\"\n    ims = [Image.new(\"RGB\", (1, 1))]\n    pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims)\n\n    with Image.open(out) as reloaded:\n        assert \"transparency\" not in reloaded.info\n\n\ndef test_rgba_transparency(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im = hopper(\"P\")\n    im.save(out, save_all=True, append_images=[Image.new(\"RGBA\", im.size)])\n\n    with Image.open(out) as reloaded:\n        reloaded.seek(1)\n        assert_image_equal(hopper(\"P\").convert(\"RGB\"), reloaded)\n\n\ndef test_bbox(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"RGB\", (100, 100), \"#fff\")\n    ims = [Image.new(\"RGB\", (100, 100), \"#000\")]\n    im.save(out, save_all=True, append_images=ims)\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 2\n\n\ndef test_palette_save_L(tmp_path):\n    # Generate an L mode image with a separate palette\n\n    im = hopper(\"P\")\n    im_l = Image.frombytes(\"L\", im.size, im.tobytes())\n    palette = bytes(im.getpalette())\n\n    out = str(tmp_path / \"temp.gif\")\n    im_l.save(out, palette=palette)\n\n    with Image.open(out) as reloaded:\n        assert_image_equal(reloaded.convert(\"RGB\"), im.convert(\"RGB\"))\n\n\ndef test_palette_save_P(tmp_path):\n    # Pass in a different palette, then construct what the image would look like.\n    # Forcing a non-straight grayscale palette.\n\n    im = hopper(\"P\")\n    palette = bytes(255 - i // 3 for i in range(768))\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, palette=palette)\n\n    with Image.open(out) as reloaded:\n        im.putpalette(palette)\n        assert_image_equal(reloaded, im)\n\n\ndef test_palette_save_duplicate_entries(tmp_path):\n    im = Image.new(\"P\", (1, 2))\n    im.putpixel((0, 1), 1)\n\n    im.putpalette((0, 0, 0, 0, 0, 0))\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1])\n\n    with Image.open(out) as reloaded:\n        assert reloaded.convert(\"RGB\").getpixel((0, 1)) == (0, 0, 0)\n\n\ndef test_palette_save_all_P(tmp_path):\n    frames = []\n    colors = ((255, 0, 0), (0, 255, 0))\n    for color in colors:\n        frame = Image.new(\"P\", (100, 100))\n        frame.putpalette(color)\n        frames.append(frame)\n\n    out = str(tmp_path / \"temp.gif\")\n    frames[0].save(\n        out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:]\n    )\n\n    with Image.open(out) as im:\n        # Assert that the frames are correct, and each frame has the same palette\n        assert_image_equal(im.convert(\"RGB\"), frames[0].convert(\"RGB\"))\n        assert im.palette.palette == im.global_palette.palette\n\n        im.seek(1)\n        assert_image_equal(im.convert(\"RGB\"), frames[1].convert(\"RGB\"))\n        assert im.palette.palette == im.global_palette.palette\n\n\ndef test_palette_save_ImagePalette(tmp_path):\n    # Pass in a different palette, as an ImagePalette.ImagePalette\n    # effectively the same as test_palette_save_P\n\n    im = hopper(\"P\")\n    palette = ImagePalette.ImagePalette(\"RGB\", list(range(256))[::-1] * 3)\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, palette=palette)\n\n    with Image.open(out) as reloaded:\n        im.putpalette(palette)\n        assert_image_equal(reloaded.convert(\"RGB\"), im.convert(\"RGB\"))\n\n\ndef test_save_I(tmp_path):\n    # Test saving something that would trigger the auto-convert to 'L'\n\n    im = hopper(\"I\")\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out)\n\n    with Image.open(out) as reloaded:\n        assert_image_equal(reloaded.convert(\"L\"), im.convert(\"L\"))\n\n\ndef test_getdata():\n    # Test getheader/getdata against legacy values.\n    # Create a 'P' image with holes in the palette.\n    im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)\n    im.putpalette(ImagePalette.ImagePalette(\"RGB\"))\n    im.info = {\"background\": 0}\n\n    passed_palette = bytes(255 - i // 3 for i in range(768))\n\n    GifImagePlugin._FORCE_OPTIMIZE = True\n    try:\n        h = GifImagePlugin.getheader(im, passed_palette)\n        d = GifImagePlugin.getdata(im)\n\n        import pickle\n\n        # Enable to get target values on pre-refactor version\n        # with open('Tests/images/gif_header_data.pkl', 'wb') as f:\n        #    pickle.dump((h, d), f, 1)\n        with open(\"Tests/images/gif_header_data.pkl\", \"rb\") as f:\n            (h_target, d_target) = pickle.load(f)\n\n        assert h == h_target\n        assert d == d_target\n    finally:\n        GifImagePlugin._FORCE_OPTIMIZE = False\n\n\ndef test_lzw_bits():\n    # see https://github.com/python-pillow/Pillow/issues/2811\n    with Image.open(\"Tests/images/issue_2811.gif\") as im:\n        assert im.tile[0][3][0] == 11  # LZW bits\n        # codec error prepatch\n        im.load()\n\n\ndef test_extents():\n    with Image.open(\"Tests/images/test_extents.gif\") as im:\n        assert im.size == (100, 100)\n\n        # Check that n_frames does not change the size\n        assert im.n_frames == 2\n        assert im.size == (100, 100)\n\n        im.seek(1)\n        assert im.size == (150, 150)\n\n\ndef test_missing_background():\n    # The Global Color Table Flag isn't set, so there is no background color index,\n    # but the disposal method is \"Restore to background color\"\n    with Image.open(\"Tests/images/missing_background.gif\") as im:\n        im.seek(1)\n        assert_image_equal_tofile(im, \"Tests/images/missing_background_first_frame.png\")\n\n\ndef test_saving_rgba(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/transparent.png\") as im:\n        im.save(out)\n\n    with Image.open(out) as reloaded:\n        reloaded_rgba = reloaded.convert(\"RGBA\")\n        assert reloaded_rgba.load()[0, 0][3] == 0\n",
            "new_file_content": "import warnings\nfrom io import BytesIO\n\nimport pytest\n\nfrom PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, ImageSequence, features\n\nfrom .helper import (\n    assert_image_equal,\n    assert_image_equal_tofile,\n    assert_image_similar,\n    hopper,\n    is_pypy,\n    netpbm_available,\n)\n\n# sample gif stream\nTEST_GIF = \"Tests/images/hopper.gif\"\n\nwith open(TEST_GIF, \"rb\") as f:\n    data = f.read()\n\n\ndef test_sanity():\n    with Image.open(TEST_GIF) as im:\n        im.load()\n        assert im.mode == \"P\"\n        assert im.size == (128, 128)\n        assert im.format == \"GIF\"\n        assert im.info[\"version\"] == b\"GIF89a\"\n\n\n@pytest.mark.skipif(is_pypy(), reason=\"Requires CPython\")\ndef test_unclosed_file():\n    def open():\n        im = Image.open(TEST_GIF)\n        im.load()\n\n    pytest.warns(ResourceWarning, open)\n\n\ndef test_closed_file():\n    with warnings.catch_warnings():\n        im = Image.open(TEST_GIF)\n        im.load()\n        im.close()\n\n\ndef test_seek_after_close():\n    im = Image.open(\"Tests/images/iss634.gif\")\n    im.load()\n    im.close()\n\n    with pytest.raises(ValueError):\n        im.is_animated\n    with pytest.raises(ValueError):\n        im.n_frames\n    with pytest.raises(ValueError):\n        im.seek(1)\n\n\ndef test_context_manager():\n    with warnings.catch_warnings():\n        with Image.open(TEST_GIF) as im:\n            im.load()\n\n\ndef test_invalid_file():\n    invalid_file = \"Tests/images/flower.jpg\"\n\n    with pytest.raises(SyntaxError):\n        GifImagePlugin.GifImageFile(invalid_file)\n\n\ndef test_l_mode_transparency():\n    with Image.open(\"Tests/images/no_palette_with_transparency.gif\") as im:\n        assert im.mode == \"L\"\n        assert im.load()[0, 0] == 128\n        assert im.info[\"transparency\"] == 255\n\n        im.seek(1)\n        assert im.mode == \"L\"\n        assert im.load()[0, 0] == 128\n\n\ndef test_l_mode_after_rgb():\n    with Image.open(\"Tests/images/no_palette_after_rgb.gif\") as im:\n        im.seek(1)\n        assert im.mode == \"RGB\"\n\n        im.seek(2)\n        assert im.mode == \"RGB\"\n\n\ndef test_palette_not_needed_for_second_frame():\n    with Image.open(\"Tests/images/palette_not_needed_for_second_frame.gif\") as im:\n        im.seek(1)\n        assert_image_similar(im, hopper(\"L\").convert(\"RGB\"), 8)\n\n\ndef test_strategy():\n    with Image.open(\"Tests/images/iss634.gif\") as im:\n        expected_rgb_always = im.convert(\"RGB\")\n\n    with Image.open(\"Tests/images/chi.gif\") as im:\n        expected_rgb_always_rgba = im.convert(\"RGBA\")\n\n        im.seek(1)\n        expected_different = im.convert(\"RGB\")\n\n    try:\n        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS\n        with Image.open(\"Tests/images/iss634.gif\") as im:\n            assert im.mode == \"RGB\"\n            assert_image_equal(im, expected_rgb_always)\n\n        with Image.open(\"Tests/images/chi.gif\") as im:\n            assert im.mode == \"RGBA\"\n            assert_image_equal(im, expected_rgb_always_rgba)\n\n        GifImagePlugin.LOADING_STRATEGY = (\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY\n        )\n        # Stay in P mode with only a global palette\n        with Image.open(\"Tests/images/chi.gif\") as im:\n            assert im.mode == \"P\"\n\n            im.seek(1)\n            assert im.mode == \"P\"\n            assert_image_equal(im.convert(\"RGB\"), expected_different)\n\n        # Change to RGB mode when a frame has an individual palette\n        with Image.open(\"Tests/images/iss634.gif\") as im:\n            assert im.mode == \"P\"\n\n            im.seek(1)\n            assert im.mode == \"RGB\"\n    finally:\n        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST\n\n\ndef test_optimize():\n    def test_grayscale(optimize):\n        im = Image.new(\"L\", (1, 1), 0)\n        filename = BytesIO()\n        im.save(filename, \"GIF\", optimize=optimize)\n        return len(filename.getvalue())\n\n    def test_bilevel(optimize):\n        im = Image.new(\"1\", (1, 1), 0)\n        test_file = BytesIO()\n        im.save(test_file, \"GIF\", optimize=optimize)\n        return len(test_file.getvalue())\n\n    assert test_grayscale(0) == 799\n    assert test_grayscale(1) == 43\n    assert test_bilevel(0) == 799\n    assert test_bilevel(1) == 799\n\n\ndef test_optimize_correctness():\n    # 256 color Palette image, posterize to > 128 and < 128 levels\n    # Size bigger and smaller than 512x512\n    # Check the palette for number of colors allocated.\n    # Check for correctness after conversion back to RGB\n    def check(colors, size, expected_palette_length):\n        # make an image with empty colors in the start of the palette range\n        im = Image.frombytes(\n            \"P\", (colors, colors), bytes(range(256 - colors, 256)) * colors\n        )\n        im = im.resize((size, size))\n        outfile = BytesIO()\n        im.save(outfile, \"GIF\")\n        outfile.seek(0)\n        with Image.open(outfile) as reloaded:\n            # check palette length\n            palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v)\n            assert expected_palette_length == palette_length\n\n            assert_image_equal(im.convert(\"RGB\"), reloaded.convert(\"RGB\"))\n\n    # These do optimize the palette\n    check(256, 511, 256)\n    check(255, 511, 255)\n    check(129, 511, 129)\n    check(128, 511, 128)\n    check(64, 511, 64)\n    check(4, 511, 4)\n\n    # These don't optimize the palette\n    check(128, 513, 256)\n    check(64, 513, 256)\n    check(4, 513, 256)\n\n\ndef test_optimize_full_l():\n    im = Image.frombytes(\"L\", (16, 16), bytes(range(256)))\n    test_file = BytesIO()\n    im.save(test_file, \"GIF\", optimize=True)\n    assert im.mode == \"L\"\n\n\ndef test_optimize_if_palette_can_be_reduced_by_half():\n    with Image.open(\"Tests/images/test.colors.gif\") as im:\n        # Reduce dimensions because original is too big for _get_optimize()\n        im = im.resize((591, 443))\n    im_rgb = im.convert(\"RGB\")\n\n    for (optimize, colors) in ((False, 256), (True, 8)):\n        out = BytesIO()\n        im_rgb.save(out, \"GIF\", optimize=optimize)\n        with Image.open(out) as reloaded:\n            assert len(reloaded.palette.palette) // 3 == colors\n\n\ndef test_roundtrip(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im = hopper()\n    im.save(out)\n    with Image.open(out) as reread:\n\n        assert_image_similar(reread.convert(\"RGB\"), im, 50)\n\n\ndef test_roundtrip2(tmp_path):\n    # see https://github.com/python-pillow/Pillow/issues/403\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(TEST_GIF) as im:\n        im2 = im.copy()\n        im2.save(out)\n    with Image.open(out) as reread:\n\n        assert_image_similar(reread.convert(\"RGB\"), hopper(), 50)\n\n\ndef test_roundtrip_save_all(tmp_path):\n    # Single frame image\n    out = str(tmp_path / \"temp.gif\")\n    im = hopper()\n    im.save(out, save_all=True)\n    with Image.open(out) as reread:\n\n        assert_image_similar(reread.convert(\"RGB\"), im, 50)\n\n    # Multiframe image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\n        out = str(tmp_path / \"temp.gif\")\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 5\n\n\n@pytest.mark.parametrize(\n    \"path, mode\",\n    (\n        (\"Tests/images/dispose_bgnd.gif\", \"RGB\"),\n        # Hexeditted copy of dispose_bgnd to add transparency\n        (\"Tests/images/dispose_bgnd_rgba.gif\", \"RGBA\"),\n    ),\n)\ndef test_loading_multiple_palettes(path, mode):\n    with Image.open(path) as im:\n        assert im.mode == \"P\"\n        first_frame_colors = im.palette.colors.keys()\n        original_color = im.convert(\"RGB\").load()[0, 0]\n\n        im.seek(1)\n        assert im.mode == mode\n        if mode == \"RGBA\":\n            im = im.convert(\"RGB\")\n\n        # Check a color only from the old palette\n        assert im.load()[0, 0] == original_color\n\n        # Check a color from the new palette\n        assert im.load()[24, 24] not in first_frame_colors\n\n\ndef test_headers_saving_for_animated_gifs(tmp_path):\n    important_headers = [\"background\", \"version\", \"duration\", \"loop\"]\n    # Multiframe image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\n\n        info = im.info.copy()\n\n        out = str(tmp_path / \"temp.gif\")\n        im.save(out, save_all=True)\n    with Image.open(out) as reread:\n\n        for header in important_headers:\n            assert info[header] == reread.info[header]\n\n\ndef test_palette_handling(tmp_path):\n    # see https://github.com/python-pillow/Pillow/issues/513\n\n    with Image.open(TEST_GIF) as im:\n        im = im.convert(\"RGB\")\n\n        im = im.resize((100, 100), Image.Resampling.LANCZOS)\n        im2 = im.convert(\"P\", palette=Image.Palette.ADAPTIVE, colors=256)\n\n        f = str(tmp_path / \"temp.gif\")\n        im2.save(f, optimize=True)\n\n    with Image.open(f) as reloaded:\n\n        assert_image_similar(im, reloaded.convert(\"RGB\"), 10)\n\n\ndef test_palette_434(tmp_path):\n    # see https://github.com/python-pillow/Pillow/issues/434\n\n    def roundtrip(im, *args, **kwargs):\n        out = str(tmp_path / \"temp.gif\")\n        im.copy().save(out, *args, **kwargs)\n        reloaded = Image.open(out)\n\n        return reloaded\n\n    orig = \"Tests/images/test.colors.gif\"\n    with Image.open(orig) as im:\n\n        with roundtrip(im) as reloaded:\n            assert_image_similar(im, reloaded, 1)\n        with roundtrip(im, optimize=True) as reloaded:\n            assert_image_similar(im, reloaded, 1)\n\n        im = im.convert(\"RGB\")\n        # check automatic P conversion\n        with roundtrip(im) as reloaded:\n            reloaded = reloaded.convert(\"RGB\")\n            assert_image_equal(im, reloaded)\n\n\n@pytest.mark.skipif(not netpbm_available(), reason=\"Netpbm not available\")\ndef test_save_netpbm_bmp_mode(tmp_path):\n    with Image.open(TEST_GIF) as img:\n        img = img.convert(\"RGB\")\n\n        tempfile = str(tmp_path / \"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        with Image.open(tempfile) as reloaded:\n            assert_image_similar(img, reloaded.convert(\"RGB\"), 0)\n\n\n@pytest.mark.skipif(not netpbm_available(), reason=\"Netpbm not available\")\ndef test_save_netpbm_l_mode(tmp_path):\n    with Image.open(TEST_GIF) as img:\n        img = img.convert(\"L\")\n\n        tempfile = str(tmp_path / \"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        with Image.open(tempfile) as reloaded:\n            assert_image_similar(img, reloaded.convert(\"L\"), 0)\n\n\ndef test_seek():\n    with Image.open(\"Tests/images/dispose_none.gif\") as img:\n        frame_count = 0\n        try:\n            while True:\n                frame_count += 1\n                img.seek(img.tell() + 1)\n        except EOFError:\n            assert frame_count == 5\n\n\ndef test_seek_info():\n    with Image.open(\"Tests/images/iss634.gif\") as im:\n        info = im.info.copy()\n\n        im.seek(1)\n        im.seek(0)\n\n        assert im.info == info\n\n\ndef test_seek_rewind():\n    with Image.open(\"Tests/images/iss634.gif\") as im:\n        im.seek(2)\n        im.seek(1)\n\n        with Image.open(\"Tests/images/iss634.gif\") as expected:\n            expected.seek(1)\n            assert_image_equal(im, expected)\n\n\n@pytest.mark.parametrize(\n    \"path, n_frames\",\n    (\n        (TEST_GIF, 1),\n        (\"Tests/images/comment_after_last_frame.gif\", 2),\n        (\"Tests/images/iss634.gif\", 42),\n    ),\n)\ndef test_n_frames(path, n_frames):\n    # Test is_animated before n_frames\n    with Image.open(path) as im:\n        assert im.is_animated == (n_frames != 1)\n\n    # Test is_animated after n_frames\n    with Image.open(path) as im:\n        assert im.n_frames == n_frames\n        assert im.is_animated == (n_frames != 1)\n\n\ndef test_no_change():\n    # Test n_frames does not change the image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\n        im.seek(1)\n        expected = im.copy()\n        assert im.n_frames == 5\n        assert_image_equal(im, expected)\n\n    # Test is_animated does not change the image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\n        im.seek(3)\n        expected = im.copy()\n        assert im.is_animated\n        assert_image_equal(im, expected)\n\n    with Image.open(\"Tests/images/comment_after_only_frame.gif\") as im:\n        expected = Image.new(\"P\", (1, 1))\n        assert not im.is_animated\n        assert_image_equal(im, expected)\n\n\ndef test_eoferror():\n    with Image.open(TEST_GIF) as im:\n        n_frames = im.n_frames\n\n        # Test seeking past the last frame\n        with pytest.raises(EOFError):\n            im.seek(n_frames)\n        assert im.tell() < n_frames\n\n        # Test that seeking to the last frame does not raise an error\n        im.seek(n_frames - 1)\n\n\ndef test_first_frame_transparency():\n    with Image.open(\"Tests/images/first_frame_transparency.gif\") as im:\n        px = im.load()\n        assert px[0, 0] == im.info[\"transparency\"]\n\n\ndef test_dispose_none():\n    with Image.open(\"Tests/images/dispose_none.gif\") as img:\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                assert img.disposal_method == 1\n        except EOFError:\n            pass\n\n\ndef test_dispose_none_load_end():\n    # Test image created with:\n    #\n    # im = Image.open(\"transparent.gif\")\n    # im_rotated = im.rotate(180)\n    # im.save(\"dispose_none_load_end.gif\",\n    #         save_all=True, append_images=[im_rotated], disposal=[1,2])\n    with Image.open(\"Tests/images/dispose_none_load_end.gif\") as img:\n        img.seek(1)\n\n        assert_image_equal_tofile(img, \"Tests/images/dispose_none_load_end_second.png\")\n\n\ndef test_dispose_background():\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as img:\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                assert img.disposal_method == 2\n        except EOFError:\n            pass\n\n\ndef test_dispose_background_transparency():\n    with Image.open(\"Tests/images/dispose_bgnd_transparency.gif\") as img:\n        img.seek(2)\n        px = img.load()\n        assert px[35, 30][3] == 0\n\n\n@pytest.mark.parametrize(\n    \"loading_strategy, expected_colors\",\n    (\n        (\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,\n            (\n                (2, 1, 2),\n                ((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),\n                ((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),\n            ),\n        ),\n        (\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,\n            (\n                (2, 1, 2),\n                (0, 1, 0),\n                (2, 1, 2),\n            ),\n        ),\n    ),\n)\ndef test_transparent_dispose(loading_strategy, expected_colors):\n    GifImagePlugin.LOADING_STRATEGY = loading_strategy\n    try:\n        with Image.open(\"Tests/images/transparent_dispose.gif\") as img:\n            for frame in range(3):\n                img.seek(frame)\n                for x in range(3):\n                    color = img.getpixel((x, 0))\n                    assert color == expected_colors[frame][x]\n    finally:\n        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST\n\n\ndef test_dispose_previous():\n    with Image.open(\"Tests/images/dispose_prev.gif\") as img:\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                assert img.disposal_method == 3\n        except EOFError:\n            pass\n\n\ndef test_dispose_previous_first_frame():\n    with Image.open(\"Tests/images/dispose_prev_first_frame.gif\") as im:\n        im.seek(1)\n        assert_image_equal_tofile(\n            im, \"Tests/images/dispose_prev_first_frame_seeked.png\"\n        )\n\n\ndef test_previous_frame_loaded():\n    with Image.open(\"Tests/images/dispose_none.gif\") as img:\n        img.load()\n        img.seek(1)\n        img.load()\n        img.seek(2)\n        with Image.open(\"Tests/images/dispose_none.gif\") as img_skipped:\n            img_skipped.seek(2)\n            assert_image_equal(img_skipped, img)\n\n\ndef test_save_dispose(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im_list = [\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#111\"),\n        Image.new(\"L\", (100, 100), \"#222\"),\n    ]\n    for method in range(0, 4):\n        im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)\n        with Image.open(out) as img:\n            for _ in range(2):\n                img.seek(img.tell() + 1)\n                assert img.disposal_method == method\n\n    # Check per frame disposal\n    im_list[0].save(\n        out,\n        save_all=True,\n        append_images=im_list[1:],\n        disposal=tuple(range(len(im_list))),\n    )\n\n    with Image.open(out) as img:\n\n        for i in range(2):\n            img.seek(img.tell() + 1)\n            assert img.disposal_method == i + 1\n\n\ndef test_dispose2_palette(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # Four colors: white, grey, black, red\n    circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]\n\n    im_list = []\n    for circle in circles:\n        # Red background\n        img = Image.new(\"RGB\", (100, 100), (255, 0, 0))\n\n        # Circle in center of each frame\n        d = ImageDraw.Draw(img)\n        d.ellipse([(40, 40), (60, 60)], fill=circle)\n\n        im_list.append(img)\n\n    im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)\n\n    with Image.open(out) as img:\n        for i, circle in enumerate(circles):\n            img.seek(i)\n            rgb_img = img.convert(\"RGB\")\n\n            # Check top left pixel matches background\n            assert rgb_img.getpixel((0, 0)) == (255, 0, 0)\n\n            # Center remains red every frame\n            assert rgb_img.getpixel((50, 50)) == circle\n\n\ndef test_dispose2_diff(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # 4 frames: red/blue, red/red, blue/blue, red/blue\n    circles = [\n        ((255, 0, 0, 255), (0, 0, 255, 255)),\n        ((255, 0, 0, 255), (255, 0, 0, 255)),\n        ((0, 0, 255, 255), (0, 0, 255, 255)),\n        ((255, 0, 0, 255), (0, 0, 255, 255)),\n    ]\n\n    im_list = []\n    for i in range(len(circles)):\n        # Transparent BG\n        img = Image.new(\"RGBA\", (100, 100), (255, 255, 255, 0))\n\n        # Two circles per frame\n        d = ImageDraw.Draw(img)\n        d.ellipse([(0, 30), (40, 70)], fill=circles[i][0])\n        d.ellipse([(60, 30), (100, 70)], fill=circles[i][1])\n\n        im_list.append(img)\n\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0\n    )\n\n    with Image.open(out) as img:\n        for i, colours in enumerate(circles):\n            img.seek(i)\n            rgb_img = img.convert(\"RGBA\")\n\n            # Check left circle is correct colour\n            assert rgb_img.getpixel((20, 50)) == colours[0]\n\n            # Check right circle is correct colour\n            assert rgb_img.getpixel((80, 50)) == colours[1]\n\n            # Check BG is correct colour\n            assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0)\n\n\ndef test_dispose2_background(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im_list = []\n\n    im = Image.new(\"P\", (100, 100))\n    d = ImageDraw.Draw(im)\n    d.rectangle([(50, 0), (100, 100)], fill=\"#f00\")\n    d.rectangle([(0, 0), (50, 100)], fill=\"#0f0\")\n    im_list.append(im)\n\n    im = Image.new(\"P\", (100, 100))\n    d = ImageDraw.Draw(im)\n    d.rectangle([(0, 0), (100, 50)], fill=\"#f00\")\n    d.rectangle([(0, 50), (100, 100)], fill=\"#0f0\")\n    im_list.append(im)\n\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1\n    )\n\n    with Image.open(out) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (255, 0, 0)\n\n\ndef test_transparency_in_second_frame(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/different_transparency.gif\") as im:\n        assert im.info[\"transparency\"] == 0\n\n        # Seek to the second frame\n        im.seek(im.tell() + 1)\n        assert \"transparency\" not in im.info\n\n        assert_image_equal_tofile(im, \"Tests/images/different_transparency_merged.png\")\n\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reread:\n        reread.seek(reread.tell() + 1)\n        assert_image_equal_tofile(\n            reread, \"Tests/images/different_transparency_merged.png\"\n        )\n\n\ndef test_no_transparency_in_second_frame():\n    with Image.open(\"Tests/images/iss634.gif\") as img:\n        # Seek to the second frame\n        img.seek(img.tell() + 1)\n        assert \"transparency\" not in img.info\n\n        # All transparent pixels should be replaced with the color from the first frame\n        assert img.histogram()[255] == 0\n\n\ndef test_remapped_transparency(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"P\", (1, 2))\n    im2 = im.copy()\n\n    # Add transparency at a higher index\n    # so that it will be optimized to a lower index\n    im.putpixel((0, 1), 5)\n    im.info[\"transparency\"] = 5\n    im.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reloaded:\n        assert reloaded.info[\"transparency\"] == reloaded.getpixel((0, 1))\n\n\ndef test_duration(tmp_path):\n    duration = 1000\n\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n\n    # Check that the argument has priority over the info settings\n    im.info[\"duration\"] = 100\n    im.save(out, duration=duration)\n\n    with Image.open(out) as reread:\n        assert reread.info[\"duration\"] == duration\n\n\ndef test_multiple_duration(tmp_path):\n    duration_list = [1000, 2000, 3000]\n\n    out = str(tmp_path / \"temp.gif\")\n    im_list = [\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#111\"),\n        Image.new(\"L\", (100, 100), \"#222\"),\n    ]\n\n    # Duration as list\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], duration=duration_list\n    )\n    with Image.open(out) as reread:\n\n        for duration in duration_list:\n            assert reread.info[\"duration\"] == duration\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n    # Duration as tuple\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list)\n    )\n    with Image.open(out) as reread:\n\n        for duration in duration_list:\n            assert reread.info[\"duration\"] == duration\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n\ndef test_roundtrip_info_duration(tmp_path):\n    duration_list = [100, 500, 500]\n\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/transparent_dispose.gif\") as im:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(im)\n        ] == duration_list\n\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reloaded:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(reloaded)\n        ] == duration_list\n\n\ndef test_roundtrip_info_duration_combined(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/duplicate_frame.gif\") as im:\n        assert [frame.info[\"duration\"] for frame in ImageSequence.Iterator(im)] == [\n            1000,\n            1000,\n            1000,\n        ]\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reloaded:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(reloaded)\n        ] == [1000, 2000]\n\n\ndef test_identical_frames(tmp_path):\n    duration_list = [1000, 1500, 2000, 4000]\n\n    out = str(tmp_path / \"temp.gif\")\n    im_list = [\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#111\"),\n    ]\n\n    # Duration as list\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], duration=duration_list\n    )\n    with Image.open(out) as reread:\n\n        # Assert that the first three frames were combined\n        assert reread.n_frames == 2\n\n        # Assert that the new duration is the total of the identical frames\n        assert reread.info[\"duration\"] == 4500\n\n\n@pytest.mark.parametrize(\n    \"duration\", ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500)\n)\ndef test_identical_frames_to_single_frame(duration, tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im_list = [\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#000\"),\n    ]\n\n    im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)\n    with Image.open(out) as reread:\n        # Assert that all frames were combined\n        assert reread.n_frames == 1\n\n        # Assert that the new duration is the total of the identical frames\n        assert reread.info[\"duration\"] == 8500\n\n\ndef test_number_of_loops(tmp_path):\n    number_of_loops = 2\n\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.save(out, loop=number_of_loops)\n    with Image.open(out) as reread:\n        assert reread.info[\"loop\"] == number_of_loops\n\n    # Check that even if a subsequent GIF frame has the number of loops specified,\n    # only the value from the first frame is used\n    with Image.open(\"Tests/images/duplicate_number_of_loops.gif\") as im:\n        assert im.info[\"loop\"] == 2\n\n        im.seek(1)\n        assert im.info[\"loop\"] == 2\n\n\ndef test_background(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"background\"] = 1\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"background\"] == im.info[\"background\"]\n\n\ndef test_webp_background(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # Test opaque WebP background\n    if features.check(\"webp\") and features.check(\"webp_anim\"):\n        with Image.open(\"Tests/images/hopper.webp\") as im:\n            assert im.info[\"background\"] == (255, 255, 255, 255)\n            im.save(out)\n\n    # Test non-opaque WebP background\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"background\"] = (0, 0, 0, 0)\n    im.save(out)\n\n\ndef test_comment(tmp_path):\n    with Image.open(TEST_GIF) as im:\n        assert im.info[\"comment\"] == b\"File written by Adobe Photoshop\\xa8 4.0\"\n\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"comment\"] = b\"Test comment text\"\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"comment\"] == im.info[\"comment\"]\n\n    im.info[\"comment\"] = \"Test comment text\"\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"comment\"] == im.info[\"comment\"].encode()\n\n        # Test that GIF89a is used for comments\n        assert reread.info[\"version\"] == b\"GIF89a\"\n\n\ndef test_comment_over_255(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    comment = b\"Test comment text\"\n    while len(comment) < 256:\n        comment += comment\n    im.info[\"comment\"] = comment\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"comment\"] == comment\n\n        # Test that GIF89a is used for comments\n        assert reread.info[\"version\"] == b\"GIF89a\"\n\n\ndef test_zero_comment_subblocks():\n    with Image.open(\"Tests/images/hopper_zero_comment_subblocks.gif\") as im:\n        assert_image_equal_tofile(im, TEST_GIF)\n\n\ndef test_read_multiple_comment_blocks():\n    with Image.open(\"Tests/images/multiple_comments.gif\") as im:\n        # Multiple comment blocks in a frame are separated not concatenated\n        assert im.info[\"comment\"] == b\"Test comment 1\\nTest comment 2\"\n\n\ndef test_empty_string_comment(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/chi.gif\") as im:\n        assert \"comment\" in im.info\n\n        # Empty string comment should suppress existing comment\n        im.save(out, save_all=True, comment=\"\")\n\n    with Image.open(out) as reread:\n        for frame in ImageSequence.Iterator(reread):\n            assert \"comment\" not in frame.info\n\n\ndef test_retain_comment_in_subsequent_frames(tmp_path):\n    # Test that a comment block at the beginning is kept\n    with Image.open(\"Tests/images/chi.gif\") as im:\n        for frame in ImageSequence.Iterator(im):\n            assert frame.info[\"comment\"] == b\"Created with GIMP\"\n\n    with Image.open(\"Tests/images/second_frame_comment.gif\") as im:\n        assert \"comment\" not in im.info\n\n        # Test that a comment in the middle is read\n        im.seek(1)\n        assert im.info[\"comment\"] == b\"Comment in the second frame\"\n\n        # Test that it is still present in a later frame\n        im.seek(2)\n        assert im.info[\"comment\"] == b\"Comment in the second frame\"\n\n        # Test that rewinding removes the comment\n        im.seek(0)\n        assert \"comment\" not in im.info\n\n    # Test that a saved image keeps the comment\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/dispose_prev.gif\") as im:\n        im.save(out, save_all=True, comment=\"Test\")\n\n    with Image.open(out) as reread:\n        for frame in ImageSequence.Iterator(reread):\n            assert frame.info[\"comment\"] == b\"Test\"\n\n\ndef test_version(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    def assert_version_after_save(im, version):\n        im.save(out)\n        with Image.open(out) as reread:\n            assert reread.info[\"version\"] == version\n\n    # Test that GIF87a is used by default\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    assert_version_after_save(im, b\"GIF87a\")\n\n    # Test setting the version to 89a\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"version\"] = b\"89a\"\n    assert_version_after_save(im, b\"GIF89a\")\n\n    # Test that adding a GIF89a feature changes the version\n    im.info[\"transparency\"] = 1\n    assert_version_after_save(im, b\"GIF89a\")\n\n    # Test that a GIF87a image is also saved in that format\n    with Image.open(\"Tests/images/test.colors.gif\") as im:\n        assert_version_after_save(im, b\"GIF87a\")\n\n        # Test that a GIF89a image is also saved in that format\n        im.info[\"version\"] = b\"GIF89a\"\n        assert_version_after_save(im, b\"GIF87a\")\n\n\ndef test_append_images(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # Test appending single frame images\n    im = Image.new(\"RGB\", (100, 100), \"#f00\")\n    ims = [Image.new(\"RGB\", (100, 100), color) for color in [\"#0f0\", \"#00f\"]]\n    im.copy().save(out, save_all=True, append_images=ims)\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 3\n\n    # Tests appending using a generator\n    def im_generator(ims):\n        yield from ims\n\n    im.save(out, save_all=True, append_images=im_generator(ims))\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 3\n\n    # Tests appending single and multiple frame images\n    with Image.open(\"Tests/images/dispose_none.gif\") as im:\n        with Image.open(\"Tests/images/dispose_prev.gif\") as im2:\n            im.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 10\n\n\ndef test_transparent_optimize(tmp_path):\n    # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses\n    # transparency.\n    # Need a palette that isn't using the 0 color,\n    # where the transparent color is actually the top palette entry to trigger the bug.\n\n    data = bytes(range(1, 254))\n    palette = ImagePalette.ImagePalette(\"RGB\", list(range(256)) * 3)\n\n    im = Image.new(\"L\", (253, 1))\n    im.frombytes(data)\n    im.putpalette(palette)\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, transparency=im.getpixel((252, 0)))\n\n    with Image.open(out) as reloaded:\n        assert reloaded.info[\"transparency\"] == reloaded.getpixel((252, 0))\n\n\ndef test_rgb_transparency(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # Single frame\n    im = Image.new(\"RGB\", (1, 1))\n    im.info[\"transparency\"] = (255, 0, 0)\n    im.save(out)\n\n    with Image.open(out) as reloaded:\n        assert \"transparency\" in reloaded.info\n\n    # Multiple frames\n    im = Image.new(\"RGB\", (1, 1))\n    im.info[\"transparency\"] = b\"\"\n    ims = [Image.new(\"RGB\", (1, 1))]\n    pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims)\n\n    with Image.open(out) as reloaded:\n        assert \"transparency\" not in reloaded.info\n\n\ndef test_rgba_transparency(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im = hopper(\"P\")\n    im.save(out, save_all=True, append_images=[Image.new(\"RGBA\", im.size)])\n\n    with Image.open(out) as reloaded:\n        reloaded.seek(1)\n        assert_image_equal(hopper(\"P\").convert(\"RGB\"), reloaded)\n\n\ndef test_bbox(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"RGB\", (100, 100), \"#fff\")\n    ims = [Image.new(\"RGB\", (100, 100), \"#000\")]\n    im.save(out, save_all=True, append_images=ims)\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 2\n\n\ndef test_palette_save_L(tmp_path):\n    # Generate an L mode image with a separate palette\n\n    im = hopper(\"P\")\n    im_l = Image.frombytes(\"L\", im.size, im.tobytes())\n    palette = bytes(im.getpalette())\n\n    out = str(tmp_path / \"temp.gif\")\n    im_l.save(out, palette=palette)\n\n    with Image.open(out) as reloaded:\n        assert_image_equal(reloaded.convert(\"RGB\"), im.convert(\"RGB\"))\n\n\ndef test_palette_save_P(tmp_path):\n    # Pass in a different palette, then construct what the image would look like.\n    # Forcing a non-straight grayscale palette.\n\n    im = hopper(\"P\")\n    palette = bytes(255 - i // 3 for i in range(768))\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, palette=palette)\n\n    with Image.open(out) as reloaded:\n        im.putpalette(palette)\n        assert_image_equal(reloaded, im)\n\n\ndef test_palette_save_duplicate_entries(tmp_path):\n    im = Image.new(\"P\", (1, 2))\n    im.putpixel((0, 1), 1)\n\n    im.putpalette((0, 0, 0, 0, 0, 0))\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1])\n\n    with Image.open(out) as reloaded:\n        assert reloaded.convert(\"RGB\").getpixel((0, 1)) == (0, 0, 0)\n\n\ndef test_palette_save_all_P(tmp_path):\n    frames = []\n    colors = ((255, 0, 0), (0, 255, 0))\n    for color in colors:\n        frame = Image.new(\"P\", (100, 100))\n        frame.putpalette(color)\n        frames.append(frame)\n\n    out = str(tmp_path / \"temp.gif\")\n    frames[0].save(\n        out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:]\n    )\n\n    with Image.open(out) as im:\n        # Assert that the frames are correct, and each frame has the same palette\n        assert_image_equal(im.convert(\"RGB\"), frames[0].convert(\"RGB\"))\n        assert im.palette.palette == im.global_palette.palette\n\n        im.seek(1)\n        assert_image_equal(im.convert(\"RGB\"), frames[1].convert(\"RGB\"))\n        assert im.palette.palette == im.global_palette.palette\n\n\ndef test_palette_save_ImagePalette(tmp_path):\n    # Pass in a different palette, as an ImagePalette.ImagePalette\n    # effectively the same as test_palette_save_P\n\n    im = hopper(\"P\")\n    palette = ImagePalette.ImagePalette(\"RGB\", list(range(256))[::-1] * 3)\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, palette=palette)\n\n    with Image.open(out) as reloaded:\n        im.putpalette(palette)\n        assert_image_equal(reloaded.convert(\"RGB\"), im.convert(\"RGB\"))\n\n\ndef test_save_I(tmp_path):\n    # Test saving something that would trigger the auto-convert to 'L'\n\n    im = hopper(\"I\")\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out)\n\n    with Image.open(out) as reloaded:\n        assert_image_equal(reloaded.convert(\"L\"), im.convert(\"L\"))\n\n\ndef test_getdata():\n    # Test getheader/getdata against legacy values.\n    # Create a 'P' image with holes in the palette.\n    im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)\n    im.putpalette(ImagePalette.ImagePalette(\"RGB\"))\n    im.info = {\"background\": 0}\n\n    passed_palette = bytes(255 - i // 3 for i in range(768))\n\n    GifImagePlugin._FORCE_OPTIMIZE = True\n    try:\n        h = GifImagePlugin.getheader(im, passed_palette)\n        d = GifImagePlugin.getdata(im)\n\n        import pickle\n\n        # Enable to get target values on pre-refactor version\n        # with open('Tests/images/gif_header_data.pkl', 'wb') as f:\n        #    pickle.dump((h, d), f, 1)\n        with open(\"Tests/images/gif_header_data.pkl\", \"rb\") as f:\n            (h_target, d_target) = pickle.load(f)\n\n        assert h == h_target\n        assert d == d_target\n    finally:\n        GifImagePlugin._FORCE_OPTIMIZE = False\n\n\ndef test_lzw_bits():\n    # see https://github.com/python-pillow/Pillow/issues/2811\n    with Image.open(\"Tests/images/issue_2811.gif\") as im:\n        assert im.tile[0][3][0] == 11  # LZW bits\n        # codec error prepatch\n        im.load()\n\n\ndef test_extents():\n    with Image.open(\"Tests/images/test_extents.gif\") as im:\n        assert im.size == (100, 100)\n\n        # Check that n_frames does not change the size\n        assert im.n_frames == 2\n        assert im.size == (100, 100)\n\n        im.seek(1)\n        assert im.size == (150, 150)\n\n\ndef test_missing_background():\n    # The Global Color Table Flag isn't set, so there is no background color index,\n    # but the disposal method is \"Restore to background color\"\n    with Image.open(\"Tests/images/missing_background.gif\") as im:\n        im.seek(1)\n        assert_image_equal_tofile(im, \"Tests/images/missing_background_first_frame.png\")\n\n\ndef test_saving_rgba(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/transparent.png\") as im:\n        im.save(out)\n\n    with Image.open(out) as reloaded:\n        reloaded_rgba = reloaded.convert(\"RGBA\")\n        assert reloaded_rgba.load()[0, 0][3] == 0\n",
            "header": {
                "file": {
                    "path": "Tests/test_file_gif.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "49820d6da",
                "new_commit_hash": "a9789a99c",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Tests/test_file_gif.py"
            },
            "plus_file": {
                "path": "b/Tests/test_file_gif.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 875,
                            "length": 14
                        },
                        "new_range": {
                            "start": 875,
                            "length": 23
                        },
                        "section": "def test_background(tmp_path):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "    im.info[\"background\"] = 1",
                                "type": "context"
                            },
                            {
                                "content": "    im.save(out)",
                                "type": "context"
                            },
                            {
                                "content": "    with Image.open(out) as reread:",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "deleted"
                            },
                            {
                                "content": "        assert reread.info[\"background\"] == im.info[\"background\"]",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "def test_webp_background(tmp_path):",
                                "type": "added"
                            },
                            {
                                "content": "    out = str(tmp_path / \"temp.gif\")",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    # Test opaque WebP background",
                                "type": "added"
                            },
                            {
                                "content": "    if features.check(\"webp\") and features.check(\"webp_anim\"):",
                                "type": "context"
                            },
                            {
                                "content": "        with Image.open(\"Tests/images/hopper.webp\") as im:",
                                "type": "context"
                            },
                            {
                                "content": "            assert isinstance(im.info[\"background\"], tuple)",
                                "type": "deleted"
                            },
                            {
                                "content": "            assert im.info[\"background\"] == (255, 255, 255, 255)",
                                "type": "added"
                            },
                            {
                                "content": "            im.save(out)",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    # Test non-opaque WebP background",
                                "type": "added"
                            },
                            {
                                "content": "    im = Image.new(\"L\", (100, 100), \"#000\")",
                                "type": "added"
                            },
                            {
                                "content": "    im.info[\"background\"] = (0, 0, 0, 0)",
                                "type": "added"
                            },
                            {
                                "content": "    im.save(out)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "def test_comment(tmp_path):",
                                "type": "context"
                            },
                            {
                                "content": "    with Image.open(TEST_GIF) as im:",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Tests/test_file_gif.py",
                            "type": "function",
                            "name": "test_background",
                            "content": "def test_background(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"background\"] = 1\n    im.save(out)\n    with Image.open(out) as reread:\n\n        assert reread.info[\"background\"] == im.info[\"background\"]\n\n    if features.check(\"webp\") and features.check(\"webp_anim\"):\n        with Image.open(\"Tests/images/hopper.webp\") as im:\n            assert isinstance(im.info[\"background\"], tuple)\n            im.save(out)",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 872,
                            "end_lineno": 884,
                            "parent": null
                        }
                    ],
                    "added_entities": [
                        {
                            "file_name": "Tests/test_file_gif.py",
                            "type": "function",
                            "name": "test_webp_background",
                            "content": "def test_webp_background(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # Test opaque WebP background\n    if features.check(\"webp\") and features.check(\"webp_anim\"):\n        with Image.open(\"Tests/images/hopper.webp\") as im:\n            assert im.info[\"background\"] == (255, 255, 255, 255)\n            im.save(out)\n\n    # Test non-opaque WebP background\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"background\"] = (0, 0, 0, 0)\n    im.save(out)",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 881,
                            "end_lineno": 893,
                            "parent": null
                        }
                    ],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "#\n# The Python Imaging Library.\n# $Id$\n#\n# GIF file handling\n#\n# History:\n# 1995-09-01 fl   Created\n# 1996-12-14 fl   Added interlace support\n# 1996-12-30 fl   Added animation support\n# 1997-01-05 fl   Added write support, fixed local colour map bug\n# 1997-02-23 fl   Make sure to load raster data in getdata()\n# 1997-07-05 fl   Support external decoder (0.4)\n# 1998-07-09 fl   Handle all modes when saving (0.5)\n# 1998-07-15 fl   Renamed offset attribute to avoid name clash\n# 2001-04-16 fl   Added rewind support (seek to frame 0) (0.6)\n# 2001-04-17 fl   Added palette optimization (0.7)\n# 2002-06-06 fl   Added transparency support for save (0.8)\n# 2004-02-24 fl   Disable interlacing for small images\n#\n# Copyright (c) 1997-2004 by Secret Labs AB\n# Copyright (c) 1995-2004 by Fredrik Lundh\n#\n# See the README file for information on usage and redistribution.\n#\n\nimport itertools\nimport math\nimport os\nimport subprocess\nfrom enum import IntEnum\n\nfrom . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence\nfrom ._binary import i16le as i16\nfrom ._binary import o8\nfrom ._binary import o16le as o16\n\n\nclass LoadingStrategy(IntEnum):\n    \"\"\".. versionadded:: 9.1.0\"\"\"\n\n    RGB_AFTER_FIRST = 0\n    RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1\n    RGB_ALWAYS = 2\n\n\n#: .. versionadded:: 9.1.0\nLOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST\n\n# --------------------------------------------------------------------\n# Identify/read GIF files\n\n\ndef _accept(prefix):\n    return prefix[:6] in [b\"GIF87a\", b\"GIF89a\"]\n\n\n##\n# Image plugin for GIF images.  This plugin supports both GIF87 and\n# GIF89 images.\n\n\nclass GifImageFile(ImageFile.ImageFile):\n\n    format = \"GIF\"\n    format_description = \"Compuserve GIF\"\n    _close_exclusive_fp_after_loading = False\n\n    global_palette = None\n\n    def data(self):\n        s = self.fp.read(1)\n        if s and s[0]:\n            return self.fp.read(s[0])\n        return None\n\n    def _is_palette_needed(self, p):\n        for i in range(0, len(p), 3):\n            if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):\n                return True\n        return False\n\n    def _open(self):\n\n        # Screen\n        s = self.fp.read(13)\n        if not _accept(s):\n            raise SyntaxError(\"not a GIF file\")\n\n        self.info[\"version\"] = s[:6]\n        self._size = i16(s, 6), i16(s, 8)\n        self.tile = []\n        flags = s[10]\n        bits = (flags & 7) + 1\n\n        if flags & 128:\n            # get global palette\n            self.info[\"background\"] = s[11]\n            # check if palette contains colour indices\n            p = self.fp.read(3 << bits)\n            if self._is_palette_needed(p):\n                p = ImagePalette.raw(\"RGB\", p)\n                self.global_palette = self.palette = p\n\n        self._fp = self.fp  # FIXME: hack\n        self.__rewind = self.fp.tell()\n        self._n_frames = None\n        self._is_animated = None\n        self._seek(0)  # get ready to read first frame\n\n    @property\n    def n_frames(self):\n        if self._n_frames is None:\n            current = self.tell()\n            try:\n                while True:\n                    self._seek(self.tell() + 1, False)\n            except EOFError:\n                self._n_frames = self.tell() + 1\n            self.seek(current)\n        return self._n_frames\n\n    @property\n    def is_animated(self):\n        if self._is_animated is None:\n            if self._n_frames is not None:\n                self._is_animated = self._n_frames != 1\n            else:\n                current = self.tell()\n                if current:\n                    self._is_animated = True\n                else:\n                    try:\n                        self._seek(1, False)\n                        self._is_animated = True\n                    except EOFError:\n                        self._is_animated = False\n\n                    self.seek(current)\n        return self._is_animated\n\n    def seek(self, frame):\n        if not self._seek_check(frame):\n            return\n        if frame < self.__frame:\n            self.im = None\n            self._seek(0)\n\n        last_frame = self.__frame\n        for f in range(self.__frame + 1, frame + 1):\n            try:\n                self._seek(f)\n            except EOFError as e:\n                self.seek(last_frame)\n                raise EOFError(\"no more images in GIF file\") from e\n\n    def _seek(self, frame, update_image=True):\n\n        if frame == 0:\n            # rewind\n            self.__offset = 0\n            self.dispose = None\n            self.__frame = -1\n            self._fp.seek(self.__rewind)\n            self.disposal_method = 0\n            if \"comment\" in self.info:\n                del self.info[\"comment\"]\n        else:\n            # ensure that the previous frame was loaded\n            if self.tile and update_image:\n                self.load()\n\n        if frame != self.__frame + 1:\n            raise ValueError(f\"cannot seek to frame {frame}\")\n\n        self.fp = self._fp\n        if self.__offset:\n            # backup to last frame\n            self.fp.seek(self.__offset)\n            while self.data():\n                pass\n            self.__offset = 0\n\n        s = self.fp.read(1)\n        if not s or s == b\";\":\n            raise EOFError\n\n        palette = None\n\n        info = {}\n        frame_transparency = None\n        interlace = None\n        frame_dispose_extent = None\n        while True:\n\n            if not s:\n                s = self.fp.read(1)\n            if not s or s == b\";\":\n                break\n\n            elif s == b\"!\":\n                #\n                # extensions\n                #\n                s = self.fp.read(1)\n                block = self.data()\n                if s[0] == 249:\n                    #\n                    # graphic control extension\n                    #\n                    flags = block[0]\n                    if flags & 1:\n                        frame_transparency = block[3]\n                    info[\"duration\"] = i16(block, 1) * 10\n\n                    # disposal method - find the value of bits 4 - 6\n                    dispose_bits = 0b00011100 & flags\n                    dispose_bits = dispose_bits >> 2\n                    if dispose_bits:\n                        # only set the dispose if it is not\n                        # unspecified. I'm not sure if this is\n                        # correct, but it seems to prevent the last\n                        # frame from looking odd for some animations\n                        self.disposal_method = dispose_bits\n                elif s[0] == 254:\n                    #\n                    # comment extension\n                    #\n                    comment = b\"\"\n\n                    # Read this comment block\n                    while block:\n                        comment += block\n                        block = self.data()\n\n                    if \"comment\" in info:\n                        # If multiple comment blocks in frame, separate with \\n\n                        info[\"comment\"] += b\"\\n\" + comment\n                    else:\n                        info[\"comment\"] = comment\n                    s = None\n                    continue\n                elif s[0] == 255 and frame == 0:\n                    #\n                    # application extension\n                    #\n                    info[\"extension\"] = block, self.fp.tell()\n                    if block[:11] == b\"NETSCAPE2.0\":\n                        block = self.data()\n                        if len(block) >= 3 and block[0] == 1:\n                            self.info[\"loop\"] = i16(block, 1)\n                while self.data():\n                    pass\n\n            elif s == b\",\":\n                #\n                # local image\n                #\n                s = self.fp.read(9)\n\n                # extent\n                x0, y0 = i16(s, 0), i16(s, 2)\n                x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)\n                if (x1 > self.size[0] or y1 > self.size[1]) and update_image:\n                    self._size = max(x1, self.size[0]), max(y1, self.size[1])\n                    Image._decompression_bomb_check(self._size)\n                frame_dispose_extent = x0, y0, x1, y1\n                flags = s[8]\n\n                interlace = (flags & 64) != 0\n\n                if flags & 128:\n                    bits = (flags & 7) + 1\n                    p = self.fp.read(3 << bits)\n                    if self._is_palette_needed(p):\n                        palette = ImagePalette.raw(\"RGB\", p)\n                    else:\n                        palette = False\n\n                # image data\n                bits = self.fp.read(1)[0]\n                self.__offset = self.fp.tell()\n                break\n\n            else:\n                pass\n                # raise OSError, \"illegal GIF tag `%x`\" % s[0]\n            s = None\n\n        if interlace is None:\n            # self._fp = None\n            raise EOFError\n\n        self.__frame = frame\n        if not update_image:\n            return\n\n        self.tile = []\n\n        if self.dispose:\n            self.im.paste(self.dispose, self.dispose_extent)\n\n        self._frame_palette = palette if palette is not None else self.global_palette\n        self._frame_transparency = frame_transparency\n        if frame == 0:\n            if self._frame_palette:\n                if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:\n                    self.mode = \"RGBA\" if frame_transparency is not None else \"RGB\"\n                else:\n                    self.mode = \"P\"\n            else:\n                self.mode = \"L\"\n\n            if not palette and self.global_palette:\n                from copy import copy\n\n                palette = copy(self.global_palette)\n            self.palette = palette\n        else:\n            if self.mode == \"P\":\n                if (\n                    LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY\n                    or palette\n                ):\n                    self.pyaccess = None\n                    if \"transparency\" in self.info:\n                        self.im.putpalettealpha(self.info[\"transparency\"], 0)\n                        self.im = self.im.convert(\"RGBA\", Image.Dither.FLOYDSTEINBERG)\n                        self.mode = \"RGBA\"\n                        del self.info[\"transparency\"]\n                    else:\n                        self.mode = \"RGB\"\n                        self.im = self.im.convert(\"RGB\", Image.Dither.FLOYDSTEINBERG)\n\n        def _rgb(color):\n            if self._frame_palette:\n                color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])\n            else:\n                color = (color, color, color)\n            return color\n\n        self.dispose_extent = frame_dispose_extent\n        try:\n            if self.disposal_method < 2:\n                # do not dispose or none specified\n                self.dispose = None\n            elif self.disposal_method == 2:\n                # replace with background colour\n\n                # only dispose the extent in this frame\n                x0, y0, x1, y1 = self.dispose_extent\n                dispose_size = (x1 - x0, y1 - y0)\n\n                Image._decompression_bomb_check(dispose_size)\n\n                # by convention, attempt to use transparency first\n                dispose_mode = \"P\"\n                color = self.info.get(\"transparency\", frame_transparency)\n                if color is not None:\n                    if self.mode in (\"RGB\", \"RGBA\"):\n                        dispose_mode = \"RGBA\"\n                        color = _rgb(color) + (0,)\n                else:\n                    color = self.info.get(\"background\", 0)\n                    if self.mode in (\"RGB\", \"RGBA\"):\n                        dispose_mode = \"RGB\"\n                        color = _rgb(color)\n                self.dispose = Image.core.fill(dispose_mode, dispose_size, color)\n            else:\n                # replace with previous contents\n                if self.im is not None:\n                    # only dispose the extent in this frame\n                    self.dispose = self._crop(self.im, self.dispose_extent)\n                elif frame_transparency is not None:\n                    x0, y0, x1, y1 = self.dispose_extent\n                    dispose_size = (x1 - x0, y1 - y0)\n\n                    Image._decompression_bomb_check(dispose_size)\n                    dispose_mode = \"P\"\n                    color = frame_transparency\n                    if self.mode in (\"RGB\", \"RGBA\"):\n                        dispose_mode = \"RGBA\"\n                        color = _rgb(frame_transparency) + (0,)\n                    self.dispose = Image.core.fill(dispose_mode, dispose_size, color)\n        except AttributeError:\n            pass\n\n        if interlace is not None:\n            transparency = -1\n            if frame_transparency is not None:\n                if frame == 0:\n                    if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS:\n                        self.info[\"transparency\"] = frame_transparency\n                elif self.mode not in (\"RGB\", \"RGBA\"):\n                    transparency = frame_transparency\n            self.tile = [\n                (\n                    \"gif\",\n                    (x0, y0, x1, y1),\n                    self.__offset,\n                    (bits, interlace, transparency),\n                )\n            ]\n\n        if info.get(\"comment\"):\n            self.info[\"comment\"] = info[\"comment\"]\n        for k in [\"duration\", \"extension\"]:\n            if k in info:\n                self.info[k] = info[k]\n            elif k in self.info:\n                del self.info[k]\n\n    def load_prepare(self):\n        temp_mode = \"P\" if self._frame_palette else \"L\"\n        self._prev_im = None\n        if self.__frame == 0:\n            if self._frame_transparency is not None:\n                self.im = Image.core.fill(\n                    temp_mode, self.size, self._frame_transparency\n                )\n        elif self.mode in (\"RGB\", \"RGBA\"):\n            self._prev_im = self.im\n            if self._frame_palette:\n                self.im = Image.core.fill(\"P\", self.size, self._frame_transparency or 0)\n                self.im.putpalette(*self._frame_palette.getdata())\n            else:\n                self.im = None\n        self.mode = temp_mode\n        self._frame_palette = None\n\n        super().load_prepare()\n\n    def load_end(self):\n        if self.__frame == 0:\n            if self.mode == \"P\" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:\n                if self._frame_transparency is not None:\n                    self.im.putpalettealpha(self._frame_transparency, 0)\n                    self.mode = \"RGBA\"\n                else:\n                    self.mode = \"RGB\"\n                self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)\n            return\n        if not self._prev_im:\n            return\n        if self._frame_transparency is not None:\n            self.im.putpalettealpha(self._frame_transparency, 0)\n            frame_im = self.im.convert(\"RGBA\")\n        else:\n            frame_im = self.im.convert(\"RGB\")\n        frame_im = self._crop(frame_im, self.dispose_extent)\n\n        self.im = self._prev_im\n        self.mode = self.im.mode\n        if frame_im.mode == \"RGBA\":\n            self.im.paste(frame_im, self.dispose_extent, frame_im)\n        else:\n            self.im.paste(frame_im, self.dispose_extent)\n\n    def tell(self):\n        return self.__frame\n\n\n# --------------------------------------------------------------------\n# Write GIF files\n\n\nRAWMODE = {\"1\": \"L\", \"L\": \"L\", \"P\": \"P\"}\n\n\ndef _normalize_mode(im):\n    \"\"\"\n    Takes an image (or frame), returns an image in a mode that is appropriate\n    for saving in a Gif.\n\n    It may return the original image, or it may return an image converted to\n    palette or 'L' mode.\n\n    :param im: Image object\n    :returns: Image object\n    \"\"\"\n    if im.mode in RAWMODE:\n        im.load()\n        return im\n    if Image.getmodebase(im.mode) == \"RGB\":\n        im = im.convert(\"P\", palette=Image.Palette.ADAPTIVE)\n        if im.palette.mode == \"RGBA\":\n            for rgba in im.palette.colors.keys():\n                if rgba[3] == 0:\n                    im.info[\"transparency\"] = im.palette.colors[rgba]\n                    break\n        return im\n    return im.convert(\"L\")\n\n\ndef _normalize_palette(im, palette, info):\n    \"\"\"\n    Normalizes the palette for image.\n      - Sets the palette to the incoming palette, if provided.\n      - Ensures that there's a palette for L mode images\n      - Optimizes the palette if necessary/desired.\n\n    :param im: Image object\n    :param palette: bytes object containing the source palette, or ....\n    :param info: encoderinfo\n    :returns: Image object\n    \"\"\"\n    source_palette = None\n    if palette:\n        # a bytes palette\n        if isinstance(palette, (bytes, bytearray, list)):\n            source_palette = bytearray(palette[:768])\n        if isinstance(palette, ImagePalette.ImagePalette):\n            source_palette = bytearray(palette.palette)\n\n    if im.mode == \"P\":\n        if not source_palette:\n            source_palette = im.im.getpalette(\"RGB\")[:768]\n    else:  # L-mode\n        if not source_palette:\n            source_palette = bytearray(i // 3 for i in range(768))\n        im.palette = ImagePalette.ImagePalette(\"RGB\", palette=source_palette)\n\n    if palette:\n        used_palette_colors = []\n        for i in range(0, len(source_palette), 3):\n            source_color = tuple(source_palette[i : i + 3])\n            index = im.palette.colors.get(source_color)\n            if index in used_palette_colors:\n                index = None\n            used_palette_colors.append(index)\n        for i, index in enumerate(used_palette_colors):\n            if index is None:\n                for j in range(len(used_palette_colors)):\n                    if j not in used_palette_colors:\n                        used_palette_colors[i] = j\n                        break\n        im = im.remap_palette(used_palette_colors)\n    else:\n        used_palette_colors = _get_optimize(im, info)\n        if used_palette_colors is not None:\n            return im.remap_palette(used_palette_colors, source_palette)\n\n    im.palette.palette = source_palette\n    return im\n\n\ndef _write_single_frame(im, fp, palette):\n    im_out = _normalize_mode(im)\n    for k, v in im_out.info.items():\n        im.encoderinfo.setdefault(k, v)\n    im_out = _normalize_palette(im_out, palette, im.encoderinfo)\n\n    for s in _get_global_header(im_out, im.encoderinfo):\n        fp.write(s)\n\n    # local image header\n    flags = 0\n    if get_interlace(im):\n        flags = flags | 64\n    _write_local_header(fp, im, (0, 0), flags)\n\n    im_out.encoderconfig = (8, get_interlace(im))\n    ImageFile._save(im_out, fp, [(\"gif\", (0, 0) + im.size, 0, RAWMODE[im_out.mode])])\n\n    fp.write(b\"\\0\")  # end of image data\n\n\ndef _write_multiple_frames(im, fp, palette):\n\n    duration = im.encoderinfo.get(\"duration\")\n    disposal = im.encoderinfo.get(\"disposal\", im.info.get(\"disposal\"))\n\n    im_frames = []\n    frame_count = 0\n    background_im = None\n    for imSequence in itertools.chain([im], im.encoderinfo.get(\"append_images\", [])):\n        for im_frame in ImageSequence.Iterator(imSequence):\n            # a copy is required here since seek can still mutate the image\n            im_frame = _normalize_mode(im_frame.copy())\n            if frame_count == 0:\n                for k, v in im_frame.info.items():\n                    if k == \"transparency\":\n                        continue\n                    im.encoderinfo.setdefault(k, v)\n\n            encoderinfo = im.encoderinfo.copy()\n            im_frame = _normalize_palette(im_frame, palette, encoderinfo)\n            if \"transparency\" in im_frame.info:\n                encoderinfo.setdefault(\"transparency\", im_frame.info[\"transparency\"])\n            if isinstance(duration, (list, tuple)):\n                encoderinfo[\"duration\"] = duration[frame_count]\n            elif duration is None and \"duration\" in im_frame.info:\n                encoderinfo[\"duration\"] = im_frame.info[\"duration\"]\n            if isinstance(disposal, (list, tuple)):\n                encoderinfo[\"disposal\"] = disposal[frame_count]\n            frame_count += 1\n\n            if im_frames:\n                # delta frame\n                previous = im_frames[-1]\n                if encoderinfo.get(\"disposal\") == 2:\n                    if background_im is None:\n                        color = im.encoderinfo.get(\n                            \"transparency\", im.info.get(\"transparency\", (0, 0, 0))\n                        )\n                        background = _get_background(im_frame, color)\n                        background_im = Image.new(\"P\", im_frame.size, background)\n                        background_im.putpalette(im_frames[0][\"im\"].palette)\n                    base_im = background_im\n                else:\n                    base_im = previous[\"im\"]\n                if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):\n                    delta = ImageChops.subtract_modulo(im_frame, base_im)\n                else:\n                    delta = ImageChops.subtract_modulo(\n                        im_frame.convert(\"RGB\"), base_im.convert(\"RGB\")\n                    )\n                bbox = delta.getbbox()\n                if not bbox:\n                    # This frame is identical to the previous frame\n                    if encoderinfo.get(\"duration\"):\n                        previous[\"encoderinfo\"][\"duration\"] += encoderinfo[\"duration\"]\n                    continue\n            else:\n                bbox = None\n            im_frames.append({\"im\": im_frame, \"bbox\": bbox, \"encoderinfo\": encoderinfo})\n\n    if len(im_frames) > 1:\n        for frame_data in im_frames:\n            im_frame = frame_data[\"im\"]\n            if not frame_data[\"bbox\"]:\n                # global header\n                for s in _get_global_header(im_frame, frame_data[\"encoderinfo\"]):\n                    fp.write(s)\n                offset = (0, 0)\n            else:\n                # compress difference\n                if not palette:\n                    frame_data[\"encoderinfo\"][\"include_color_table\"] = True\n\n                im_frame = im_frame.crop(frame_data[\"bbox\"])\n                offset = frame_data[\"bbox\"][:2]\n            _write_frame_data(fp, im_frame, offset, frame_data[\"encoderinfo\"])\n        return True\n    elif \"duration\" in im.encoderinfo and isinstance(\n        im.encoderinfo[\"duration\"], (list, tuple)\n    ):\n        # Since multiple frames will not be written, add together the frame durations\n        im.encoderinfo[\"duration\"] = sum(im.encoderinfo[\"duration\"])\n\n\ndef _save_all(im, fp, filename):\n    _save(im, fp, filename, save_all=True)\n\n\ndef _save(im, fp, filename, save_all=False):\n    # header\n    if \"palette\" in im.encoderinfo or \"palette\" in im.info:\n        palette = im.encoderinfo.get(\"palette\", im.info.get(\"palette\"))\n    else:\n        palette = None\n        im.encoderinfo[\"optimize\"] = im.encoderinfo.get(\"optimize\", True)\n\n    if not save_all or not _write_multiple_frames(im, fp, palette):\n        _write_single_frame(im, fp, palette)\n\n    fp.write(b\";\")  # end of file\n\n    if hasattr(fp, \"flush\"):\n        fp.flush()\n\n\ndef get_interlace(im):\n    interlace = im.encoderinfo.get(\"interlace\", 1)\n\n    # workaround for @PIL153\n    if min(im.size) < 16:\n        interlace = 0\n\n    return interlace\n\n\ndef _write_local_header(fp, im, offset, flags):\n    transparent_color_exists = False\n    try:\n        if \"transparency\" in im.encoderinfo:\n            transparency = im.encoderinfo[\"transparency\"]\n        else:\n            transparency = im.info[\"transparency\"]\n        transparency = int(transparency)\n    except (KeyError, ValueError):\n        pass\n    else:\n        # optimize the block away if transparent color is not used\n        transparent_color_exists = True\n\n        used_palette_colors = _get_optimize(im, im.encoderinfo)\n        if used_palette_colors is not None:\n            # adjust the transparency index after optimize\n            try:\n                transparency = used_palette_colors.index(transparency)\n            except ValueError:\n                transparent_color_exists = False\n\n    if \"duration\" in im.encoderinfo:\n        duration = int(im.encoderinfo[\"duration\"] / 10)\n    else:\n        duration = 0\n\n    disposal = int(im.encoderinfo.get(\"disposal\", 0))\n\n    if transparent_color_exists or duration != 0 or disposal:\n        packed_flag = 1 if transparent_color_exists else 0\n        packed_flag |= disposal << 2\n        if not transparent_color_exists:\n            transparency = 0\n\n        fp.write(\n            b\"!\"\n            + o8(249)  # extension intro\n            + o8(4)  # length\n            + o8(packed_flag)  # packed fields\n            + o16(duration)  # duration\n            + o8(transparency)  # transparency index\n            + o8(0)\n        )\n\n    include_color_table = im.encoderinfo.get(\"include_color_table\")\n    if include_color_table:\n        palette_bytes = _get_palette_bytes(im)\n        color_table_size = _get_color_table_size(palette_bytes)\n        if color_table_size:\n            flags = flags | 128  # local color table flag\n            flags = flags | color_table_size\n\n    fp.write(\n        b\",\"\n        + o16(offset[0])  # offset\n        + o16(offset[1])\n        + o16(im.size[0])  # size\n        + o16(im.size[1])\n        + o8(flags)  # flags\n    )\n    if include_color_table and color_table_size:\n        fp.write(_get_header_palette(palette_bytes))\n    fp.write(o8(8))  # bits\n\n\ndef _save_netpbm(im, fp, filename):\n\n    # Unused by default.\n    # To use, uncomment the register_save call at the end of the file.\n    #\n    # If you need real GIF compression and/or RGB quantization, you\n    # can use the external NETPBM/PBMPLUS utilities.  See comments\n    # below for information on how to enable this.\n    tempfile = im._dump()\n\n    try:\n        with open(filename, \"wb\") as f:\n            if im.mode != \"RGB\":\n                subprocess.check_call(\n                    [\"ppmtogif\", tempfile], stdout=f, stderr=subprocess.DEVNULL\n                )\n            else:\n                # Pipe ppmquant output into ppmtogif\n                # \"ppmquant 256 %s | ppmtogif > %s\" % (tempfile, filename)\n                quant_cmd = [\"ppmquant\", \"256\", tempfile]\n                togif_cmd = [\"ppmtogif\"]\n                quant_proc = subprocess.Popen(\n                    quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL\n                )\n                togif_proc = subprocess.Popen(\n                    togif_cmd,\n                    stdin=quant_proc.stdout,\n                    stdout=f,\n                    stderr=subprocess.DEVNULL,\n                )\n\n                # Allow ppmquant to receive SIGPIPE if ppmtogif exits\n                quant_proc.stdout.close()\n\n                retcode = quant_proc.wait()\n                if retcode:\n                    raise subprocess.CalledProcessError(retcode, quant_cmd)\n\n                retcode = togif_proc.wait()\n                if retcode:\n                    raise subprocess.CalledProcessError(retcode, togif_cmd)\n    finally:\n        try:\n            os.unlink(tempfile)\n        except OSError:\n            pass\n\n\n# Force optimization so that we can test performance against\n# cases where it took lots of memory and time previously.\n_FORCE_OPTIMIZE = False\n\n\ndef _get_optimize(im, info):\n    \"\"\"\n    Palette optimization is a potentially expensive operation.\n\n    This function determines if the palette should be optimized using\n    some heuristics, then returns the list of palette entries in use.\n\n    :param im: Image object\n    :param info: encoderinfo\n    :returns: list of indexes of palette entries in use, or None\n    \"\"\"\n    if im.mode in (\"P\", \"L\") and info and info.get(\"optimize\", 0):\n        # Potentially expensive operation.\n\n        # The palette saves 3 bytes per color not used, but palette\n        # lengths are restricted to 3*(2**N) bytes. Max saving would\n        # be 768 -> 6 bytes if we went all the way down to 2 colors.\n        # * If we're over 128 colors, we can't save any space.\n        # * If there aren't any holes, it's not worth collapsing.\n        # * If we have a 'large' image, the palette is in the noise.\n\n        # create the new palette if not every color is used\n        optimise = _FORCE_OPTIMIZE or im.mode == \"L\"\n        if optimise or im.width * im.height < 512 * 512:\n            # check which colors are used\n            used_palette_colors = []\n            for i, count in enumerate(im.histogram()):\n                if count:\n                    used_palette_colors.append(i)\n\n            if optimise or max(used_palette_colors) >= len(used_palette_colors):\n                return used_palette_colors\n\n            num_palette_colors = len(im.palette.palette) // Image.getmodebands(\n                im.palette.mode\n            )\n            current_palette_size = 1 << (num_palette_colors - 1).bit_length()\n            if (\n                # check that the palette would become smaller when saved\n                len(used_palette_colors) <= current_palette_size // 2\n                # check that the palette is not already the smallest possible size\n                and current_palette_size > 2\n            ):\n                return used_palette_colors\n\n\ndef _get_color_table_size(palette_bytes):\n    # calculate the palette size for the header\n    if not palette_bytes:\n        return 0\n    elif len(palette_bytes) < 9:\n        return 1\n    else:\n        return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1\n\n\ndef _get_header_palette(palette_bytes):\n    \"\"\"\n    Returns the palette, null padded to the next power of 2 (*3) bytes\n    suitable for direct inclusion in the GIF header\n\n    :param palette_bytes: Unpadded palette bytes, in RGBRGB form\n    :returns: Null padded palette\n    \"\"\"\n    color_table_size = _get_color_table_size(palette_bytes)\n\n    # add the missing amount of bytes\n    # the palette has to be 2<<n in size\n    actual_target_size_diff = (2 << color_table_size) - len(palette_bytes) // 3\n    if actual_target_size_diff > 0:\n        palette_bytes += o8(0) * 3 * actual_target_size_diff\n    return palette_bytes\n\n\ndef _get_palette_bytes(im):\n    \"\"\"\n    Gets the palette for inclusion in the gif header\n\n    :param im: Image object\n    :returns: Bytes, len<=768 suitable for inclusion in gif header\n    \"\"\"\n    return im.palette.palette\n\n\ndef _get_background(im, info_background):\n    background = 0\n    if info_background:\n        background = info_background\n        if isinstance(background, tuple):\n            # WebPImagePlugin stores an RGBA value in info[\"background\"]\n            # So it must be converted to the same format as GifImagePlugin's\n            # info[\"background\"] - a global color table index\n            try:\n                background = im.palette.getcolor(background, im)\n            except ValueError as e:\n                if str(e) == \"cannot allocate more than 256 colors\":\n                    # If all 256 colors are in use,\n                    # then there is no need for the background color\n                    return 0\n                else:\n                    raise\n    return background\n\n\ndef _get_global_header(im, info):\n    \"\"\"Return a list of strings representing a GIF header\"\"\"\n\n    # Header Block\n    # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp\n\n    version = b\"87a\"\n    if im.info.get(\"version\") == b\"89a\" or (\n        info\n        and (\n            \"transparency\" in info\n            or \"loop\" in info\n            or info.get(\"duration\")\n            or info.get(\"comment\")\n        )\n    ):\n        version = b\"89a\"\n\n    background = _get_background(im, info.get(\"background\"))\n\n    palette_bytes = _get_palette_bytes(im)\n    color_table_size = _get_color_table_size(palette_bytes)\n\n    header = [\n        b\"GIF\"  # signature\n        + version  # version\n        + o16(im.size[0])  # canvas width\n        + o16(im.size[1]),  # canvas height\n        # Logical Screen Descriptor\n        # size of global color table + global color table flag\n        o8(color_table_size + 128),  # packed fields\n        # background + reserved/aspect\n        o8(background) + o8(0),\n        # Global Color Table\n        _get_header_palette(palette_bytes),\n    ]\n    if \"loop\" in info:\n        header.append(\n            b\"!\"\n            + o8(255)  # extension intro\n            + o8(11)\n            + b\"NETSCAPE2.0\"\n            + o8(3)\n            + o8(1)\n            + o16(info[\"loop\"])  # number of loops\n            + o8(0)\n        )\n    if info.get(\"comment\"):\n        comment_block = b\"!\" + o8(254)  # extension intro\n\n        comment = info[\"comment\"]\n        if isinstance(comment, str):\n            comment = comment.encode()\n        for i in range(0, len(comment), 255):\n            subblock = comment[i : i + 255]\n            comment_block += o8(len(subblock)) + subblock\n\n        comment_block += o8(0)\n        header.append(comment_block)\n    return header\n\n\ndef _write_frame_data(fp, im_frame, offset, params):\n    try:\n        im_frame.encoderinfo = params\n\n        # local image header\n        _write_local_header(fp, im_frame, offset, 0)\n\n        ImageFile._save(\n            im_frame, fp, [(\"gif\", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])]\n        )\n\n        fp.write(b\"\\0\")  # end of image data\n    finally:\n        del im_frame.encoderinfo\n\n\n# --------------------------------------------------------------------\n# Legacy GIF utilities\n\n\ndef getheader(im, palette=None, info=None):\n    \"\"\"\n    Legacy Method to get Gif data from image.\n\n    Warning:: May modify image data.\n\n    :param im: Image object\n    :param palette: bytes object containing the source palette, or ....\n    :param info: encoderinfo\n    :returns: tuple of(list of header items, optimized palette)\n\n    \"\"\"\n    used_palette_colors = _get_optimize(im, info)\n\n    if info is None:\n        info = {}\n\n    if \"background\" not in info and \"background\" in im.info:\n        info[\"background\"] = im.info[\"background\"]\n\n    im_mod = _normalize_palette(im, palette, info)\n    im.palette = im_mod.palette\n    im.im = im_mod.im\n    header = _get_global_header(im, info)\n\n    return header, used_palette_colors\n\n\ndef getdata(im, offset=(0, 0), **params):\n    \"\"\"\n    Legacy Method\n\n    Return a list of strings representing this image.\n    The first string is a local image header, the rest contains\n    encoded image data.\n\n    To specify duration, add the time in milliseconds,\n    e.g. ``getdata(im_frame, duration=1000)``\n\n    :param im: Image object\n    :param offset: Tuple of (x, y) pixels. Defaults to (0, 0)\n    :param \\\\**params: e.g. duration or other encoder info parameters\n    :returns: List of bytes containing GIF encoded frame data\n\n    \"\"\"\n\n    class Collector:\n        data = []\n\n        def write(self, data):\n            self.data.append(data)\n\n    im.load()  # make sure raster data is available\n\n    fp = Collector()\n\n    _write_frame_data(fp, im, offset, params)\n\n    return fp.data\n\n\n# --------------------------------------------------------------------\n# Registry\n\nImage.register_open(GifImageFile.format, GifImageFile, _accept)\nImage.register_save(GifImageFile.format, _save)\nImage.register_save_all(GifImageFile.format, _save_all)\nImage.register_extension(GifImageFile.format, \".gif\")\nImage.register_mime(GifImageFile.format, \"image/gif\")\n\n#\n# Uncomment the following line if you wish to use NETPBM/PBMPLUS\n# instead of the built-in \"uncompressed\" GIF encoder\n\n# Image.register_save(GifImageFile.format, _save_netpbm)\n",
            "new_file_content": "#\n# The Python Imaging Library.\n# $Id$\n#\n# GIF file handling\n#\n# History:\n# 1995-09-01 fl   Created\n# 1996-12-14 fl   Added interlace support\n# 1996-12-30 fl   Added animation support\n# 1997-01-05 fl   Added write support, fixed local colour map bug\n# 1997-02-23 fl   Make sure to load raster data in getdata()\n# 1997-07-05 fl   Support external decoder (0.4)\n# 1998-07-09 fl   Handle all modes when saving (0.5)\n# 1998-07-15 fl   Renamed offset attribute to avoid name clash\n# 2001-04-16 fl   Added rewind support (seek to frame 0) (0.6)\n# 2001-04-17 fl   Added palette optimization (0.7)\n# 2002-06-06 fl   Added transparency support for save (0.8)\n# 2004-02-24 fl   Disable interlacing for small images\n#\n# Copyright (c) 1997-2004 by Secret Labs AB\n# Copyright (c) 1995-2004 by Fredrik Lundh\n#\n# See the README file for information on usage and redistribution.\n#\n\nimport itertools\nimport math\nimport os\nimport subprocess\nfrom enum import IntEnum\n\nfrom . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence\nfrom ._binary import i16le as i16\nfrom ._binary import o8\nfrom ._binary import o16le as o16\n\n\nclass LoadingStrategy(IntEnum):\n    \"\"\".. versionadded:: 9.1.0\"\"\"\n\n    RGB_AFTER_FIRST = 0\n    RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1\n    RGB_ALWAYS = 2\n\n\n#: .. versionadded:: 9.1.0\nLOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST\n\n# --------------------------------------------------------------------\n# Identify/read GIF files\n\n\ndef _accept(prefix):\n    return prefix[:6] in [b\"GIF87a\", b\"GIF89a\"]\n\n\n##\n# Image plugin for GIF images.  This plugin supports both GIF87 and\n# GIF89 images.\n\n\nclass GifImageFile(ImageFile.ImageFile):\n\n    format = \"GIF\"\n    format_description = \"Compuserve GIF\"\n    _close_exclusive_fp_after_loading = False\n\n    global_palette = None\n\n    def data(self):\n        s = self.fp.read(1)\n        if s and s[0]:\n            return self.fp.read(s[0])\n        return None\n\n    def _is_palette_needed(self, p):\n        for i in range(0, len(p), 3):\n            if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):\n                return True\n        return False\n\n    def _open(self):\n\n        # Screen\n        s = self.fp.read(13)\n        if not _accept(s):\n            raise SyntaxError(\"not a GIF file\")\n\n        self.info[\"version\"] = s[:6]\n        self._size = i16(s, 6), i16(s, 8)\n        self.tile = []\n        flags = s[10]\n        bits = (flags & 7) + 1\n\n        if flags & 128:\n            # get global palette\n            self.info[\"background\"] = s[11]\n            # check if palette contains colour indices\n            p = self.fp.read(3 << bits)\n            if self._is_palette_needed(p):\n                p = ImagePalette.raw(\"RGB\", p)\n                self.global_palette = self.palette = p\n\n        self._fp = self.fp  # FIXME: hack\n        self.__rewind = self.fp.tell()\n        self._n_frames = None\n        self._is_animated = None\n        self._seek(0)  # get ready to read first frame\n\n    @property\n    def n_frames(self):\n        if self._n_frames is None:\n            current = self.tell()\n            try:\n                while True:\n                    self._seek(self.tell() + 1, False)\n            except EOFError:\n                self._n_frames = self.tell() + 1\n            self.seek(current)\n        return self._n_frames\n\n    @property\n    def is_animated(self):\n        if self._is_animated is None:\n            if self._n_frames is not None:\n                self._is_animated = self._n_frames != 1\n            else:\n                current = self.tell()\n                if current:\n                    self._is_animated = True\n                else:\n                    try:\n                        self._seek(1, False)\n                        self._is_animated = True\n                    except EOFError:\n                        self._is_animated = False\n\n                    self.seek(current)\n        return self._is_animated\n\n    def seek(self, frame):\n        if not self._seek_check(frame):\n            return\n        if frame < self.__frame:\n            self.im = None\n            self._seek(0)\n\n        last_frame = self.__frame\n        for f in range(self.__frame + 1, frame + 1):\n            try:\n                self._seek(f)\n            except EOFError as e:\n                self.seek(last_frame)\n                raise EOFError(\"no more images in GIF file\") from e\n\n    def _seek(self, frame, update_image=True):\n\n        if frame == 0:\n            # rewind\n            self.__offset = 0\n            self.dispose = None\n            self.__frame = -1\n            self._fp.seek(self.__rewind)\n            self.disposal_method = 0\n            if \"comment\" in self.info:\n                del self.info[\"comment\"]\n        else:\n            # ensure that the previous frame was loaded\n            if self.tile and update_image:\n                self.load()\n\n        if frame != self.__frame + 1:\n            raise ValueError(f\"cannot seek to frame {frame}\")\n\n        self.fp = self._fp\n        if self.__offset:\n            # backup to last frame\n            self.fp.seek(self.__offset)\n            while self.data():\n                pass\n            self.__offset = 0\n\n        s = self.fp.read(1)\n        if not s or s == b\";\":\n            raise EOFError\n\n        palette = None\n\n        info = {}\n        frame_transparency = None\n        interlace = None\n        frame_dispose_extent = None\n        while True:\n\n            if not s:\n                s = self.fp.read(1)\n            if not s or s == b\";\":\n                break\n\n            elif s == b\"!\":\n                #\n                # extensions\n                #\n                s = self.fp.read(1)\n                block = self.data()\n                if s[0] == 249:\n                    #\n                    # graphic control extension\n                    #\n                    flags = block[0]\n                    if flags & 1:\n                        frame_transparency = block[3]\n                    info[\"duration\"] = i16(block, 1) * 10\n\n                    # disposal method - find the value of bits 4 - 6\n                    dispose_bits = 0b00011100 & flags\n                    dispose_bits = dispose_bits >> 2\n                    if dispose_bits:\n                        # only set the dispose if it is not\n                        # unspecified. I'm not sure if this is\n                        # correct, but it seems to prevent the last\n                        # frame from looking odd for some animations\n                        self.disposal_method = dispose_bits\n                elif s[0] == 254:\n                    #\n                    # comment extension\n                    #\n                    comment = b\"\"\n\n                    # Read this comment block\n                    while block:\n                        comment += block\n                        block = self.data()\n\n                    if \"comment\" in info:\n                        # If multiple comment blocks in frame, separate with \\n\n                        info[\"comment\"] += b\"\\n\" + comment\n                    else:\n                        info[\"comment\"] = comment\n                    s = None\n                    continue\n                elif s[0] == 255 and frame == 0:\n                    #\n                    # application extension\n                    #\n                    info[\"extension\"] = block, self.fp.tell()\n                    if block[:11] == b\"NETSCAPE2.0\":\n                        block = self.data()\n                        if len(block) >= 3 and block[0] == 1:\n                            self.info[\"loop\"] = i16(block, 1)\n                while self.data():\n                    pass\n\n            elif s == b\",\":\n                #\n                # local image\n                #\n                s = self.fp.read(9)\n\n                # extent\n                x0, y0 = i16(s, 0), i16(s, 2)\n                x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)\n                if (x1 > self.size[0] or y1 > self.size[1]) and update_image:\n                    self._size = max(x1, self.size[0]), max(y1, self.size[1])\n                    Image._decompression_bomb_check(self._size)\n                frame_dispose_extent = x0, y0, x1, y1\n                flags = s[8]\n\n                interlace = (flags & 64) != 0\n\n                if flags & 128:\n                    bits = (flags & 7) + 1\n                    p = self.fp.read(3 << bits)\n                    if self._is_palette_needed(p):\n                        palette = ImagePalette.raw(\"RGB\", p)\n                    else:\n                        palette = False\n\n                # image data\n                bits = self.fp.read(1)[0]\n                self.__offset = self.fp.tell()\n                break\n\n            else:\n                pass\n                # raise OSError, \"illegal GIF tag `%x`\" % s[0]\n            s = None\n\n        if interlace is None:\n            # self._fp = None\n            raise EOFError\n\n        self.__frame = frame\n        if not update_image:\n            return\n\n        self.tile = []\n\n        if self.dispose:\n            self.im.paste(self.dispose, self.dispose_extent)\n\n        self._frame_palette = palette if palette is not None else self.global_palette\n        self._frame_transparency = frame_transparency\n        if frame == 0:\n            if self._frame_palette:\n                if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:\n                    self.mode = \"RGBA\" if frame_transparency is not None else \"RGB\"\n                else:\n                    self.mode = \"P\"\n            else:\n                self.mode = \"L\"\n\n            if not palette and self.global_palette:\n                from copy import copy\n\n                palette = copy(self.global_palette)\n            self.palette = palette\n        else:\n            if self.mode == \"P\":\n                if (\n                    LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY\n                    or palette\n                ):\n                    self.pyaccess = None\n                    if \"transparency\" in self.info:\n                        self.im.putpalettealpha(self.info[\"transparency\"], 0)\n                        self.im = self.im.convert(\"RGBA\", Image.Dither.FLOYDSTEINBERG)\n                        self.mode = \"RGBA\"\n                        del self.info[\"transparency\"]\n                    else:\n                        self.mode = \"RGB\"\n                        self.im = self.im.convert(\"RGB\", Image.Dither.FLOYDSTEINBERG)\n\n        def _rgb(color):\n            if self._frame_palette:\n                color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])\n            else:\n                color = (color, color, color)\n            return color\n\n        self.dispose_extent = frame_dispose_extent\n        try:\n            if self.disposal_method < 2:\n                # do not dispose or none specified\n                self.dispose = None\n            elif self.disposal_method == 2:\n                # replace with background colour\n\n                # only dispose the extent in this frame\n                x0, y0, x1, y1 = self.dispose_extent\n                dispose_size = (x1 - x0, y1 - y0)\n\n                Image._decompression_bomb_check(dispose_size)\n\n                # by convention, attempt to use transparency first\n                dispose_mode = \"P\"\n                color = self.info.get(\"transparency\", frame_transparency)\n                if color is not None:\n                    if self.mode in (\"RGB\", \"RGBA\"):\n                        dispose_mode = \"RGBA\"\n                        color = _rgb(color) + (0,)\n                else:\n                    color = self.info.get(\"background\", 0)\n                    if self.mode in (\"RGB\", \"RGBA\"):\n                        dispose_mode = \"RGB\"\n                        color = _rgb(color)\n                self.dispose = Image.core.fill(dispose_mode, dispose_size, color)\n            else:\n                # replace with previous contents\n                if self.im is not None:\n                    # only dispose the extent in this frame\n                    self.dispose = self._crop(self.im, self.dispose_extent)\n                elif frame_transparency is not None:\n                    x0, y0, x1, y1 = self.dispose_extent\n                    dispose_size = (x1 - x0, y1 - y0)\n\n                    Image._decompression_bomb_check(dispose_size)\n                    dispose_mode = \"P\"\n                    color = frame_transparency\n                    if self.mode in (\"RGB\", \"RGBA\"):\n                        dispose_mode = \"RGBA\"\n                        color = _rgb(frame_transparency) + (0,)\n                    self.dispose = Image.core.fill(dispose_mode, dispose_size, color)\n        except AttributeError:\n            pass\n\n        if interlace is not None:\n            transparency = -1\n            if frame_transparency is not None:\n                if frame == 0:\n                    if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS:\n                        self.info[\"transparency\"] = frame_transparency\n                elif self.mode not in (\"RGB\", \"RGBA\"):\n                    transparency = frame_transparency\n            self.tile = [\n                (\n                    \"gif\",\n                    (x0, y0, x1, y1),\n                    self.__offset,\n                    (bits, interlace, transparency),\n                )\n            ]\n\n        if info.get(\"comment\"):\n            self.info[\"comment\"] = info[\"comment\"]\n        for k in [\"duration\", \"extension\"]:\n            if k in info:\n                self.info[k] = info[k]\n            elif k in self.info:\n                del self.info[k]\n\n    def load_prepare(self):\n        temp_mode = \"P\" if self._frame_palette else \"L\"\n        self._prev_im = None\n        if self.__frame == 0:\n            if self._frame_transparency is not None:\n                self.im = Image.core.fill(\n                    temp_mode, self.size, self._frame_transparency\n                )\n        elif self.mode in (\"RGB\", \"RGBA\"):\n            self._prev_im = self.im\n            if self._frame_palette:\n                self.im = Image.core.fill(\"P\", self.size, self._frame_transparency or 0)\n                self.im.putpalette(*self._frame_palette.getdata())\n            else:\n                self.im = None\n        self.mode = temp_mode\n        self._frame_palette = None\n\n        super().load_prepare()\n\n    def load_end(self):\n        if self.__frame == 0:\n            if self.mode == \"P\" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:\n                if self._frame_transparency is not None:\n                    self.im.putpalettealpha(self._frame_transparency, 0)\n                    self.mode = \"RGBA\"\n                else:\n                    self.mode = \"RGB\"\n                self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)\n            return\n        if not self._prev_im:\n            return\n        if self._frame_transparency is not None:\n            self.im.putpalettealpha(self._frame_transparency, 0)\n            frame_im = self.im.convert(\"RGBA\")\n        else:\n            frame_im = self.im.convert(\"RGB\")\n        frame_im = self._crop(frame_im, self.dispose_extent)\n\n        self.im = self._prev_im\n        self.mode = self.im.mode\n        if frame_im.mode == \"RGBA\":\n            self.im.paste(frame_im, self.dispose_extent, frame_im)\n        else:\n            self.im.paste(frame_im, self.dispose_extent)\n\n    def tell(self):\n        return self.__frame\n\n\n# --------------------------------------------------------------------\n# Write GIF files\n\n\nRAWMODE = {\"1\": \"L\", \"L\": \"L\", \"P\": \"P\"}\n\n\ndef _normalize_mode(im):\n    \"\"\"\n    Takes an image (or frame), returns an image in a mode that is appropriate\n    for saving in a Gif.\n\n    It may return the original image, or it may return an image converted to\n    palette or 'L' mode.\n\n    :param im: Image object\n    :returns: Image object\n    \"\"\"\n    if im.mode in RAWMODE:\n        im.load()\n        return im\n    if Image.getmodebase(im.mode) == \"RGB\":\n        im = im.convert(\"P\", palette=Image.Palette.ADAPTIVE)\n        if im.palette.mode == \"RGBA\":\n            for rgba in im.palette.colors.keys():\n                if rgba[3] == 0:\n                    im.info[\"transparency\"] = im.palette.colors[rgba]\n                    break\n        return im\n    return im.convert(\"L\")\n\n\ndef _normalize_palette(im, palette, info):\n    \"\"\"\n    Normalizes the palette for image.\n      - Sets the palette to the incoming palette, if provided.\n      - Ensures that there's a palette for L mode images\n      - Optimizes the palette if necessary/desired.\n\n    :param im: Image object\n    :param palette: bytes object containing the source palette, or ....\n    :param info: encoderinfo\n    :returns: Image object\n    \"\"\"\n    source_palette = None\n    if palette:\n        # a bytes palette\n        if isinstance(palette, (bytes, bytearray, list)):\n            source_palette = bytearray(palette[:768])\n        if isinstance(palette, ImagePalette.ImagePalette):\n            source_palette = bytearray(palette.palette)\n\n    if im.mode == \"P\":\n        if not source_palette:\n            source_palette = im.im.getpalette(\"RGB\")[:768]\n    else:  # L-mode\n        if not source_palette:\n            source_palette = bytearray(i // 3 for i in range(768))\n        im.palette = ImagePalette.ImagePalette(\"RGB\", palette=source_palette)\n\n    if palette:\n        used_palette_colors = []\n        for i in range(0, len(source_palette), 3):\n            source_color = tuple(source_palette[i : i + 3])\n            index = im.palette.colors.get(source_color)\n            if index in used_palette_colors:\n                index = None\n            used_palette_colors.append(index)\n        for i, index in enumerate(used_palette_colors):\n            if index is None:\n                for j in range(len(used_palette_colors)):\n                    if j not in used_palette_colors:\n                        used_palette_colors[i] = j\n                        break\n        im = im.remap_palette(used_palette_colors)\n    else:\n        used_palette_colors = _get_optimize(im, info)\n        if used_palette_colors is not None:\n            return im.remap_palette(used_palette_colors, source_palette)\n\n    im.palette.palette = source_palette\n    return im\n\n\ndef _write_single_frame(im, fp, palette):\n    im_out = _normalize_mode(im)\n    for k, v in im_out.info.items():\n        im.encoderinfo.setdefault(k, v)\n    im_out = _normalize_palette(im_out, palette, im.encoderinfo)\n\n    for s in _get_global_header(im_out, im.encoderinfo):\n        fp.write(s)\n\n    # local image header\n    flags = 0\n    if get_interlace(im):\n        flags = flags | 64\n    _write_local_header(fp, im, (0, 0), flags)\n\n    im_out.encoderconfig = (8, get_interlace(im))\n    ImageFile._save(im_out, fp, [(\"gif\", (0, 0) + im.size, 0, RAWMODE[im_out.mode])])\n\n    fp.write(b\"\\0\")  # end of image data\n\n\ndef _write_multiple_frames(im, fp, palette):\n\n    duration = im.encoderinfo.get(\"duration\")\n    disposal = im.encoderinfo.get(\"disposal\", im.info.get(\"disposal\"))\n\n    im_frames = []\n    frame_count = 0\n    background_im = None\n    for imSequence in itertools.chain([im], im.encoderinfo.get(\"append_images\", [])):\n        for im_frame in ImageSequence.Iterator(imSequence):\n            # a copy is required here since seek can still mutate the image\n            im_frame = _normalize_mode(im_frame.copy())\n            if frame_count == 0:\n                for k, v in im_frame.info.items():\n                    if k == \"transparency\":\n                        continue\n                    im.encoderinfo.setdefault(k, v)\n\n            encoderinfo = im.encoderinfo.copy()\n            im_frame = _normalize_palette(im_frame, palette, encoderinfo)\n            if \"transparency\" in im_frame.info:\n                encoderinfo.setdefault(\"transparency\", im_frame.info[\"transparency\"])\n            if isinstance(duration, (list, tuple)):\n                encoderinfo[\"duration\"] = duration[frame_count]\n            elif duration is None and \"duration\" in im_frame.info:\n                encoderinfo[\"duration\"] = im_frame.info[\"duration\"]\n            if isinstance(disposal, (list, tuple)):\n                encoderinfo[\"disposal\"] = disposal[frame_count]\n            frame_count += 1\n\n            if im_frames:\n                # delta frame\n                previous = im_frames[-1]\n                if encoderinfo.get(\"disposal\") == 2:\n                    if background_im is None:\n                        color = im.encoderinfo.get(\n                            \"transparency\", im.info.get(\"transparency\", (0, 0, 0))\n                        )\n                        background = _get_background(im_frame, color)\n                        background_im = Image.new(\"P\", im_frame.size, background)\n                        background_im.putpalette(im_frames[0][\"im\"].palette)\n                    base_im = background_im\n                else:\n                    base_im = previous[\"im\"]\n                if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):\n                    delta = ImageChops.subtract_modulo(im_frame, base_im)\n                else:\n                    delta = ImageChops.subtract_modulo(\n                        im_frame.convert(\"RGB\"), base_im.convert(\"RGB\")\n                    )\n                bbox = delta.getbbox()\n                if not bbox:\n                    # This frame is identical to the previous frame\n                    if encoderinfo.get(\"duration\"):\n                        previous[\"encoderinfo\"][\"duration\"] += encoderinfo[\"duration\"]\n                    continue\n            else:\n                bbox = None\n            im_frames.append({\"im\": im_frame, \"bbox\": bbox, \"encoderinfo\": encoderinfo})\n\n    if len(im_frames) > 1:\n        for frame_data in im_frames:\n            im_frame = frame_data[\"im\"]\n            if not frame_data[\"bbox\"]:\n                # global header\n                for s in _get_global_header(im_frame, frame_data[\"encoderinfo\"]):\n                    fp.write(s)\n                offset = (0, 0)\n            else:\n                # compress difference\n                if not palette:\n                    frame_data[\"encoderinfo\"][\"include_color_table\"] = True\n\n                im_frame = im_frame.crop(frame_data[\"bbox\"])\n                offset = frame_data[\"bbox\"][:2]\n            _write_frame_data(fp, im_frame, offset, frame_data[\"encoderinfo\"])\n        return True\n    elif \"duration\" in im.encoderinfo and isinstance(\n        im.encoderinfo[\"duration\"], (list, tuple)\n    ):\n        # Since multiple frames will not be written, add together the frame durations\n        im.encoderinfo[\"duration\"] = sum(im.encoderinfo[\"duration\"])\n\n\ndef _save_all(im, fp, filename):\n    _save(im, fp, filename, save_all=True)\n\n\ndef _save(im, fp, filename, save_all=False):\n    # header\n    if \"palette\" in im.encoderinfo or \"palette\" in im.info:\n        palette = im.encoderinfo.get(\"palette\", im.info.get(\"palette\"))\n    else:\n        palette = None\n        im.encoderinfo[\"optimize\"] = im.encoderinfo.get(\"optimize\", True)\n\n    if not save_all or not _write_multiple_frames(im, fp, palette):\n        _write_single_frame(im, fp, palette)\n\n    fp.write(b\";\")  # end of file\n\n    if hasattr(fp, \"flush\"):\n        fp.flush()\n\n\ndef get_interlace(im):\n    interlace = im.encoderinfo.get(\"interlace\", 1)\n\n    # workaround for @PIL153\n    if min(im.size) < 16:\n        interlace = 0\n\n    return interlace\n\n\ndef _write_local_header(fp, im, offset, flags):\n    transparent_color_exists = False\n    try:\n        if \"transparency\" in im.encoderinfo:\n            transparency = im.encoderinfo[\"transparency\"]\n        else:\n            transparency = im.info[\"transparency\"]\n        transparency = int(transparency)\n    except (KeyError, ValueError):\n        pass\n    else:\n        # optimize the block away if transparent color is not used\n        transparent_color_exists = True\n\n        used_palette_colors = _get_optimize(im, im.encoderinfo)\n        if used_palette_colors is not None:\n            # adjust the transparency index after optimize\n            try:\n                transparency = used_palette_colors.index(transparency)\n            except ValueError:\n                transparent_color_exists = False\n\n    if \"duration\" in im.encoderinfo:\n        duration = int(im.encoderinfo[\"duration\"] / 10)\n    else:\n        duration = 0\n\n    disposal = int(im.encoderinfo.get(\"disposal\", 0))\n\n    if transparent_color_exists or duration != 0 or disposal:\n        packed_flag = 1 if transparent_color_exists else 0\n        packed_flag |= disposal << 2\n        if not transparent_color_exists:\n            transparency = 0\n\n        fp.write(\n            b\"!\"\n            + o8(249)  # extension intro\n            + o8(4)  # length\n            + o8(packed_flag)  # packed fields\n            + o16(duration)  # duration\n            + o8(transparency)  # transparency index\n            + o8(0)\n        )\n\n    include_color_table = im.encoderinfo.get(\"include_color_table\")\n    if include_color_table:\n        palette_bytes = _get_palette_bytes(im)\n        color_table_size = _get_color_table_size(palette_bytes)\n        if color_table_size:\n            flags = flags | 128  # local color table flag\n            flags = flags | color_table_size\n\n    fp.write(\n        b\",\"\n        + o16(offset[0])  # offset\n        + o16(offset[1])\n        + o16(im.size[0])  # size\n        + o16(im.size[1])\n        + o8(flags)  # flags\n    )\n    if include_color_table and color_table_size:\n        fp.write(_get_header_palette(palette_bytes))\n    fp.write(o8(8))  # bits\n\n\ndef _save_netpbm(im, fp, filename):\n\n    # Unused by default.\n    # To use, uncomment the register_save call at the end of the file.\n    #\n    # If you need real GIF compression and/or RGB quantization, you\n    # can use the external NETPBM/PBMPLUS utilities.  See comments\n    # below for information on how to enable this.\n    tempfile = im._dump()\n\n    try:\n        with open(filename, \"wb\") as f:\n            if im.mode != \"RGB\":\n                subprocess.check_call(\n                    [\"ppmtogif\", tempfile], stdout=f, stderr=subprocess.DEVNULL\n                )\n            else:\n                # Pipe ppmquant output into ppmtogif\n                # \"ppmquant 256 %s | ppmtogif > %s\" % (tempfile, filename)\n                quant_cmd = [\"ppmquant\", \"256\", tempfile]\n                togif_cmd = [\"ppmtogif\"]\n                quant_proc = subprocess.Popen(\n                    quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL\n                )\n                togif_proc = subprocess.Popen(\n                    togif_cmd,\n                    stdin=quant_proc.stdout,\n                    stdout=f,\n                    stderr=subprocess.DEVNULL,\n                )\n\n                # Allow ppmquant to receive SIGPIPE if ppmtogif exits\n                quant_proc.stdout.close()\n\n                retcode = quant_proc.wait()\n                if retcode:\n                    raise subprocess.CalledProcessError(retcode, quant_cmd)\n\n                retcode = togif_proc.wait()\n                if retcode:\n                    raise subprocess.CalledProcessError(retcode, togif_cmd)\n    finally:\n        try:\n            os.unlink(tempfile)\n        except OSError:\n            pass\n\n\n# Force optimization so that we can test performance against\n# cases where it took lots of memory and time previously.\n_FORCE_OPTIMIZE = False\n\n\ndef _get_optimize(im, info):\n    \"\"\"\n    Palette optimization is a potentially expensive operation.\n\n    This function determines if the palette should be optimized using\n    some heuristics, then returns the list of palette entries in use.\n\n    :param im: Image object\n    :param info: encoderinfo\n    :returns: list of indexes of palette entries in use, or None\n    \"\"\"\n    if im.mode in (\"P\", \"L\") and info and info.get(\"optimize\", 0):\n        # Potentially expensive operation.\n\n        # The palette saves 3 bytes per color not used, but palette\n        # lengths are restricted to 3*(2**N) bytes. Max saving would\n        # be 768 -> 6 bytes if we went all the way down to 2 colors.\n        # * If we're over 128 colors, we can't save any space.\n        # * If there aren't any holes, it's not worth collapsing.\n        # * If we have a 'large' image, the palette is in the noise.\n\n        # create the new palette if not every color is used\n        optimise = _FORCE_OPTIMIZE or im.mode == \"L\"\n        if optimise or im.width * im.height < 512 * 512:\n            # check which colors are used\n            used_palette_colors = []\n            for i, count in enumerate(im.histogram()):\n                if count:\n                    used_palette_colors.append(i)\n\n            if optimise or max(used_palette_colors) >= len(used_palette_colors):\n                return used_palette_colors\n\n            num_palette_colors = len(im.palette.palette) // Image.getmodebands(\n                im.palette.mode\n            )\n            current_palette_size = 1 << (num_palette_colors - 1).bit_length()\n            if (\n                # check that the palette would become smaller when saved\n                len(used_palette_colors) <= current_palette_size // 2\n                # check that the palette is not already the smallest possible size\n                and current_palette_size > 2\n            ):\n                return used_palette_colors\n\n\ndef _get_color_table_size(palette_bytes):\n    # calculate the palette size for the header\n    if not palette_bytes:\n        return 0\n    elif len(palette_bytes) < 9:\n        return 1\n    else:\n        return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1\n\n\ndef _get_header_palette(palette_bytes):\n    \"\"\"\n    Returns the palette, null padded to the next power of 2 (*3) bytes\n    suitable for direct inclusion in the GIF header\n\n    :param palette_bytes: Unpadded palette bytes, in RGBRGB form\n    :returns: Null padded palette\n    \"\"\"\n    color_table_size = _get_color_table_size(palette_bytes)\n\n    # add the missing amount of bytes\n    # the palette has to be 2<<n in size\n    actual_target_size_diff = (2 << color_table_size) - len(palette_bytes) // 3\n    if actual_target_size_diff > 0:\n        palette_bytes += o8(0) * 3 * actual_target_size_diff\n    return palette_bytes\n\n\ndef _get_palette_bytes(im):\n    \"\"\"\n    Gets the palette for inclusion in the gif header\n\n    :param im: Image object\n    :returns: Bytes, len<=768 suitable for inclusion in gif header\n    \"\"\"\n    return im.palette.palette\n\n\ndef _get_background(im, info_background):\n    background = 0\n    if info_background:\n        if isinstance(info_background, tuple):\n            # WebPImagePlugin stores an RGBA value in info[\"background\"]\n            # So it must be converted to the same format as GifImagePlugin's\n            # info[\"background\"] - a global color table index\n            try:\n                background = im.palette.getcolor(info_background, im)\n            except ValueError as e:\n                if str(e) not in (\n                    # If all 256 colors are in use,\n                    # then there is no need for the background color\n                    \"cannot allocate more than 256 colors\",\n                    # Ignore non-opaque WebP background\n                    \"cannot add non-opaque RGBA color to RGB palette\",\n                ):\n                    raise\n        else:\n            background = info_background\n    return background\n\n\ndef _get_global_header(im, info):\n    \"\"\"Return a list of strings representing a GIF header\"\"\"\n\n    # Header Block\n    # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp\n\n    version = b\"87a\"\n    if im.info.get(\"version\") == b\"89a\" or (\n        info\n        and (\n            \"transparency\" in info\n            or \"loop\" in info\n            or info.get(\"duration\")\n            or info.get(\"comment\")\n        )\n    ):\n        version = b\"89a\"\n\n    background = _get_background(im, info.get(\"background\"))\n\n    palette_bytes = _get_palette_bytes(im)\n    color_table_size = _get_color_table_size(palette_bytes)\n\n    header = [\n        b\"GIF\"  # signature\n        + version  # version\n        + o16(im.size[0])  # canvas width\n        + o16(im.size[1]),  # canvas height\n        # Logical Screen Descriptor\n        # size of global color table + global color table flag\n        o8(color_table_size + 128),  # packed fields\n        # background + reserved/aspect\n        o8(background) + o8(0),\n        # Global Color Table\n        _get_header_palette(palette_bytes),\n    ]\n    if \"loop\" in info:\n        header.append(\n            b\"!\"\n            + o8(255)  # extension intro\n            + o8(11)\n            + b\"NETSCAPE2.0\"\n            + o8(3)\n            + o8(1)\n            + o16(info[\"loop\"])  # number of loops\n            + o8(0)\n        )\n    if info.get(\"comment\"):\n        comment_block = b\"!\" + o8(254)  # extension intro\n\n        comment = info[\"comment\"]\n        if isinstance(comment, str):\n            comment = comment.encode()\n        for i in range(0, len(comment), 255):\n            subblock = comment[i : i + 255]\n            comment_block += o8(len(subblock)) + subblock\n\n        comment_block += o8(0)\n        header.append(comment_block)\n    return header\n\n\ndef _write_frame_data(fp, im_frame, offset, params):\n    try:\n        im_frame.encoderinfo = params\n\n        # local image header\n        _write_local_header(fp, im_frame, offset, 0)\n\n        ImageFile._save(\n            im_frame, fp, [(\"gif\", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])]\n        )\n\n        fp.write(b\"\\0\")  # end of image data\n    finally:\n        del im_frame.encoderinfo\n\n\n# --------------------------------------------------------------------\n# Legacy GIF utilities\n\n\ndef getheader(im, palette=None, info=None):\n    \"\"\"\n    Legacy Method to get Gif data from image.\n\n    Warning:: May modify image data.\n\n    :param im: Image object\n    :param palette: bytes object containing the source palette, or ....\n    :param info: encoderinfo\n    :returns: tuple of(list of header items, optimized palette)\n\n    \"\"\"\n    used_palette_colors = _get_optimize(im, info)\n\n    if info is None:\n        info = {}\n\n    if \"background\" not in info and \"background\" in im.info:\n        info[\"background\"] = im.info[\"background\"]\n\n    im_mod = _normalize_palette(im, palette, info)\n    im.palette = im_mod.palette\n    im.im = im_mod.im\n    header = _get_global_header(im, info)\n\n    return header, used_palette_colors\n\n\ndef getdata(im, offset=(0, 0), **params):\n    \"\"\"\n    Legacy Method\n\n    Return a list of strings representing this image.\n    The first string is a local image header, the rest contains\n    encoded image data.\n\n    To specify duration, add the time in milliseconds,\n    e.g. ``getdata(im_frame, duration=1000)``\n\n    :param im: Image object\n    :param offset: Tuple of (x, y) pixels. Defaults to (0, 0)\n    :param \\\\**params: e.g. duration or other encoder info parameters\n    :returns: List of bytes containing GIF encoded frame data\n\n    \"\"\"\n\n    class Collector:\n        data = []\n\n        def write(self, data):\n            self.data.append(data)\n\n    im.load()  # make sure raster data is available\n\n    fp = Collector()\n\n    _write_frame_data(fp, im, offset, params)\n\n    return fp.data\n\n\n# --------------------------------------------------------------------\n# Registry\n\nImage.register_open(GifImageFile.format, GifImageFile, _accept)\nImage.register_save(GifImageFile.format, _save)\nImage.register_save_all(GifImageFile.format, _save_all)\nImage.register_extension(GifImageFile.format, \".gif\")\nImage.register_mime(GifImageFile.format, \"image/gif\")\n\n#\n# Uncomment the following line if you wish to use NETPBM/PBMPLUS\n# instead of the built-in \"uncompressed\" GIF encoder\n\n# Image.register_save(GifImageFile.format, _save_netpbm)\n",
            "header": {
                "file": {
                    "path": "src/PIL/GifImagePlugin.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "13894c8d6",
                "new_commit_hash": "b1d842cef",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/src/PIL/GifImagePlugin.py"
            },
            "plus_file": {
                "path": "b/src/PIL/GifImagePlugin.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 886,
                            "length": 20
                        },
                        "new_range": {
                            "start": 886,
                            "length": 23
                        },
                        "section": "def _get_palette_bytes(im):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "def _get_background(im, info_background):",
                                "type": "context"
                            },
                            {
                                "content": "    background = 0",
                                "type": "context"
                            },
                            {
                                "content": "    if info_background:",
                                "type": "context"
                            },
                            {
                                "content": "        background = info_background",
                                "type": "deleted"
                            },
                            {
                                "content": "        if isinstance(background, tuple):",
                                "type": "deleted"
                            },
                            {
                                "content": "        if isinstance(info_background, tuple):",
                                "type": "added"
                            },
                            {
                                "content": "            # WebPImagePlugin stores an RGBA value in info[\"background\"]",
                                "type": "context"
                            },
                            {
                                "content": "            # So it must be converted to the same format as GifImagePlugin's",
                                "type": "context"
                            },
                            {
                                "content": "            # info[\"background\"] - a global color table index",
                                "type": "context"
                            },
                            {
                                "content": "            try:",
                                "type": "context"
                            },
                            {
                                "content": "                background = im.palette.getcolor(background, im)",
                                "type": "deleted"
                            },
                            {
                                "content": "                background = im.palette.getcolor(info_background, im)",
                                "type": "added"
                            },
                            {
                                "content": "            except ValueError as e:",
                                "type": "context"
                            },
                            {
                                "content": "                if str(e) == \"cannot allocate more than 256 colors\":",
                                "type": "deleted"
                            },
                            {
                                "content": "                if str(e) not in (",
                                "type": "added"
                            },
                            {
                                "content": "                    # If all 256 colors are in use,",
                                "type": "context"
                            },
                            {
                                "content": "                    # then there is no need for the background color",
                                "type": "context"
                            },
                            {
                                "content": "                    return 0",
                                "type": "deleted"
                            },
                            {
                                "content": "                else:",
                                "type": "deleted"
                            },
                            {
                                "content": "                    \"cannot allocate more than 256 colors\",",
                                "type": "added"
                            },
                            {
                                "content": "                    # Ignore non-opaque WebP background",
                                "type": "added"
                            },
                            {
                                "content": "                    \"cannot add non-opaque RGBA color to RGB palette\",",
                                "type": "added"
                            },
                            {
                                "content": "                ):",
                                "type": "added"
                            },
                            {
                                "content": "                    raise",
                                "type": "context"
                            },
                            {
                                "content": "        else:",
                                "type": "added"
                            },
                            {
                                "content": "            background = info_background",
                                "type": "added"
                            },
                            {
                                "content": "    return background",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "src/PIL/GifImagePlugin.py",
                            "type": "function",
                            "name": "_get_background",
                            "content": "def _get_background(im, info_background):\n    background = 0\n    if info_background:\n        background = info_background\n        if isinstance(background, tuple):\n            # WebPImagePlugin stores an RGBA value in info[\"background\"]\n            # So it must be converted to the same format as GifImagePlugin's\n            # info[\"background\"] - a global color table index\n            try:\n                background = im.palette.getcolor(background, im)\n            except ValueError as e:\n                if str(e) == \"cannot allocate more than 256 colors\":\n                    # If all 256 colors are in use,\n                    # then there is no need for the background color\n                    return 0\n                else:\n                    raise\n    return background",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 886,
                            "end_lineno": 903,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "aab4adc441f151ca8b51a7769928d420ecd6df07^",
    "new_commit_hash": "aab4adc441f151ca8b51a7769928d420ecd6df07",
    "commit_message": "Merge pull request #6792 from radarhere/webp_gif\n\nResolves https://github.com/python-pillow/Pillow/issues/6791",
    "commit_date": "2022-12-21T21:09:23+02:00",
    "metadata": {}
}
{
    "repo_name": "pillow",
    "new_commit_hash": "aab4adc441f151ca8b51a7769928d420ecd6df07",
    "test_file_codes": [
        "import warnings\nfrom io import BytesIO\n\nimport pytest\n\nfrom PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, ImageSequence, features\n\nfrom .helper import (\n    assert_image_equal,\n    assert_image_equal_tofile,\n    assert_image_similar,\n    hopper,\n    is_pypy,\n    netpbm_available,\n)\n\n# sample gif stream\nTEST_GIF = \"Tests/images/hopper.gif\"\n\nwith open(TEST_GIF, \"rb\") as f:\n    data = f.read()\n\n\ndef test_sanity():\n    with Image.open(TEST_GIF) as im:\n        im.load()\n        assert im.mode == \"P\"\n        assert im.size == (128, 128)\n        assert im.format == \"GIF\"\n        assert im.info[\"version\"] == b\"GIF89a\"\n\n\n@pytest.mark.skipif(is_pypy(), reason=\"Requires CPython\")\ndef test_unclosed_file():\n    def open():\n        im = Image.open(TEST_GIF)\n        im.load()\n\n    pytest.warns(ResourceWarning, open)\n\n\ndef test_closed_file():\n    with warnings.catch_warnings():\n        im = Image.open(TEST_GIF)\n        im.load()\n        im.close()\n\n\ndef test_seek_after_close():\n    im = Image.open(\"Tests/images/iss634.gif\")\n    im.load()\n    im.close()\n\n    with pytest.raises(ValueError):\n        im.is_animated\n    with pytest.raises(ValueError):\n        im.n_frames\n    with pytest.raises(ValueError):\n        im.seek(1)\n\n\ndef test_context_manager():\n    with warnings.catch_warnings():\n        with Image.open(TEST_GIF) as im:\n            im.load()\n\n\ndef test_invalid_file():\n    invalid_file = \"Tests/images/flower.jpg\"\n\n    with pytest.raises(SyntaxError):\n        GifImagePlugin.GifImageFile(invalid_file)\n\n\ndef test_l_mode_transparency():\n    with Image.open(\"Tests/images/no_palette_with_transparency.gif\") as im:\n        assert im.mode == \"L\"\n        assert im.load()[0, 0] == 128\n        assert im.info[\"transparency\"] == 255\n\n        im.seek(1)\n        assert im.mode == \"L\"\n        assert im.load()[0, 0] == 128\n\n\ndef test_l_mode_after_rgb():\n    with Image.open(\"Tests/images/no_palette_after_rgb.gif\") as im:\n        im.seek(1)\n        assert im.mode == \"RGB\"\n\n        im.seek(2)\n        assert im.mode == \"RGB\"\n\n\ndef test_palette_not_needed_for_second_frame():\n    with Image.open(\"Tests/images/palette_not_needed_for_second_frame.gif\") as im:\n        im.seek(1)\n        assert_image_similar(im, hopper(\"L\").convert(\"RGB\"), 8)\n\n\ndef test_strategy():\n    with Image.open(\"Tests/images/iss634.gif\") as im:\n        expected_rgb_always = im.convert(\"RGB\")\n\n    with Image.open(\"Tests/images/chi.gif\") as im:\n        expected_rgb_always_rgba = im.convert(\"RGBA\")\n\n        im.seek(1)\n        expected_different = im.convert(\"RGB\")\n\n    try:\n        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS\n        with Image.open(\"Tests/images/iss634.gif\") as im:\n            assert im.mode == \"RGB\"\n            assert_image_equal(im, expected_rgb_always)\n\n        with Image.open(\"Tests/images/chi.gif\") as im:\n            assert im.mode == \"RGBA\"\n            assert_image_equal(im, expected_rgb_always_rgba)\n\n        GifImagePlugin.LOADING_STRATEGY = (\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY\n        )\n        # Stay in P mode with only a global palette\n        with Image.open(\"Tests/images/chi.gif\") as im:\n            assert im.mode == \"P\"\n\n            im.seek(1)\n            assert im.mode == \"P\"\n            assert_image_equal(im.convert(\"RGB\"), expected_different)\n\n        # Change to RGB mode when a frame has an individual palette\n        with Image.open(\"Tests/images/iss634.gif\") as im:\n            assert im.mode == \"P\"\n\n            im.seek(1)\n            assert im.mode == \"RGB\"\n    finally:\n        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST\n\n\ndef test_optimize():\n    def test_grayscale(optimize):\n        im = Image.new(\"L\", (1, 1), 0)\n        filename = BytesIO()\n        im.save(filename, \"GIF\", optimize=optimize)\n        return len(filename.getvalue())\n\n    def test_bilevel(optimize):\n        im = Image.new(\"1\", (1, 1), 0)\n        test_file = BytesIO()\n        im.save(test_file, \"GIF\", optimize=optimize)\n        return len(test_file.getvalue())\n\n    assert test_grayscale(0) == 799\n    assert test_grayscale(1) == 43\n    assert test_bilevel(0) == 799\n    assert test_bilevel(1) == 799\n\n\ndef test_optimize_correctness():\n    # 256 color Palette image, posterize to > 128 and < 128 levels\n    # Size bigger and smaller than 512x512\n    # Check the palette for number of colors allocated.\n    # Check for correctness after conversion back to RGB\n    def check(colors, size, expected_palette_length):\n        # make an image with empty colors in the start of the palette range\n        im = Image.frombytes(\n            \"P\", (colors, colors), bytes(range(256 - colors, 256)) * colors\n        )\n        im = im.resize((size, size))\n        outfile = BytesIO()\n        im.save(outfile, \"GIF\")\n        outfile.seek(0)\n        with Image.open(outfile) as reloaded:\n            # check palette length\n            palette_length = max(i + 1 for i, v in enumerate(reloaded.histogram()) if v)\n            assert expected_palette_length == palette_length\n\n            assert_image_equal(im.convert(\"RGB\"), reloaded.convert(\"RGB\"))\n\n    # These do optimize the palette\n    check(256, 511, 256)\n    check(255, 511, 255)\n    check(129, 511, 129)\n    check(128, 511, 128)\n    check(64, 511, 64)\n    check(4, 511, 4)\n\n    # These don't optimize the palette\n    check(128, 513, 256)\n    check(64, 513, 256)\n    check(4, 513, 256)\n\n\ndef test_optimize_full_l():\n    im = Image.frombytes(\"L\", (16, 16), bytes(range(256)))\n    test_file = BytesIO()\n    im.save(test_file, \"GIF\", optimize=True)\n    assert im.mode == \"L\"\n\n\ndef test_optimize_if_palette_can_be_reduced_by_half():\n    with Image.open(\"Tests/images/test.colors.gif\") as im:\n        # Reduce dimensions because original is too big for _get_optimize()\n        im = im.resize((591, 443))\n    im_rgb = im.convert(\"RGB\")\n\n    for (optimize, colors) in ((False, 256), (True, 8)):\n        out = BytesIO()\n        im_rgb.save(out, \"GIF\", optimize=optimize)\n        with Image.open(out) as reloaded:\n            assert len(reloaded.palette.palette) // 3 == colors\n\n\ndef test_roundtrip(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im = hopper()\n    im.save(out)\n    with Image.open(out) as reread:\n\n        assert_image_similar(reread.convert(\"RGB\"), im, 50)\n\n\ndef test_roundtrip2(tmp_path):\n    # see https://github.com/python-pillow/Pillow/issues/403\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(TEST_GIF) as im:\n        im2 = im.copy()\n        im2.save(out)\n    with Image.open(out) as reread:\n\n        assert_image_similar(reread.convert(\"RGB\"), hopper(), 50)\n\n\ndef test_roundtrip_save_all(tmp_path):\n    # Single frame image\n    out = str(tmp_path / \"temp.gif\")\n    im = hopper()\n    im.save(out, save_all=True)\n    with Image.open(out) as reread:\n\n        assert_image_similar(reread.convert(\"RGB\"), im, 50)\n\n    # Multiframe image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\n        out = str(tmp_path / \"temp.gif\")\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 5\n\n\n@pytest.mark.parametrize(\n    \"path, mode\",\n    (\n        (\"Tests/images/dispose_bgnd.gif\", \"RGB\"),\n        # Hexeditted copy of dispose_bgnd to add transparency\n        (\"Tests/images/dispose_bgnd_rgba.gif\", \"RGBA\"),\n    ),\n)\ndef test_loading_multiple_palettes(path, mode):\n    with Image.open(path) as im:\n        assert im.mode == \"P\"\n        first_frame_colors = im.palette.colors.keys()\n        original_color = im.convert(\"RGB\").load()[0, 0]\n\n        im.seek(1)\n        assert im.mode == mode\n        if mode == \"RGBA\":\n            im = im.convert(\"RGB\")\n\n        # Check a color only from the old palette\n        assert im.load()[0, 0] == original_color\n\n        # Check a color from the new palette\n        assert im.load()[24, 24] not in first_frame_colors\n\n\ndef test_headers_saving_for_animated_gifs(tmp_path):\n    important_headers = [\"background\", \"version\", \"duration\", \"loop\"]\n    # Multiframe image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\n\n        info = im.info.copy()\n\n        out = str(tmp_path / \"temp.gif\")\n        im.save(out, save_all=True)\n    with Image.open(out) as reread:\n\n        for header in important_headers:\n            assert info[header] == reread.info[header]\n\n\ndef test_palette_handling(tmp_path):\n    # see https://github.com/python-pillow/Pillow/issues/513\n\n    with Image.open(TEST_GIF) as im:\n        im = im.convert(\"RGB\")\n\n        im = im.resize((100, 100), Image.Resampling.LANCZOS)\n        im2 = im.convert(\"P\", palette=Image.Palette.ADAPTIVE, colors=256)\n\n        f = str(tmp_path / \"temp.gif\")\n        im2.save(f, optimize=True)\n\n    with Image.open(f) as reloaded:\n\n        assert_image_similar(im, reloaded.convert(\"RGB\"), 10)\n\n\ndef test_palette_434(tmp_path):\n    # see https://github.com/python-pillow/Pillow/issues/434\n\n    def roundtrip(im, *args, **kwargs):\n        out = str(tmp_path / \"temp.gif\")\n        im.copy().save(out, *args, **kwargs)\n        reloaded = Image.open(out)\n\n        return reloaded\n\n    orig = \"Tests/images/test.colors.gif\"\n    with Image.open(orig) as im:\n\n        with roundtrip(im) as reloaded:\n            assert_image_similar(im, reloaded, 1)\n        with roundtrip(im, optimize=True) as reloaded:\n            assert_image_similar(im, reloaded, 1)\n\n        im = im.convert(\"RGB\")\n        # check automatic P conversion\n        with roundtrip(im) as reloaded:\n            reloaded = reloaded.convert(\"RGB\")\n            assert_image_equal(im, reloaded)\n\n\n@pytest.mark.skipif(not netpbm_available(), reason=\"Netpbm not available\")\ndef test_save_netpbm_bmp_mode(tmp_path):\n    with Image.open(TEST_GIF) as img:\n        img = img.convert(\"RGB\")\n\n        tempfile = str(tmp_path / \"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        with Image.open(tempfile) as reloaded:\n            assert_image_similar(img, reloaded.convert(\"RGB\"), 0)\n\n\n@pytest.mark.skipif(not netpbm_available(), reason=\"Netpbm not available\")\ndef test_save_netpbm_l_mode(tmp_path):\n    with Image.open(TEST_GIF) as img:\n        img = img.convert(\"L\")\n\n        tempfile = str(tmp_path / \"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        with Image.open(tempfile) as reloaded:\n            assert_image_similar(img, reloaded.convert(\"L\"), 0)\n\n\ndef test_seek():\n    with Image.open(\"Tests/images/dispose_none.gif\") as img:\n        frame_count = 0\n        try:\n            while True:\n                frame_count += 1\n                img.seek(img.tell() + 1)\n        except EOFError:\n            assert frame_count == 5\n\n\ndef test_seek_info():\n    with Image.open(\"Tests/images/iss634.gif\") as im:\n        info = im.info.copy()\n\n        im.seek(1)\n        im.seek(0)\n\n        assert im.info == info\n\n\ndef test_seek_rewind():\n    with Image.open(\"Tests/images/iss634.gif\") as im:\n        im.seek(2)\n        im.seek(1)\n\n        with Image.open(\"Tests/images/iss634.gif\") as expected:\n            expected.seek(1)\n            assert_image_equal(im, expected)\n\n\n@pytest.mark.parametrize(\n    \"path, n_frames\",\n    (\n        (TEST_GIF, 1),\n        (\"Tests/images/comment_after_last_frame.gif\", 2),\n        (\"Tests/images/iss634.gif\", 42),\n    ),\n)\ndef test_n_frames(path, n_frames):\n    # Test is_animated before n_frames\n    with Image.open(path) as im:\n        assert im.is_animated == (n_frames != 1)\n\n    # Test is_animated after n_frames\n    with Image.open(path) as im:\n        assert im.n_frames == n_frames\n        assert im.is_animated == (n_frames != 1)\n\n\ndef test_no_change():\n    # Test n_frames does not change the image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\n        im.seek(1)\n        expected = im.copy()\n        assert im.n_frames == 5\n        assert_image_equal(im, expected)\n\n    # Test is_animated does not change the image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\n        im.seek(3)\n        expected = im.copy()\n        assert im.is_animated\n        assert_image_equal(im, expected)\n\n    with Image.open(\"Tests/images/comment_after_only_frame.gif\") as im:\n        expected = Image.new(\"P\", (1, 1))\n        assert not im.is_animated\n        assert_image_equal(im, expected)\n\n\ndef test_eoferror():\n    with Image.open(TEST_GIF) as im:\n        n_frames = im.n_frames\n\n        # Test seeking past the last frame\n        with pytest.raises(EOFError):\n            im.seek(n_frames)\n        assert im.tell() < n_frames\n\n        # Test that seeking to the last frame does not raise an error\n        im.seek(n_frames - 1)\n\n\ndef test_first_frame_transparency():\n    with Image.open(\"Tests/images/first_frame_transparency.gif\") as im:\n        px = im.load()\n        assert px[0, 0] == im.info[\"transparency\"]\n\n\ndef test_dispose_none():\n    with Image.open(\"Tests/images/dispose_none.gif\") as img:\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                assert img.disposal_method == 1\n        except EOFError:\n            pass\n\n\ndef test_dispose_none_load_end():\n    # Test image created with:\n    #\n    # im = Image.open(\"transparent.gif\")\n    # im_rotated = im.rotate(180)\n    # im.save(\"dispose_none_load_end.gif\",\n    #         save_all=True, append_images=[im_rotated], disposal=[1,2])\n    with Image.open(\"Tests/images/dispose_none_load_end.gif\") as img:\n        img.seek(1)\n\n        assert_image_equal_tofile(img, \"Tests/images/dispose_none_load_end_second.png\")\n\n\ndef test_dispose_background():\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as img:\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                assert img.disposal_method == 2\n        except EOFError:\n            pass\n\n\ndef test_dispose_background_transparency():\n    with Image.open(\"Tests/images/dispose_bgnd_transparency.gif\") as img:\n        img.seek(2)\n        px = img.load()\n        assert px[35, 30][3] == 0\n\n\n@pytest.mark.parametrize(\n    \"loading_strategy, expected_colors\",\n    (\n        (\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,\n            (\n                (2, 1, 2),\n                ((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),\n                ((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),\n            ),\n        ),\n        (\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,\n            (\n                (2, 1, 2),\n                (0, 1, 0),\n                (2, 1, 2),\n            ),\n        ),\n    ),\n)\ndef test_transparent_dispose(loading_strategy, expected_colors):\n    GifImagePlugin.LOADING_STRATEGY = loading_strategy\n    try:\n        with Image.open(\"Tests/images/transparent_dispose.gif\") as img:\n            for frame in range(3):\n                img.seek(frame)\n                for x in range(3):\n                    color = img.getpixel((x, 0))\n                    assert color == expected_colors[frame][x]\n    finally:\n        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST\n\n\ndef test_dispose_previous():\n    with Image.open(\"Tests/images/dispose_prev.gif\") as img:\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                assert img.disposal_method == 3\n        except EOFError:\n            pass\n\n\ndef test_dispose_previous_first_frame():\n    with Image.open(\"Tests/images/dispose_prev_first_frame.gif\") as im:\n        im.seek(1)\n        assert_image_equal_tofile(\n            im, \"Tests/images/dispose_prev_first_frame_seeked.png\"\n        )\n\n\ndef test_previous_frame_loaded():\n    with Image.open(\"Tests/images/dispose_none.gif\") as img:\n        img.load()\n        img.seek(1)\n        img.load()\n        img.seek(2)\n        with Image.open(\"Tests/images/dispose_none.gif\") as img_skipped:\n            img_skipped.seek(2)\n            assert_image_equal(img_skipped, img)\n\n\ndef test_save_dispose(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im_list = [\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#111\"),\n        Image.new(\"L\", (100, 100), \"#222\"),\n    ]\n    for method in range(0, 4):\n        im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method)\n        with Image.open(out) as img:\n            for _ in range(2):\n                img.seek(img.tell() + 1)\n                assert img.disposal_method == method\n\n    # Check per frame disposal\n    im_list[0].save(\n        out,\n        save_all=True,\n        append_images=im_list[1:],\n        disposal=tuple(range(len(im_list))),\n    )\n\n    with Image.open(out) as img:\n\n        for i in range(2):\n            img.seek(img.tell() + 1)\n            assert img.disposal_method == i + 1\n\n\ndef test_dispose2_palette(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # Four colors: white, grey, black, red\n    circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]\n\n    im_list = []\n    for circle in circles:\n        # Red background\n        img = Image.new(\"RGB\", (100, 100), (255, 0, 0))\n\n        # Circle in center of each frame\n        d = ImageDraw.Draw(img)\n        d.ellipse([(40, 40), (60, 60)], fill=circle)\n\n        im_list.append(img)\n\n    im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)\n\n    with Image.open(out) as img:\n        for i, circle in enumerate(circles):\n            img.seek(i)\n            rgb_img = img.convert(\"RGB\")\n\n            # Check top left pixel matches background\n            assert rgb_img.getpixel((0, 0)) == (255, 0, 0)\n\n            # Center remains red every frame\n            assert rgb_img.getpixel((50, 50)) == circle\n\n\ndef test_dispose2_diff(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # 4 frames: red/blue, red/red, blue/blue, red/blue\n    circles = [\n        ((255, 0, 0, 255), (0, 0, 255, 255)),\n        ((255, 0, 0, 255), (255, 0, 0, 255)),\n        ((0, 0, 255, 255), (0, 0, 255, 255)),\n        ((255, 0, 0, 255), (0, 0, 255, 255)),\n    ]\n\n    im_list = []\n    for i in range(len(circles)):\n        # Transparent BG\n        img = Image.new(\"RGBA\", (100, 100), (255, 255, 255, 0))\n\n        # Two circles per frame\n        d = ImageDraw.Draw(img)\n        d.ellipse([(0, 30), (40, 70)], fill=circles[i][0])\n        d.ellipse([(60, 30), (100, 70)], fill=circles[i][1])\n\n        im_list.append(img)\n\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0\n    )\n\n    with Image.open(out) as img:\n        for i, colours in enumerate(circles):\n            img.seek(i)\n            rgb_img = img.convert(\"RGBA\")\n\n            # Check left circle is correct colour\n            assert rgb_img.getpixel((20, 50)) == colours[0]\n\n            # Check right circle is correct colour\n            assert rgb_img.getpixel((80, 50)) == colours[1]\n\n            # Check BG is correct colour\n            assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0)\n\n\ndef test_dispose2_background(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im_list = []\n\n    im = Image.new(\"P\", (100, 100))\n    d = ImageDraw.Draw(im)\n    d.rectangle([(50, 0), (100, 100)], fill=\"#f00\")\n    d.rectangle([(0, 0), (50, 100)], fill=\"#0f0\")\n    im_list.append(im)\n\n    im = Image.new(\"P\", (100, 100))\n    d = ImageDraw.Draw(im)\n    d.rectangle([(0, 0), (100, 50)], fill=\"#f00\")\n    d.rectangle([(0, 50), (100, 100)], fill=\"#0f0\")\n    im_list.append(im)\n\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1\n    )\n\n    with Image.open(out) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (255, 0, 0)\n\n\ndef test_transparency_in_second_frame(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/different_transparency.gif\") as im:\n        assert im.info[\"transparency\"] == 0\n\n        # Seek to the second frame\n        im.seek(im.tell() + 1)\n        assert \"transparency\" not in im.info\n\n        assert_image_equal_tofile(im, \"Tests/images/different_transparency_merged.png\")\n\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reread:\n        reread.seek(reread.tell() + 1)\n        assert_image_equal_tofile(\n            reread, \"Tests/images/different_transparency_merged.png\"\n        )\n\n\ndef test_no_transparency_in_second_frame():\n    with Image.open(\"Tests/images/iss634.gif\") as img:\n        # Seek to the second frame\n        img.seek(img.tell() + 1)\n        assert \"transparency\" not in img.info\n\n        # All transparent pixels should be replaced with the color from the first frame\n        assert img.histogram()[255] == 0\n\n\ndef test_remapped_transparency(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"P\", (1, 2))\n    im2 = im.copy()\n\n    # Add transparency at a higher index\n    # so that it will be optimized to a lower index\n    im.putpixel((0, 1), 5)\n    im.info[\"transparency\"] = 5\n    im.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reloaded:\n        assert reloaded.info[\"transparency\"] == reloaded.getpixel((0, 1))\n\n\ndef test_duration(tmp_path):\n    duration = 1000\n\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n\n    # Check that the argument has priority over the info settings\n    im.info[\"duration\"] = 100\n    im.save(out, duration=duration)\n\n    with Image.open(out) as reread:\n        assert reread.info[\"duration\"] == duration\n\n\ndef test_multiple_duration(tmp_path):\n    duration_list = [1000, 2000, 3000]\n\n    out = str(tmp_path / \"temp.gif\")\n    im_list = [\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#111\"),\n        Image.new(\"L\", (100, 100), \"#222\"),\n    ]\n\n    # Duration as list\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], duration=duration_list\n    )\n    with Image.open(out) as reread:\n\n        for duration in duration_list:\n            assert reread.info[\"duration\"] == duration\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n    # Duration as tuple\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list)\n    )\n    with Image.open(out) as reread:\n\n        for duration in duration_list:\n            assert reread.info[\"duration\"] == duration\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n\ndef test_roundtrip_info_duration(tmp_path):\n    duration_list = [100, 500, 500]\n\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/transparent_dispose.gif\") as im:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(im)\n        ] == duration_list\n\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reloaded:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(reloaded)\n        ] == duration_list\n\n\ndef test_roundtrip_info_duration_combined(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/duplicate_frame.gif\") as im:\n        assert [frame.info[\"duration\"] for frame in ImageSequence.Iterator(im)] == [\n            1000,\n            1000,\n            1000,\n        ]\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reloaded:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(reloaded)\n        ] == [1000, 2000]\n\n\ndef test_identical_frames(tmp_path):\n    duration_list = [1000, 1500, 2000, 4000]\n\n    out = str(tmp_path / \"temp.gif\")\n    im_list = [\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#111\"),\n    ]\n\n    # Duration as list\n    im_list[0].save(\n        out, save_all=True, append_images=im_list[1:], duration=duration_list\n    )\n    with Image.open(out) as reread:\n\n        # Assert that the first three frames were combined\n        assert reread.n_frames == 2\n\n        # Assert that the new duration is the total of the identical frames\n        assert reread.info[\"duration\"] == 4500\n\n\n@pytest.mark.parametrize(\n    \"duration\", ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500)\n)\ndef test_identical_frames_to_single_frame(duration, tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im_list = [\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#000\"),\n        Image.new(\"L\", (100, 100), \"#000\"),\n    ]\n\n    im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration)\n    with Image.open(out) as reread:\n        # Assert that all frames were combined\n        assert reread.n_frames == 1\n\n        # Assert that the new duration is the total of the identical frames\n        assert reread.info[\"duration\"] == 8500\n\n\ndef test_number_of_loops(tmp_path):\n    number_of_loops = 2\n\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.save(out, loop=number_of_loops)\n    with Image.open(out) as reread:\n        assert reread.info[\"loop\"] == number_of_loops\n\n    # Check that even if a subsequent GIF frame has the number of loops specified,\n    # only the value from the first frame is used\n    with Image.open(\"Tests/images/duplicate_number_of_loops.gif\") as im:\n        assert im.info[\"loop\"] == 2\n\n        im.seek(1)\n        assert im.info[\"loop\"] == 2\n\n\ndef test_background(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"background\"] = 1\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"background\"] == im.info[\"background\"]\n\n\ndef test_webp_background(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # Test opaque WebP background\n    if features.check(\"webp\") and features.check(\"webp_anim\"):\n        with Image.open(\"Tests/images/hopper.webp\") as im:\n            assert im.info[\"background\"] == (255, 255, 255, 255)\n            im.save(out)\n\n    # Test non-opaque WebP background\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"background\"] = (0, 0, 0, 0)\n    im.save(out)\n\n\ndef test_comment(tmp_path):\n    with Image.open(TEST_GIF) as im:\n        assert im.info[\"comment\"] == b\"File written by Adobe Photoshop\\xa8 4.0\"\n\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"comment\"] = b\"Test comment text\"\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"comment\"] == im.info[\"comment\"]\n\n    im.info[\"comment\"] = \"Test comment text\"\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"comment\"] == im.info[\"comment\"].encode()\n\n        # Test that GIF89a is used for comments\n        assert reread.info[\"version\"] == b\"GIF89a\"\n\n\ndef test_comment_over_255(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    comment = b\"Test comment text\"\n    while len(comment) < 256:\n        comment += comment\n    im.info[\"comment\"] = comment\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"comment\"] == comment\n\n        # Test that GIF89a is used for comments\n        assert reread.info[\"version\"] == b\"GIF89a\"\n\n\ndef test_zero_comment_subblocks():\n    with Image.open(\"Tests/images/hopper_zero_comment_subblocks.gif\") as im:\n        assert_image_equal_tofile(im, TEST_GIF)\n\n\ndef test_read_multiple_comment_blocks():\n    with Image.open(\"Tests/images/multiple_comments.gif\") as im:\n        # Multiple comment blocks in a frame are separated not concatenated\n        assert im.info[\"comment\"] == b\"Test comment 1\\nTest comment 2\"\n\n\ndef test_empty_string_comment(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/chi.gif\") as im:\n        assert \"comment\" in im.info\n\n        # Empty string comment should suppress existing comment\n        im.save(out, save_all=True, comment=\"\")\n\n    with Image.open(out) as reread:\n        for frame in ImageSequence.Iterator(reread):\n            assert \"comment\" not in frame.info\n\n\ndef test_retain_comment_in_subsequent_frames(tmp_path):\n    # Test that a comment block at the beginning is kept\n    with Image.open(\"Tests/images/chi.gif\") as im:\n        for frame in ImageSequence.Iterator(im):\n            assert frame.info[\"comment\"] == b\"Created with GIMP\"\n\n    with Image.open(\"Tests/images/second_frame_comment.gif\") as im:\n        assert \"comment\" not in im.info\n\n        # Test that a comment in the middle is read\n        im.seek(1)\n        assert im.info[\"comment\"] == b\"Comment in the second frame\"\n\n        # Test that it is still present in a later frame\n        im.seek(2)\n        assert im.info[\"comment\"] == b\"Comment in the second frame\"\n\n        # Test that rewinding removes the comment\n        im.seek(0)\n        assert \"comment\" not in im.info\n\n    # Test that a saved image keeps the comment\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/dispose_prev.gif\") as im:\n        im.save(out, save_all=True, comment=\"Test\")\n\n    with Image.open(out) as reread:\n        for frame in ImageSequence.Iterator(reread):\n            assert frame.info[\"comment\"] == b\"Test\"\n\n\ndef test_version(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    def assert_version_after_save(im, version):\n        im.save(out)\n        with Image.open(out) as reread:\n            assert reread.info[\"version\"] == version\n\n    # Test that GIF87a is used by default\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    assert_version_after_save(im, b\"GIF87a\")\n\n    # Test setting the version to 89a\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"version\"] = b\"89a\"\n    assert_version_after_save(im, b\"GIF89a\")\n\n    # Test that adding a GIF89a feature changes the version\n    im.info[\"transparency\"] = 1\n    assert_version_after_save(im, b\"GIF89a\")\n\n    # Test that a GIF87a image is also saved in that format\n    with Image.open(\"Tests/images/test.colors.gif\") as im:\n        assert_version_after_save(im, b\"GIF87a\")\n\n        # Test that a GIF89a image is also saved in that format\n        im.info[\"version\"] = b\"GIF89a\"\n        assert_version_after_save(im, b\"GIF87a\")\n\n\ndef test_append_images(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # Test appending single frame images\n    im = Image.new(\"RGB\", (100, 100), \"#f00\")\n    ims = [Image.new(\"RGB\", (100, 100), color) for color in [\"#0f0\", \"#00f\"]]\n    im.copy().save(out, save_all=True, append_images=ims)\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 3\n\n    # Tests appending using a generator\n    def im_generator(ims):\n        yield from ims\n\n    im.save(out, save_all=True, append_images=im_generator(ims))\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 3\n\n    # Tests appending single and multiple frame images\n    with Image.open(\"Tests/images/dispose_none.gif\") as im:\n        with Image.open(\"Tests/images/dispose_prev.gif\") as im2:\n            im.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 10\n\n\ndef test_transparent_optimize(tmp_path):\n    # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses\n    # transparency.\n    # Need a palette that isn't using the 0 color,\n    # where the transparent color is actually the top palette entry to trigger the bug.\n\n    data = bytes(range(1, 254))\n    palette = ImagePalette.ImagePalette(\"RGB\", list(range(256)) * 3)\n\n    im = Image.new(\"L\", (253, 1))\n    im.frombytes(data)\n    im.putpalette(palette)\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, transparency=im.getpixel((252, 0)))\n\n    with Image.open(out) as reloaded:\n        assert reloaded.info[\"transparency\"] == reloaded.getpixel((252, 0))\n\n\ndef test_rgb_transparency(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    # Single frame\n    im = Image.new(\"RGB\", (1, 1))\n    im.info[\"transparency\"] = (255, 0, 0)\n    im.save(out)\n\n    with Image.open(out) as reloaded:\n        assert \"transparency\" in reloaded.info\n\n    # Multiple frames\n    im = Image.new(\"RGB\", (1, 1))\n    im.info[\"transparency\"] = b\"\"\n    ims = [Image.new(\"RGB\", (1, 1))]\n    pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims)\n\n    with Image.open(out) as reloaded:\n        assert \"transparency\" not in reloaded.info\n\n\ndef test_rgba_transparency(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im = hopper(\"P\")\n    im.save(out, save_all=True, append_images=[Image.new(\"RGBA\", im.size)])\n\n    with Image.open(out) as reloaded:\n        reloaded.seek(1)\n        assert_image_equal(hopper(\"P\").convert(\"RGB\"), reloaded)\n\n\ndef test_bbox(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"RGB\", (100, 100), \"#fff\")\n    ims = [Image.new(\"RGB\", (100, 100), \"#000\")]\n    im.save(out, save_all=True, append_images=ims)\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 2\n\n\ndef test_palette_save_L(tmp_path):\n    # Generate an L mode image with a separate palette\n\n    im = hopper(\"P\")\n    im_l = Image.frombytes(\"L\", im.size, im.tobytes())\n    palette = bytes(im.getpalette())\n\n    out = str(tmp_path / \"temp.gif\")\n    im_l.save(out, palette=palette)\n\n    with Image.open(out) as reloaded:\n        assert_image_equal(reloaded.convert(\"RGB\"), im.convert(\"RGB\"))\n\n\ndef test_palette_save_P(tmp_path):\n    # Pass in a different palette, then construct what the image would look like.\n    # Forcing a non-straight grayscale palette.\n\n    im = hopper(\"P\")\n    palette = bytes(255 - i // 3 for i in range(768))\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, palette=palette)\n\n    with Image.open(out) as reloaded:\n        im.putpalette(palette)\n        assert_image_equal(reloaded, im)\n\n\ndef test_palette_save_duplicate_entries(tmp_path):\n    im = Image.new(\"P\", (1, 2))\n    im.putpixel((0, 1), 1)\n\n    im.putpalette((0, 0, 0, 0, 0, 0))\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1])\n\n    with Image.open(out) as reloaded:\n        assert reloaded.convert(\"RGB\").getpixel((0, 1)) == (0, 0, 0)\n\n\ndef test_palette_save_all_P(tmp_path):\n    frames = []\n    colors = ((255, 0, 0), (0, 255, 0))\n    for color in colors:\n        frame = Image.new(\"P\", (100, 100))\n        frame.putpalette(color)\n        frames.append(frame)\n\n    out = str(tmp_path / \"temp.gif\")\n    frames[0].save(\n        out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:]\n    )\n\n    with Image.open(out) as im:\n        # Assert that the frames are correct, and each frame has the same palette\n        assert_image_equal(im.convert(\"RGB\"), frames[0].convert(\"RGB\"))\n        assert im.palette.palette == im.global_palette.palette\n\n        im.seek(1)\n        assert_image_equal(im.convert(\"RGB\"), frames[1].convert(\"RGB\"))\n        assert im.palette.palette == im.global_palette.palette\n\n\ndef test_palette_save_ImagePalette(tmp_path):\n    # Pass in a different palette, as an ImagePalette.ImagePalette\n    # effectively the same as test_palette_save_P\n\n    im = hopper(\"P\")\n    palette = ImagePalette.ImagePalette(\"RGB\", list(range(256))[::-1] * 3)\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, palette=palette)\n\n    with Image.open(out) as reloaded:\n        im.putpalette(palette)\n        assert_image_equal(reloaded.convert(\"RGB\"), im.convert(\"RGB\"))\n\n\ndef test_save_I(tmp_path):\n    # Test saving something that would trigger the auto-convert to 'L'\n\n    im = hopper(\"I\")\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out)\n\n    with Image.open(out) as reloaded:\n        assert_image_equal(reloaded.convert(\"L\"), im.convert(\"L\"))\n\n\ndef test_getdata():\n    # Test getheader/getdata against legacy values.\n    # Create a 'P' image with holes in the palette.\n    im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)\n    im.putpalette(ImagePalette.ImagePalette(\"RGB\"))\n    im.info = {\"background\": 0}\n\n    passed_palette = bytes(255 - i // 3 for i in range(768))\n\n    GifImagePlugin._FORCE_OPTIMIZE = True\n    try:\n        h = GifImagePlugin.getheader(im, passed_palette)\n        d = GifImagePlugin.getdata(im)\n\n        import pickle\n\n        # Enable to get target values on pre-refactor version\n        # with open('Tests/images/gif_header_data.pkl', 'wb') as f:\n        #    pickle.dump((h, d), f, 1)\n        with open(\"Tests/images/gif_header_data.pkl\", \"rb\") as f:\n            (h_target, d_target) = pickle.load(f)\n\n        assert h == h_target\n        assert d == d_target\n    finally:\n        GifImagePlugin._FORCE_OPTIMIZE = False\n\n\ndef test_lzw_bits():\n    # see https://github.com/python-pillow/Pillow/issues/2811\n    with Image.open(\"Tests/images/issue_2811.gif\") as im:\n        assert im.tile[0][3][0] == 11  # LZW bits\n        # codec error prepatch\n        im.load()\n\n\ndef test_extents():\n    with Image.open(\"Tests/images/test_extents.gif\") as im:\n        assert im.size == (100, 100)\n\n        # Check that n_frames does not change the size\n        assert im.n_frames == 2\n        assert im.size == (100, 100)\n\n        im.seek(1)\n        assert im.size == (150, 150)\n\n\ndef test_missing_background():\n    # The Global Color Table Flag isn't set, so there is no background color index,\n    # but the disposal method is \"Restore to background color\"\n    with Image.open(\"Tests/images/missing_background.gif\") as im:\n        im.seek(1)\n        assert_image_equal_tofile(im, \"Tests/images/missing_background_first_frame.png\")\n\n\ndef test_saving_rgba(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/transparent.png\") as im:\n        im.save(out)\n\n    with Image.open(out) as reloaded:\n        reloaded_rgba = reloaded.convert(\"RGBA\")\n        assert reloaded_rgba.load()[0, 0][3] == 0\n"
    ],
    "test_file_names": [
        "test_1.py"
    ],
    "setup_res_code": 0,
    "setup_res_stdout": "Starting Pillow installation attempts...\nVerifying Pillow installation...\n✅ Pillow installation successful!\nSuccessfully installed Pillow\n",
    "setup_res_stderr": "Using CPython 3.9.21\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 12 packages in 17ms\nInstalled 12 packages in 155ms\n + coverage==7.6.10\n + exceptiongroup==1.2.2\n + iniconfig==2.0.0\n + packaging==24.2\n + pluggy==1.5.0\n + pyqt5==5.15.11\n + pyqt5-qt5==5.15.16\n + pyqt5-sip==12.16.1\n + pytest==8.3.4\n + pytest-cov==6.0.0\n + setuptools==75.8.0\n + tomli==2.2.1\nResolved 1 package in 751ms\nPrepared 1 package in 34.09s\nInstalled 1 package in 0.98ms\n + pillow==9.4.0.dev0 (from file:///home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_aab4adc441f151ca8b51a7769928d420ecd6df07)\n",
    "new_commit_res_code": 0,
    "new_commit_res_stdout": "\u001b[1m============================= test session starts ==============================\u001b[0m\nplatform linux -- Python 3.9.21, pytest-8.3.4, pluggy-1.5.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_aab4adc441f151ca8b51a7769928d420ecd6df07\nconfigfile: setup.cfg\nplugins: cov-6.0.0\ncollected 83 items\n\nr2e_tests/test_1.py \u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[33ms\u001b[0m\u001b[33ms\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m [ 62%]\n\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m                                          [100%]\u001b[0m\n\n==================================== PASSES ====================================\n\u001b[36m\u001b[1m=========================== short test summary info ============================\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_sanity\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_unclosed_file\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_closed_file\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_seek_after_close\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_context_manager\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_invalid_file\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_l_mode_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_l_mode_after_rgb\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_not_needed_for_second_frame\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_strategy\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_full_l\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_if_palette_can_be_reduced_by_half\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_roundtrip\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_roundtrip2\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_roundtrip_save_all\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_loading_multiple_palettes[Tests/images/dispose_bgnd.gif-RGB]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_loading_multiple_palettes[Tests/images/dispose_bgnd_rgba.gif-RGBA]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_headers_saving_for_animated_gifs\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_handling\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_434\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_seek\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_seek_info\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_seek_rewind\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_n_frames[Tests/images/hopper.gif-1]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_n_frames[Tests/images/comment_after_last_frame.gif-2]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_n_frames[Tests/images/iss634.gif-42]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_no_change\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_eoferror\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_first_frame_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose_none\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose_none_load_end\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose_background\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose_background_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_transparent_dispose[LoadingStrategy.RGB_AFTER_FIRST-expected_colors0]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_transparent_dispose[LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY-expected_colors1]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose_previous\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose_previous_first_frame\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_previous_frame_loaded\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_save_dispose\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose2_palette\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose2_diff\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose2_background\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_transparency_in_second_frame\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_no_transparency_in_second_frame\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_remapped_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_duration\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_multiple_duration\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_roundtrip_info_duration\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_roundtrip_info_duration_combined\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[duration0]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[duration1]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[8500]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_number_of_loops\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_background\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_webp_background\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_comment\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_comment_over_255\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_zero_comment_subblocks\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_read_multiple_comment_blocks\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_empty_string_comment\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_retain_comment_in_subsequent_frames\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_version\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_append_images\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_transparent_optimize\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_rgb_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_rgba_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_bbox\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_save_L\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_save_P\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_save_duplicate_entries\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_save_all_P\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_save_ImagePalette\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_save_I\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_getdata\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_lzw_bits\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_extents\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_missing_background\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_saving_rgba\u001b[0m\n\u001b[33mSKIPPED\u001b[0m [1] r2e_tests/test_1.py:337: Netpbm not available\n\u001b[33mSKIPPED\u001b[0m [1] r2e_tests/test_1.py:348: Netpbm not available\n\u001b[32m======================== \u001b[32m\u001b[1m81 passed\u001b[0m, \u001b[33m2 skipped\u001b[0m\u001b[32m in 1.06s\u001b[0m\u001b[32m =========================\u001b[0m\n",
    "new_commit_res_stderr": "",
    "old_commit_res_code": 1,
    "old_commit_res_stdout": "\u001b[1m============================= test session starts ==============================\u001b[0m\nplatform linux -- Python 3.9.21, pytest-8.3.4, pluggy-1.5.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_aab4adc441f151ca8b51a7769928d420ecd6df07\nconfigfile: setup.cfg\nplugins: cov-6.0.0\ncollected 83 items\n\nr2e_tests/test_1.py \u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[33ms\u001b[0m\u001b[33ms\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m [ 62%]\n\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[31mF\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[31m                                          [100%]\u001b[0m\n\n=================================== FAILURES ===================================\n\u001b[31m\u001b[1m_____________________________ test_webp_background _____________________________\u001b[0m\n\ntmp_path = PosixPath('/tmp/pytest-of-gcpuser/pytest-510/test_webp_background0')\n\n    def test_webp_background(tmp_path):\n        out = str(tmp_path / \"temp.gif\")\n    \n        # Test opaque WebP background\n        if features.check(\"webp\") and features.check(\"webp_anim\"):\n            with Image.open(\"Tests/images/hopper.webp\") as im:\n                assert im.info[\"background\"] == (255, 255, 255, 255)\n                im.save(out)\n    \n        # Test non-opaque WebP background\n        im = Image.new(\"L\", (100, 100), \"#000\")\n        im.info[\"background\"] = (0, 0, 0, 0)\n>       im.save(out)\n\n\u001b[1m\u001b[31mr2e_tests/test_1.py\u001b[0m:893: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\u001b[1m\u001b[31msrc/PIL/Image.py\u001b[0m:2402: in save\n    save_handler(self, fp, filename)\n\u001b[1m\u001b[31msrc/PIL/GifImagePlugin.py\u001b[0m:665: in _save\n    _write_single_frame(im, fp, palette)\n\u001b[1m\u001b[31msrc/PIL/GifImagePlugin.py\u001b[0m:553: in _write_single_frame\n    for s in _get_global_header(im_out, im.encoderinfo):\n\u001b[1m\u001b[31msrc/PIL/GifImagePlugin.py\u001b[0m:924: in _get_global_header\n    background = _get_background(im, info.get(\"background\"))\n\u001b[1m\u001b[31msrc/PIL/GifImagePlugin.py\u001b[0m:895: in _get_background\n    background = im.palette.getcolor(background, im)\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <PIL.ImagePalette.ImagePalette object at 0x7e68717b8490>\ncolor = (0, 0, 0, 0)\nimage = <PIL.Image.Image image mode=P size=100x100 at 0x7E68717B8400>\n\n    def getcolor(self, color, image=None):\n        \"\"\"Given an rgb tuple, allocate palette entry.\n    \n        .. warning:: This method is experimental.\n        \"\"\"\n        if self.rawmode:\n            raise ValueError(\"palette contains raw palette data\")\n        if isinstance(color, tuple):\n            if self.mode == \"RGB\":\n                if len(color) == 4:\n                    if color[3] != 255:\n>                       raise ValueError(\n                            \"cannot add non-opaque RGBA color to RGB palette\"\n                        )\n\u001b[1m\u001b[31mE                       ValueError: cannot add non-opaque RGBA color to RGB palette\u001b[0m\n\n\u001b[1m\u001b[31msrc/PIL/ImagePalette.py\u001b[0m:120: ValueError\n==================================== PASSES ====================================\n\u001b[36m\u001b[1m=========================== short test summary info ============================\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_sanity\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_unclosed_file\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_closed_file\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_seek_after_close\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_context_manager\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_invalid_file\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_l_mode_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_l_mode_after_rgb\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_not_needed_for_second_frame\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_strategy\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_full_l\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_if_palette_can_be_reduced_by_half\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_roundtrip\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_roundtrip2\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_roundtrip_save_all\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_loading_multiple_palettes[Tests/images/dispose_bgnd.gif-RGB]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_loading_multiple_palettes[Tests/images/dispose_bgnd_rgba.gif-RGBA]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_headers_saving_for_animated_gifs\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_handling\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_434\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_seek\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_seek_info\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_seek_rewind\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_n_frames[Tests/images/hopper.gif-1]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_n_frames[Tests/images/comment_after_last_frame.gif-2]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_n_frames[Tests/images/iss634.gif-42]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_no_change\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_eoferror\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_first_frame_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose_none\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose_none_load_end\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose_background\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose_background_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_transparent_dispose[LoadingStrategy.RGB_AFTER_FIRST-expected_colors0]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_transparent_dispose[LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY-expected_colors1]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose_previous\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose_previous_first_frame\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_previous_frame_loaded\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_save_dispose\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose2_palette\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose2_diff\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_dispose2_background\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_transparency_in_second_frame\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_no_transparency_in_second_frame\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_remapped_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_duration\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_multiple_duration\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_roundtrip_info_duration\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_roundtrip_info_duration_combined\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[duration0]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[duration1]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[8500]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_number_of_loops\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_background\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_comment\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_comment_over_255\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_zero_comment_subblocks\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_read_multiple_comment_blocks\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_empty_string_comment\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_retain_comment_in_subsequent_frames\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_version\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_append_images\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_transparent_optimize\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_rgb_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_rgba_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_bbox\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_save_L\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_save_P\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_save_duplicate_entries\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_save_all_P\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_save_ImagePalette\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_save_I\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_getdata\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_lzw_bits\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_extents\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_missing_background\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_saving_rgba\u001b[0m\n\u001b[33mSKIPPED\u001b[0m [1] r2e_tests/test_1.py:337: Netpbm not available\n\u001b[33mSKIPPED\u001b[0m [1] r2e_tests/test_1.py:348: Netpbm not available\n\u001b[31mFAILED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_webp_background\u001b[0m - ValueError: cannot add non-opaque RGBA color to RGB palette\n\u001b[31m=================== \u001b[31m\u001b[1m1 failed\u001b[0m, \u001b[32m80 passed\u001b[0m, \u001b[33m2 skipped\u001b[0m\u001b[31m in 0.96s\u001b[0m\u001b[31m ====================\u001b[0m\n",
    "old_commit_res_stderr": ""
}
Tests/test_file_gif.py src/PIL/GifImagePlugin.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Saving GIF with Non-Opaque RGBA Background Raises ValueError

**Description:**
When attempting to save a GIF image with a non-opaque RGBA background, a `ValueError` is raised. This occurs because the GIF palette does not support non-opaque colors.

**Example Code:**
```python
from PIL import Image

# Create an image with a transparent background
image = Image.new("RGBA", (100, 100), (0, 0, 0, 0))
image.info["background"] = (0, 0, 0, 0)

# Attempt to save as GIF
image.save("temp.gif")
```

**Expected Behavior:**
The image should be saved as a GIF with a default background color or handle the transparency appropriately without raising an error.

**Actual Behavior:**
Saving the image results in the following error:
```
ValueError: cannot add non-opaque RGBA color to RGB palette
```
[/ISSUE]
--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.

--- BEGIN FILE ---
```
### src/PIL/GifImagePlugin.py
#
# The Python Imaging Library.
# $Id$
#
# GIF file handling
#
# History:
# 1995-09-01 fl   Created
# 1996-12-14 fl   Added interlace support
# 1996-12-30 fl   Added animation support
# 1997-01-05 fl   Added write support, fixed local colour map bug
# 1997-02-23 fl   Make sure to load raster data in getdata()
# 1997-07-05 fl   Support external decoder (0.4)
# 1998-07-09 fl   Handle all modes when saving (0.5)
# 1998-07-15 fl   Renamed offset attribute to avoid name clash
# 2001-04-16 fl   Added rewind support (seek to frame 0) (0.6)
# 2001-04-17 fl   Added palette optimization (0.7)
# 2002-06-06 fl   Added transparency support for save (0.8)
# 2004-02-24 fl   Disable interlacing for small images
#
# Copyright (c) 1997-2004 by Secret Labs AB
# Copyright (c) 1995-2004 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#

import itertools
import math
import os
import subprocess
from enum import IntEnum

from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16le as i16
from ._binary import o8
from ._binary import o16le as o16


class LoadingStrategy(IntEnum):
    """.. versionadded:: 9.1.0"""

    RGB_AFTER_FIRST = 0
    RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1
    RGB_ALWAYS = 2


#: .. versionadded:: 9.1.0
LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST

# --------------------------------------------------------------------
# Identify/read GIF files


def _accept(prefix):
    return prefix[:6] in [b"GIF87a", b"GIF89a"]


##
# Image plugin for GIF images.  This plugin supports both GIF87 and
# GIF89 images.


class GifImageFile(ImageFile.ImageFile):

    format = "GIF"
    format_description = "Compuserve GIF"
    _close_exclusive_fp_after_loading = False

    global_palette = None

    def data(self):
        s = self.fp.read(1)
        if s and s[0]:
            return self.fp.read(s[0])
        return None

    def _is_palette_needed(self, p):
        for i in range(0, len(p), 3):
            if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
                return True
        return False

    def _open(self):

        # Screen
        s = self.fp.read(13)
        if not _accept(s):
            raise SyntaxError("not a GIF file")

        self.info["version"] = s[:6]
        self._size = i16(s, 6), i16(s, 8)
        self.tile = []
        flags = s[10]
        bits = (flags & 7) + 1

        if flags & 128:
            # get global palette
            self.info["background"] = s[11]
            # check if palette contains colour indices
            p = self.fp.read(3 << bits)
            if self._is_palette_needed(p):
                p = ImagePalette.raw("RGB", p)
                self.global_palette = self.palette = p

        self._fp = self.fp  # FIXME: hack
        self.__rewind = self.fp.tell()
        self._n_frames = None
        self._is_animated = None
        self._seek(0)  # get ready to read first frame

    @property
    def n_frames(self):
        if self._n_frames is None:
            current = self.tell()
            try:
                while True:
                    self._seek(self.tell() + 1, False)
            except EOFError:
                self._n_frames = self.tell() + 1
            self.seek(current)
        return self._n_frames

    @property
    def is_animated(self):
        if self._is_animated is None:
            if self._n_frames is not None:
                self._is_animated = self._n_frames != 1
            else:
                current = self.tell()
                if current:
                    self._is_animated = True
                else:
                    try:
                        self._seek(1, False)
                        self._is_animated = True
                    except EOFError:
                        self._is_animated = False

                    self.seek(current)
        return self._is_animated

    def seek(self, frame):
        if not self._seek_check(frame):
            return
        if frame < self.__frame:
            self.im = None
            self._seek(0)

        last_frame = self.__frame
        for f in range(self.__frame + 1, frame + 1):
            try:
                self._seek(f)
            except EOFError as e:
                self.seek(last_frame)
                raise EOFError("no more images in GIF file") from e

    def _seek(self, frame, update_image=True):

        if frame == 0:
            # rewind
            self.__offset = 0
            self.dispose = None
            self.__frame = -1
            self._fp.seek(self.__rewind)
            self.disposal_method = 0
            if "comment" in self.info:
                del self.info["comment"]
        else:
            # ensure that the previous frame was loaded
            if self.tile and update_image:
                self.load()

        if frame != self.__frame + 1:
            raise ValueError(f"cannot seek to frame {frame}")

        self.fp = self._fp
        if self.__offset:
            # backup to last frame
            self.fp.seek(self.__offset)
            while self.data():
                pass
            self.__offset = 0

        s = self.fp.read(1)
        if not s or s == b";":
            raise EOFError

        palette = None

        info = {}
        frame_transparency = None
        interlace = None
        frame_dispose_extent = None
        while True:

            if not s:
                s = self.fp.read(1)
            if not s or s == b";":
                break

            elif s == b"!":
                #
                # extensions
                #
                s = self.fp.read(1)
                block = self.data()
                if s[0] == 249:
                    #
                    # graphic control extension
                    #
                    flags = block[0]
                    if flags & 1:
                        frame_transparency = block[3]
                    info["duration"] = i16(block, 1) * 10

                    # disposal method - find the value of bits 4 - 6
                    dispose_bits = 0b00011100 & flags
                    dispose_bits = dispose_bits >> 2
                    if dispose_bits:
                        # only set the dispose if it is not
                        # unspecified. I'm not sure if this is
                        # correct, but it seems to prevent the last
                        # frame from looking odd for some animations
                        self.disposal_method = dispose_bits
                elif s[0] == 254:
                    #
                    # comment extension
                    #
                    comment = b""

                    # Read this comment block
                    while block:
                        comment += block
                        block = self.data()

                    if "comment" in info:
                        # If multiple comment blocks in frame, separate with \n
                        info["comment"] += b"\n" + comment
                    else:
                        info["comment"] = comment
                    s = None
                    continue
                elif s[0] == 255 and frame == 0:
                    #
                    # application extension
                    #
                    info["extension"] = block, self.fp.tell()
                    if block[:11] == b"NETSCAPE2.0":
                        block = self.data()
                        if len(block) >= 3 and block[0] == 1:
                            self.info["loop"] = i16(block, 1)
                while self.data():
                    pass

            elif s == b",":
                #
                # local image
                #
                s = self.fp.read(9)

                # extent
                x0, y0 = i16(s, 0), i16(s, 2)
                x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
                if (x1 > self.size[0] or y1 > self.size[1]) and update_image:
                    self._size = max(x1, self.size[0]), max(y1, self.size[1])
                    Image._decompression_bomb_check(self._size)
                frame_dispose_extent = x0, y0, x1, y1
                flags = s[8]

                interlace = (flags & 64) != 0

                if flags & 128:
                    bits = (flags & 7) + 1
                    p = self.fp.read(3 << bits)
                    if self._is_palette_needed(p):
                        palette = ImagePalette.raw("RGB", p)
                    else:
                        palette = False

                # image data
                bits = self.fp.read(1)[0]
                self.__offset = self.fp.tell()
                break

            else:
                pass
                # raise OSError, "illegal GIF tag `%x`" % s[0]
            s = None

        if interlace is None:
            # self._fp = None
            raise EOFError

        self.__frame = frame
        if not update_image:
            return

        self.tile = []

        if self.dispose:
            self.im.paste(self.dispose, self.dispose_extent)

        self._frame_palette = palette if palette is not None else self.global_palette
        self._frame_transparency = frame_transparency
        if frame == 0:
            if self._frame_palette:
                if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
                    self.mode = "RGBA" if frame_transparency is not None else "RGB"
                else:
                    self.mode = "P"
            else:
                self.mode = "L"

            if not palette and self.global_palette:
                from copy import copy

                palette = copy(self.global_palette)
            self.palette = palette
        else:
            if self.mode == "P":
                if (
                    LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
                    or palette
                ):
                    self.pyaccess = None
                    if "transparency" in self.info:
                        self.im.putpalettealpha(self.info["transparency"], 0)
                        self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
                        self.mode = "RGBA"
                        del self.info["transparency"]
                    else:
                        self.mode = "RGB"
                        self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)

        def _rgb(color):
            if self._frame_palette:
                color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
            else:
                color = (color, color, color)
            return color

        self.dispose_extent = frame_dispose_extent
        try:
            if self.disposal_method < 2:
                # do not dispose or none specified
                self.dispose = None
            elif self.disposal_method == 2:
                # replace with background colour

                # only dispose the extent in this frame
                x0, y0, x1, y1 = self.dispose_extent
                dispose_size = (x1 - x0, y1 - y0)

                Image._decompression_bomb_check(dispose_size)

                # by convention, attempt to use transparency first
                dispose_mode = "P"
                color = self.info.get("transparency", frame_transparency)
                if color is not None:
                    if self.mode in ("RGB", "RGBA"):
                        dispose_mode = "RGBA"
                        color = _rgb(color) + (0,)
                else:
                    color = self.info.get("background", 0)
                    if self.mode in ("RGB", "RGBA"):
                        dispose_mode = "RGB"
                        color = _rgb(color)
                self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
            else:
                # replace with previous contents
                if self.im is not None:
                    # only dispose the extent in this frame
                    self.dispose = self._crop(self.im, self.dispose_extent)
                elif frame_transparency is not None:
                    x0, y0, x1, y1 = self.dispose_extent
                    dispose_size = (x1 - x0, y1 - y0)

                    Image._decompression_bomb_check(dispose_size)
                    dispose_mode = "P"
                    color = frame_transparency
                    if self.mode in ("RGB", "RGBA"):
                        dispose_mode = "RGBA"
                        color = _rgb(frame_transparency) + (0,)
                    self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
        except AttributeError:
            pass

        if interlace is not None:
            transparency = -1
            if frame_transparency is not None:
                if frame == 0:
                    if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS:
                        self.info["transparency"] = frame_transparency
                elif self.mode not in ("RGB", "RGBA"):
                    transparency = frame_transparency
            self.tile = [
                (
                    "gif",
                    (x0, y0, x1, y1),
                    self.__offset,
                    (bits, interlace, transparency),
                )
            ]

        if info.get("comment"):
            self.info["comment"] = info["comment"]
        for k in ["duration", "extension"]:
            if k in info:
                self.info[k] = info[k]
            elif k in self.info:
                del self.info[k]

    def load_prepare(self):
        temp_mode = "P" if self._frame_palette else "L"
        self._prev_im = None
        if self.__frame == 0:
            if self._frame_transparency is not None:
                self.im = Image.core.fill(
                    temp_mode, self.size, self._frame_transparency
                )
        elif self.mode in ("RGB", "RGBA"):
            self._prev_im = self.im
            if self._frame_palette:
                self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
                self.im.putpalette(*self._frame_palette.getdata())
            else:
                self.im = None
        self.mode = temp_mode
        self._frame_palette = None

        super().load_prepare()

    def load_end(self):
        if self.__frame == 0:
            if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
                if self._frame_transparency is not None:
                    self.im.putpalettealpha(self._frame_transparency, 0)
                    self.mode = "RGBA"
                else:
                    self.mode = "RGB"
                self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
            return
        if not self._prev_im:
            return
        if self._frame_transparency is not None:
            self.im.putpalettealpha(self._frame_transparency, 0)
            frame_im = self.im.convert("RGBA")
        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":
            self.im.paste(frame_im, self.dispose_extent, frame_im)
        else:
            self.im.paste(frame_im, self.dispose_extent)

    def tell(self):
        return self.__frame


# --------------------------------------------------------------------
# Write GIF files


RAWMODE = {"1": "L", "L": "L", "P": "P"}


def _normalize_mode(im):
    """
    Takes an image (or frame), returns an image in a mode that is appropriate
    for saving in a Gif.

    It may return the original image, or it may return an image converted to
    palette or 'L' mode.

    :param im: Image object
    :returns: Image object
    """
    if im.mode in RAWMODE:
        im.load()
        return im
    if Image.getmodebase(im.mode) == "RGB":
        im = im.convert("P", palette=Image.Palette.ADAPTIVE)
        if im.palette.mode == "RGBA":
            for rgba in im.palette.colors.keys():
                if rgba[3] == 0:
                    im.info["transparency"] = im.palette.colors[rgba]
                    break
        return im
    return im.convert("L")


def _normalize_palette(im, palette, info):
    """
    Normalizes the palette for image.
      - Sets the palette to the incoming palette, if provided.
      - Ensures that there's a palette for L mode images
      - Optimizes the palette if necessary/desired.

    :param im: Image object
    :param palette: bytes object containing the source palette, or ....
    :param info: encoderinfo
    :returns: Image object
    """
    source_palette = None
    if palette:
        # a bytes palette
        if isinstance(palette, (bytes, bytearray, list)):
            source_palette = bytearray(palette[:768])
        if isinstance(palette, ImagePalette.ImagePalette):
            source_palette = bytearray(palette.palette)

    if im.mode == "P":
        if not source_palette:
            source_palette = im.im.getpalette("RGB")[:768]
    else:  # L-mode
        if not source_palette:
            source_palette = bytearray(i // 3 for i in range(768))
        im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)

    if palette:
        used_palette_colors = []
        for i in range(0, len(source_palette), 3):
            source_color = tuple(source_palette[i : i + 3])
            index = im.palette.colors.get(source_color)
            if index in used_palette_colors:
                index = None
            used_palette_colors.append(index)
        for i, index in enumerate(used_palette_colors):
            if index is None:
                for j in range(len(used_palette_colors)):
                    if j not in used_palette_colors:
                        used_palette_colors[i] = j
                        break
        im = im.remap_palette(used_palette_colors)
    else:
        used_palette_colors = _get_optimize(im, info)
        if used_palette_colors is not None:
            return im.remap_palette(used_palette_colors, source_palette)

    im.palette.palette = source_palette
    return im


def _write_single_frame(im, fp, palette):
    im_out = _normalize_mode(im)
    for k, v in im_out.info.items():
        im.encoderinfo.setdefault(k, v)
    im_out = _normalize_palette(im_out, palette, im.encoderinfo)

    for s in _get_global_header(im_out, im.encoderinfo):
        fp.write(s)

    # local image header
    flags = 0
    if get_interlace(im):
        flags = flags | 64
    _write_local_header(fp, im, (0, 0), flags)

    im_out.encoderconfig = (8, get_interlace(im))
    ImageFile._save(im_out, fp, [("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])])

    fp.write(b"\0")  # end of image data


def _write_multiple_frames(im, fp, palette):

    duration = im.encoderinfo.get("duration")
    disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))

    im_frames = []
    frame_count = 0
    background_im = None
    for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
        for im_frame in ImageSequence.Iterator(imSequence):
            # a copy is required here since seek can still mutate the image
            im_frame = _normalize_mode(im_frame.copy())
            if frame_count == 0:
                for k, v in im_frame.info.items():
                    if k == "transparency":
                        continue
                    im.encoderinfo.setdefault(k, v)

            encoderinfo = im.encoderinfo.copy()
            im_frame = _normalize_palette(im_frame, palette, encoderinfo)
            if "transparency" in im_frame.info:
                encoderinfo.setdefault("transparency", im_frame.info["transparency"])
            if isinstance(duration, (list, tuple)):
                encoderinfo["duration"] = duration[frame_count]
            elif duration is None and "duration" in im_frame.info:
                encoderinfo["duration"] = im_frame.info["duration"]
            if isinstance(disposal, (list, tuple)):
                encoderinfo["disposal"] = disposal[frame_count]
            frame_count += 1

            if im_frames:
                # delta frame
                previous = im_frames[-1]
                if encoderinfo.get("disposal") == 2:
                    if background_im is None:
                        color = im.encoderinfo.get(
                            "transparency", im.info.get("transparency", (0, 0, 0))
                        )
                        background = _get_background(im_frame, color)
                        background_im = Image.new("P", im_frame.size, background)
                        background_im.putpalette(im_frames[0]["im"].palette)
                    base_im = background_im
                else:
                    base_im = previous["im"]
                if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):
                    delta = ImageChops.subtract_modulo(im_frame, base_im)
                else:
                    delta = ImageChops.subtract_modulo(
                        im_frame.convert("RGB"), base_im.convert("RGB")
                    )
                bbox = delta.getbbox()
                if not bbox:
                    # This frame is identical to the previous frame
                    if encoderinfo.get("duration"):
                        previous["encoderinfo"]["duration"] += encoderinfo["duration"]
                    continue
            else:
                bbox = None
            im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})

    if len(im_frames) > 1:
        for frame_data in im_frames:
            im_frame = frame_data["im"]
            if not frame_data["bbox"]:
                # global header
                for s in _get_global_header(im_frame, frame_data["encoderinfo"]):
                    fp.write(s)
                offset = (0, 0)
            else:
                # compress difference
                if not palette:
                    frame_data["encoderinfo"]["include_color_table"] = True

                im_frame = im_frame.crop(frame_data["bbox"])
                offset = frame_data["bbox"][:2]
            _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"])
        return True
    elif "duration" in im.encoderinfo and isinstance(
        im.encoderinfo["duration"], (list, tuple)
    ):
        # Since multiple frames will not be written, add together the frame durations
        im.encoderinfo["duration"] = sum(im.encoderinfo["duration"])


def _save_all(im, fp, filename):
    _save(im, fp, filename, save_all=True)


def _save(im, fp, filename, save_all=False):
    # header
    if "palette" in im.encoderinfo or "palette" in im.info:
        palette = im.encoderinfo.get("palette", im.info.get("palette"))
    else:
        palette = None
        im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)

    if not save_all or not _write_multiple_frames(im, fp, palette):
        _write_single_frame(im, fp, palette)

    fp.write(b";")  # end of file

    if hasattr(fp, "flush"):
        fp.flush()


def get_interlace(im):
    interlace = im.encoderinfo.get("interlace", 1)

    # workaround for @PIL153
    if min(im.size) < 16:
        interlace = 0

    return interlace


def _write_local_header(fp, im, offset, flags):
    transparent_color_exists = False
    try:
        if "transparency" in im.encoderinfo:
            transparency = im.encoderinfo["transparency"]
        else:
            transparency = im.info["transparency"]
        transparency = int(transparency)
    except (KeyError, ValueError):
        pass
    else:
        # optimize the block away if transparent color is not used
        transparent_color_exists = True

        used_palette_colors = _get_optimize(im, im.encoderinfo)
        if used_palette_colors is not None:
            # adjust the transparency index after optimize
            try:
                transparency = used_palette_colors.index(transparency)
            except ValueError:
                transparent_color_exists = False

    if "duration" in im.encoderinfo:
        duration = int(im.encoderinfo["duration"] / 10)
    else:
        duration = 0

    disposal = int(im.encoderinfo.get("disposal", 0))

    if transparent_color_exists or duration != 0 or disposal:
        packed_flag = 1 if transparent_color_exists else 0
        packed_flag |= disposal << 2
        if not transparent_color_exists:
            transparency = 0

        fp.write(
            b"!"
            + o8(249)  # extension intro
            + o8(4)  # length
            + o8(packed_flag)  # packed fields
            + o16(duration)  # duration
            + o8(transparency)  # transparency index
            + o8(0)
        )

    include_color_table = im.encoderinfo.get("include_color_table")
    if include_color_table:
        palette_bytes = _get_palette_bytes(im)
        color_table_size = _get_color_table_size(palette_bytes)
        if color_table_size:
            flags = flags | 128  # local color table flag
            flags = flags | color_table_size

    fp.write(
        b","
        + o16(offset[0])  # offset
        + o16(offset[1])
        + o16(im.size[0])  # size
        + o16(im.size[1])
        + o8(flags)  # flags
    )
    if include_color_table and color_table_size:
        fp.write(_get_header_palette(palette_bytes))
    fp.write(o8(8))  # bits


def _save_netpbm(im, fp, filename):

    # Unused by default.
    # To use, uncomment the register_save call at the end of the file.
    #
    # If you need real GIF compression and/or RGB quantization, you
    # can use the external NETPBM/PBMPLUS utilities.  See comments
    # below for information on how to enable this.
    tempfile = im._dump()

    try:
        with open(filename, "wb") as f:
            if im.mode != "RGB":
                subprocess.check_call(
                    ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL
                )
            else:
                # Pipe ppmquant output into ppmtogif
                # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename)
                quant_cmd = ["ppmquant", "256", tempfile]
                togif_cmd = ["ppmtogif"]
                quant_proc = subprocess.Popen(
                    quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
                )
                togif_proc = subprocess.Popen(
                    togif_cmd,
                    stdin=quant_proc.stdout,
                    stdout=f,
                    stderr=subprocess.DEVNULL,
                )

                # Allow ppmquant to receive SIGPIPE if ppmtogif exits
                quant_proc.stdout.close()

                retcode = quant_proc.wait()
                if retcode:
                    raise subprocess.CalledProcessError(retcode, quant_cmd)

                retcode = togif_proc.wait()
                if retcode:
                    raise subprocess.CalledProcessError(retcode, togif_cmd)
    finally:
        try:
            os.unlink(tempfile)
        except OSError:
            pass


# Force optimization so that we can test performance against
# cases where it took lots of memory and time previously.
_FORCE_OPTIMIZE = False


def _get_optimize(im, info):
    """
    Palette optimization is a potentially expensive operation.

    This function determines if the palette should be optimized using
    some heuristics, then returns the list of palette entries in use.

    :param im: Image object
    :param info: encoderinfo
    :returns: list of indexes of palette entries in use, or None
    """
    if im.mode in ("P", "L") and info and info.get("optimize", 0):
        # Potentially expensive operation.

        # The palette saves 3 bytes per color not used, but palette
        # lengths are restricted to 3*(2**N) bytes. Max saving would
        # be 768 -> 6 bytes if we went all the way down to 2 colors.
        # * If we're over 128 colors, we can't save any space.
        # * If there aren't any holes, it's not worth collapsing.
        # * If we have a 'large' image, the palette is in the noise.

        # create the new palette if not every color is used
        optimise = _FORCE_OPTIMIZE or im.mode == "L"
        if optimise or im.width * im.height < 512 * 512:
            # check which colors are used
            used_palette_colors = []
            for i, count in enumerate(im.histogram()):
                if count:
                    used_palette_colors.append(i)

            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


def _get_color_table_size(palette_bytes):
    # calculate the palette size for the header
    if not palette_bytes:
        return 0
    elif len(palette_bytes) < 9:
        return 1
    else:
        return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1


def _get_header_palette(palette_bytes):
    """
    Returns the palette, null padded to the next power of 2 (*3) bytes
    suitable for direct inclusion in the GIF header

    :param palette_bytes: Unpadded palette bytes, in RGBRGB form
    :returns: Null padded palette
    """
    color_table_size = _get_color_table_size(palette_bytes)

    # add the missing amount of bytes
    # the palette has to be 2<<n in size
    actual_target_size_diff = (2 << color_table_size) - len(palette_bytes) // 3
    if actual_target_size_diff > 0:
        palette_bytes += o8(0) * 3 * actual_target_size_diff
    return palette_bytes


def _get_palette_bytes(im):
    """
    Gets the palette for inclusion in the gif header

    :param im: Image object
    :returns: Bytes, len<=768 suitable for inclusion in gif header
    """
    return im.palette.palette


def _get_background(im, info_background):
    background = 0
    if info_background:
        background = info_background
        if isinstance(background, tuple):
            # WebPImagePlugin stores an RGBA value in info["background"]
            # So it must be converted to the same format as GifImagePlugin's
            # info["background"] - a global color table index
            try:
                background = im.palette.getcolor(background, im)
            except ValueError as e:
                if str(e) == "cannot allocate more than 256 colors":
                    # If all 256 colors are in use,
                    # then there is no need for the background color
                    return 0
                else:
                    raise
    return background


def _get_global_header(im, info):
    """Return a list of strings representing a GIF header"""

    # Header Block
    # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp

    version = b"87a"
    if im.info.get("version") == b"89a" or (
        info
        and (
            "transparency" in info
            or "loop" in info
            or info.get("duration")
            or info.get("comment")
        )
    ):
        version = b"89a"

    background = _get_background(im, info.get("background"))

    palette_bytes = _get_palette_bytes(im)
    color_table_size = _get_color_table_size(palette_bytes)

    header = [
        b"GIF"  # signature
        + version  # version
        + o16(im.size[0])  # canvas width
        + o16(im.size[1]),  # canvas height
        # Logical Screen Descriptor
        # size of global color table + global color table flag
        o8(color_table_size + 128),  # packed fields
        # background + reserved/aspect
        o8(background) + o8(0),
        # Global Color Table
        _get_header_palette(palette_bytes),
    ]
    if "loop" in info:
        header.append(
            b"!"
            + o8(255)  # extension intro
            + o8(11)
            + b"NETSCAPE2.0"
            + o8(3)
            + o8(1)
            + o16(info["loop"])  # number of loops
            + o8(0)
        )
    if info.get("comment"):
        comment_block = b"!" + o8(254)  # extension intro

        comment = info["comment"]
        if isinstance(comment, str):
            comment = comment.encode()
        for i in range(0, len(comment), 255):
            subblock = comment[i : i + 255]
            comment_block += o8(len(subblock)) + subblock

        comment_block += o8(0)
        header.append(comment_block)
    return header


def _write_frame_data(fp, im_frame, offset, params):
    try:
        im_frame.encoderinfo = params

        # local image header
        _write_local_header(fp, im_frame, offset, 0)

        ImageFile._save(
            im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])]
        )

        fp.write(b"\0")  # end of image data
    finally:
        del im_frame.encoderinfo


# --------------------------------------------------------------------
# Legacy GIF utilities


def getheader(im, palette=None, info=None):
    """
    Legacy Method to get Gif data from image.

    Warning:: May modify image data.

    :param im: Image object
    :param palette: bytes object containing the source palette, or ....
    :param info: encoderinfo
    :returns: tuple of(list of header items, optimized palette)

    """
    used_palette_colors = _get_optimize(im, info)

    if info is None:
        info = {}

    if "background" not in info and "background" in im.info:
        info["background"] = im.info["background"]

    im_mod = _normalize_palette(im, palette, info)
    im.palette = im_mod.palette
    im.im = im_mod.im
    header = _get_global_header(im, info)

    return header, used_palette_colors


def getdata(im, offset=(0, 0), **params):
    """
    Legacy Method

    Return a list of strings representing this image.
    The first string is a local image header, the rest contains
    encoded image data.

    To specify duration, add the time in milliseconds,
    e.g. ``getdata(im_frame, duration=1000)``

    :param im: Image object
    :param offset: Tuple of (x, y) pixels. Defaults to (0, 0)
    :param \\**params: e.g. duration or other encoder info parameters
    :returns: List of bytes containing GIF encoded frame data

    """

    class Collector:
        data = []

        def write(self, data):
            self.data.append(data)

    im.load()  # make sure raster data is available

    fp = Collector()

    _write_frame_data(fp, im, offset, params)

    return fp.data


# --------------------------------------------------------------------
# Registry

Image.register_open(GifImageFile.format, GifImageFile, _accept)
Image.register_save(GifImageFile.format, _save)
Image.register_save_all(GifImageFile.format, _save_all)
Image.register_extension(GifImageFile.format, ".gif")
Image.register_mime(GifImageFile.format, "image/gif")

#
# Uncomment the following line if you wish to use NETPBM/PBMPLUS
# instead of the built-in "uncompressed" GIF encoder

# Image.register_save(GifImageFile.format, _save_netpbm)

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
[ISSUE]
**Title:** Saving GIF with Non-Opaque RGBA Background Raises ValueError

**Description:**
When attempting to save a GIF image with a non-opaque RGBA background, a `ValueError` is raised. This occurs because the GIF palette does not support non-opaque colors.

**Example Code:**
```python
from PIL import Image

# Create an image with a transparent background
image = Image.new("RGBA", (100, 100), (0, 0, 0, 0))
image.info["background"] = (0, 0, 0, 0)

# Attempt to save as GIF
image.save("temp.gif")
```

**Expected Behavior:**
The image should be saved as a GIF with a default background color or handle the transparency appropriately without raising an error.

**Actual Behavior:**
Saving the image results in the following error:
```
ValueError: cannot add non-opaque RGBA color to RGB palette
```
[/ISSUE]
{
    "\u001b[1mtest_sanity\u001b[0m": "PASSED",
    "\u001b[1mtest_unclosed_file\u001b[0m": "PASSED",
    "\u001b[1mtest_closed_file\u001b[0m": "PASSED",
    "\u001b[1mtest_seek_after_close\u001b[0m": "PASSED",
    "\u001b[1mtest_context_manager\u001b[0m": "PASSED",
    "\u001b[1mtest_invalid_file\u001b[0m": "PASSED",
    "\u001b[1mtest_l_mode_transparency\u001b[0m": "PASSED",
    "\u001b[1mtest_l_mode_after_rgb\u001b[0m": "PASSED",
    "\u001b[1mtest_palette_not_needed_for_second_frame\u001b[0m": "PASSED",
    "\u001b[1mtest_strategy\u001b[0m": "PASSED",
    "\u001b[1mtest_optimize\u001b[0m": "PASSED",
    "\u001b[1mtest_optimize_correctness\u001b[0m": "PASSED",
    "\u001b[1mtest_optimize_full_l\u001b[0m": "PASSED",
    "\u001b[1mtest_optimize_if_palette_can_be_reduced_by_half\u001b[0m": "PASSED",
    "\u001b[1mtest_roundtrip\u001b[0m": "PASSED",
    "\u001b[1mtest_roundtrip2\u001b[0m": "PASSED",
    "\u001b[1mtest_roundtrip_save_all\u001b[0m": "PASSED",
    "\u001b[1mtest_loading_multiple_palettes[Tests/images/dispose_bgnd.gif-RGB]\u001b[0m": "PASSED",
    "\u001b[1mtest_loading_multiple_palettes[Tests/images/dispose_bgnd_rgba.gif-RGBA]\u001b[0m": "PASSED",
    "\u001b[1mtest_headers_saving_for_animated_gifs\u001b[0m": "PASSED",
    "\u001b[1mtest_palette_handling\u001b[0m": "PASSED",
    "\u001b[1mtest_palette_434\u001b[0m": "PASSED",
    "\u001b[1mtest_seek\u001b[0m": "PASSED",
    "\u001b[1mtest_seek_info\u001b[0m": "PASSED",
    "\u001b[1mtest_seek_rewind\u001b[0m": "PASSED",
    "\u001b[1mtest_n_frames[Tests/images/hopper.gif-1]\u001b[0m": "PASSED",
    "\u001b[1mtest_n_frames[Tests/images/comment_after_last_frame.gif-2]\u001b[0m": "PASSED",
    "\u001b[1mtest_n_frames[Tests/images/iss634.gif-42]\u001b[0m": "PASSED",
    "\u001b[1mtest_no_change\u001b[0m": "PASSED",
    "\u001b[1mtest_eoferror\u001b[0m": "PASSED",
    "\u001b[1mtest_first_frame_transparency\u001b[0m": "PASSED",
    "\u001b[1mtest_dispose_none\u001b[0m": "PASSED",
    "\u001b[1mtest_dispose_none_load_end\u001b[0m": "PASSED",
    "\u001b[1mtest_dispose_background\u001b[0m": "PASSED",
    "\u001b[1mtest_dispose_background_transparency\u001b[0m": "PASSED",
    "\u001b[1mtest_transparent_dispose[LoadingStrategy.RGB_AFTER_FIRST-expected_colors0]\u001b[0m": "PASSED",
    "\u001b[1mtest_transparent_dispose[LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY-expected_colors1]\u001b[0m": "PASSED",
    "\u001b[1mtest_dispose_previous\u001b[0m": "PASSED",
    "\u001b[1mtest_dispose_previous_first_frame\u001b[0m": "PASSED",
    "\u001b[1mtest_previous_frame_loaded\u001b[0m": "PASSED",
    "\u001b[1mtest_save_dispose\u001b[0m": "PASSED",
    "\u001b[1mtest_dispose2_palette\u001b[0m": "PASSED",
    "\u001b[1mtest_dispose2_diff\u001b[0m": "PASSED",
    "\u001b[1mtest_dispose2_background\u001b[0m": "PASSED",
    "\u001b[1mtest_transparency_in_second_frame\u001b[0m": "PASSED",
    "\u001b[1mtest_no_transparency_in_second_frame\u001b[0m": "PASSED",
    "\u001b[1mtest_remapped_transparency\u001b[0m": "PASSED",
    "\u001b[1mtest_duration\u001b[0m": "PASSED",
    "\u001b[1mtest_multiple_duration\u001b[0m": "PASSED",
    "\u001b[1mtest_roundtrip_info_duration\u001b[0m": "PASSED",
    "\u001b[1mtest_roundtrip_info_duration_combined\u001b[0m": "PASSED",
    "\u001b[1mtest_identical_frames\u001b[0m": "PASSED",
    "\u001b[1mtest_identical_frames_to_single_frame[duration0]\u001b[0m": "PASSED",
    "\u001b[1mtest_identical_frames_to_single_frame[duration1]\u001b[0m": "PASSED",
    "\u001b[1mtest_identical_frames_to_single_frame[8500]\u001b[0m": "PASSED",
    "\u001b[1mtest_number_of_loops\u001b[0m": "PASSED",
    "\u001b[1mtest_background\u001b[0m": "PASSED",
    "\u001b[1mtest_webp_background\u001b[0m": "PASSED",
    "\u001b[1mtest_comment\u001b[0m": "PASSED",
    "\u001b[1mtest_comment_over_255\u001b[0m": "PASSED",
    "\u001b[1mtest_zero_comment_subblocks\u001b[0m": "PASSED",
    "\u001b[1mtest_read_multiple_comment_blocks\u001b[0m": "PASSED",
    "\u001b[1mtest_empty_string_comment\u001b[0m": "PASSED",
    "\u001b[1mtest_retain_comment_in_subsequent_frames\u001b[0m": "PASSED",
    "\u001b[1mtest_version\u001b[0m": "PASSED",
    "\u001b[1mtest_append_images\u001b[0m": "PASSED",
    "\u001b[1mtest_transparent_optimize\u001b[0m": "PASSED",
    "\u001b[1mtest_rgb_transparency\u001b[0m": "PASSED",
    "\u001b[1mtest_rgba_transparency\u001b[0m": "PASSED",
    "\u001b[1mtest_bbox\u001b[0m": "PASSED",
    "\u001b[1mtest_palette_save_L\u001b[0m": "PASSED",
    "\u001b[1mtest_palette_save_P\u001b[0m": "PASSED",
    "\u001b[1mtest_palette_save_duplicate_entries\u001b[0m": "PASSED",
    "\u001b[1mtest_palette_save_all_P\u001b[0m": "PASSED",
    "\u001b[1mtest_palette_save_ImagePalette\u001b[0m": "PASSED",
    "\u001b[1mtest_save_I\u001b[0m": "PASSED",
    "\u001b[1mtest_getdata\u001b[0m": "PASSED",
    "\u001b[1mtest_lzw_bits\u001b[0m": "PASSED",
    "\u001b[1mtest_extents\u001b[0m": "PASSED",
    "\u001b[1mtest_missing_background\u001b[0m": "PASSED",
    "\u001b[1mtest_saving_rgba\u001b[0m": "PASSED"
}
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index 13894c8d6..b1d842cef 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -886,20 +886,23 @@ def _get_palette_bytes(im):
 def _get_background(im, info_background):
     background = 0
     if info_background:
-        background = info_background
-        if isinstance(background, tuple):
+        if isinstance(info_background, tuple):
             # WebPImagePlugin stores an RGBA value in info["background"]
             # So it must be converted to the same format as GifImagePlugin's
             # info["background"] - a global color table index
             try:
-                background = im.palette.getcolor(background, im)
+                background = im.palette.getcolor(info_background, im)
             except ValueError as e:
-                if str(e) == "cannot allocate more than 256 colors":
+                if str(e) not in (
                     # If all 256 colors are in use,
                     # then there is no need for the background color
-                    return 0
-                else:
+                    "cannot allocate more than 256 colors",
+                    # Ignore non-opaque WebP background
+                    "cannot add non-opaque RGBA color to RGB palette",
+                ):
                     raise
+        else:
+            background = info_background
     return background
 
 
 

{"src/PIL/GifImagePlugin.py": "#\n# The Python Imaging Library.\n# $Id$\n#\n# GIF file handling\n#\n# History:\n# 1995-09-01 fl   Created\n# 1996-12-14 fl   Added interlace support\n# 1996-12-30 fl   Added animation support\n# 1997-01-05 fl   Added write support, fixed local colour map bug\n# 1997-02-23 fl   Make sure to load raster data in getdata()\n# 1997-07-05 fl   Support external decoder (0.4)\n# 1998-07-09 fl   Handle all modes when saving (0.5)\n# 1998-07-15 fl   Renamed offset attribute to avoid name clash\n# 2001-04-16 fl   Added rewind support (seek to frame 0) (0.6)\n# 2001-04-17 fl   Added palette optimization (0.7)\n# 2002-06-06 fl   Added transparency support for save (0.8)\n# 2004-02-24 fl   Disable interlacing for small images\n#\n# Copyright (c) 1997-2004 by Secret Labs AB\n# Copyright (c) 1995-2004 by Fredrik Lundh\n#\n# See the README file for information on usage and redistribution.\n#\n\nimport itertools\nimport math\nimport os\nimport subprocess\nfrom enum import IntEnum\n\nfrom . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence\nfrom ._binary import i16le as i16\nfrom ._binary import o8\nfrom ._binary import o16le as o16\n\n\nclass LoadingStrategy(IntEnum):\n    \"\"\".. versionadded:: 9.1.0\"\"\"\n\n    RGB_AFTER_FIRST = 0\n    RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1\n    RGB_ALWAYS = 2\n\n\n#: .. versionadded:: 9.1.0\nLOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST\n\n# --------------------------------------------------------------------\n# Identify/read GIF files\n\n\ndef _accept(prefix):\n    return prefix[:6] in [b\"GIF87a\", b\"GIF89a\"]\n\n\n##\n# Image plugin for GIF images.  This plugin supports both GIF87 and\n# GIF89 images.\n\n\nclass GifImageFile(ImageFile.ImageFile):\n\n    format = \"GIF\"\n    format_description = \"Compuserve GIF\"\n    _close_exclusive_fp_after_loading = False\n\n    global_palette = None\n\n    def data(self):\n        s = self.fp.read(1)\n        if s and s[0]:\n            return self.fp.read(s[0])\n        return None\n\n    def _is_palette_needed(self, p):\n        for i in range(0, len(p), 3):\n            if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):\n                return True\n        return False\n\n    def _open(self):\n\n        # Screen\n        s = self.fp.read(13)\n        if not _accept(s):\n            raise SyntaxError(\"not a GIF file\")\n\n        self.info[\"version\"] = s[:6]\n        self._size = i16(s, 6), i16(s, 8)\n        self.tile = []\n        flags = s[10]\n        bits = (flags & 7) + 1\n\n        if flags & 128:\n            # get global palette\n            self.info[\"background\"] = s[11]\n            # check if palette contains colour indices\n            p = self.fp.read(3 << bits)\n            if self._is_palette_needed(p):\n                p = ImagePalette.raw(\"RGB\", p)\n                self.global_palette = self.palette = p\n\n        self._fp = self.fp  # FIXME: hack\n        self.__rewind = self.fp.tell()\n        self._n_frames = None\n        self._is_animated = None\n        self._seek(0)  # get ready to read first frame\n\n    @property\n    def n_frames(self):\n        if self._n_frames is None:\n            current = self.tell()\n            try:\n                while True:\n                    self._seek(self.tell() + 1, False)\n            except EOFError:\n                self._n_frames = self.tell() + 1\n            self.seek(current)\n        return self._n_frames\n\n    @property\n    def is_animated(self):\n        if self._is_animated is None:\n            if self._n_frames is not None:\n                self._is_animated = self._n_frames != 1\n            else:\n                current = self.tell()\n                if current:\n                    self._is_animated = True\n                else:\n                    try:\n                        self._seek(1, False)\n                        self._is_animated = True\n                    except EOFError:\n                        self._is_animated = False\n\n                    self.seek(current)\n        return self._is_animated\n\n    def seek(self, frame):\n        if not self._seek_check(frame):\n            return\n        if frame < self.__frame:\n            self.im = None\n            self._seek(0)\n\n        last_frame = self.__frame\n        for f in range(self.__frame + 1, frame + 1):\n            try:\n                self._seek(f)\n            except EOFError as e:\n                self.seek(last_frame)\n                raise EOFError(\"no more images in GIF file\") from e\n\n    def _seek(self, frame, update_image=True):\n\n        if frame == 0:\n            # rewind\n            self.__offset = 0\n            self.dispose = None\n            self.__frame = -1\n            self._fp.seek(self.__rewind)\n            self.disposal_method = 0\n            if \"comment\" in self.info:\n                del self.info[\"comment\"]\n        else:\n            # ensure that the previous frame was loaded\n            if self.tile and update_image:\n                self.load()\n\n        if frame != self.__frame + 1:\n            raise ValueError(f\"cannot seek to frame {frame}\")\n\n        self.fp = self._fp\n        if self.__offset:\n            # backup to last frame\n            self.fp.seek(self.__offset)\n            while self.data():\n                pass\n            self.__offset = 0\n\n        s = self.fp.read(1)\n        if not s or s == b\";\":\n            raise EOFError\n\n        palette = None\n\n        info = {}\n        frame_transparency = None\n        interlace = None\n        frame_dispose_extent = None\n        while True:\n\n            if not s:\n                s = self.fp.read(1)\n            if not s or s == b\";\":\n                break\n\n            elif s == b\"!\":\n                #\n                # extensions\n                #\n                s = self.fp.read(1)\n                block = self.data()\n                if s[0] == 249:\n                    #\n                    # graphic control extension\n                    #\n                    flags = block[0]\n                    if flags & 1:\n                        frame_transparency = block[3]\n                    info[\"duration\"] = i16(block, 1) * 10\n\n                    # disposal method - find the value of bits 4 - 6\n                    dispose_bits = 0b00011100 & flags\n                    dispose_bits = dispose_bits >> 2\n                    if dispose_bits:\n                        # only set the dispose if it is not\n                        # unspecified. I'm not sure if this is\n                        # correct, but it seems to prevent the last\n                        # frame from looking odd for some animations\n                        self.disposal_method = dispose_bits\n                elif s[0] == 254:\n                    #\n                    # comment extension\n                    #\n                    comment = b\"\"\n\n                    # Read this comment block\n                    while block:\n                        comment += block\n                        block = self.data()\n\n                    if \"comment\" in info:\n                        # If multiple comment blocks in frame, separate with \\n\n                        info[\"comment\"] += b\"\\n\" + comment\n                    else:\n                        info[\"comment\"] = comment\n                    s = None\n                    continue\n                elif s[0] == 255 and frame == 0:\n                    #\n                    # application extension\n                    #\n                    info[\"extension\"] = block, self.fp.tell()\n                    if block[:11] == b\"NETSCAPE2.0\":\n                        block = self.data()\n                        if len(block) >= 3 and block[0] == 1:\n                            self.info[\"loop\"] = i16(block, 1)\n                while self.data():\n                    pass\n\n            elif s == b\",\":\n                #\n                # local image\n                #\n                s = self.fp.read(9)\n\n                # extent\n                x0, y0 = i16(s, 0), i16(s, 2)\n                x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)\n                if (x1 > self.size[0] or y1 > self.size[1]) and update_image:\n                    self._size = max(x1, self.size[0]), max(y1, self.size[1])\n                    Image._decompression_bomb_check(self._size)\n                frame_dispose_extent = x0, y0, x1, y1\n                flags = s[8]\n\n                interlace = (flags & 64) != 0\n\n                if flags & 128:\n                    bits = (flags & 7) + 1\n                    p = self.fp.read(3 << bits)\n                    if self._is_palette_needed(p):\n                        palette = ImagePalette.raw(\"RGB\", p)\n                    else:\n                        palette = False\n\n                # image data\n                bits = self.fp.read(1)[0]\n                self.__offset = self.fp.tell()\n                break\n\n            else:\n                pass\n                # raise OSError, \"illegal GIF tag `%x`\" % s[0]\n            s = None\n\n        if interlace is None:\n            # self._fp = None\n            raise EOFError\n\n        self.__frame = frame\n        if not update_image:\n            return\n\n        self.tile = []\n\n        if self.dispose:\n            self.im.paste(self.dispose, self.dispose_extent)\n\n        self._frame_palette = palette if palette is not None else self.global_palette\n        self._frame_transparency = frame_transparency\n        if frame == 0:\n            if self._frame_palette:\n                if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:\n                    self.mode = \"RGBA\" if frame_transparency is not None else \"RGB\"\n                else:\n                    self.mode = \"P\"\n            else:\n                self.mode = \"L\"\n\n            if not palette and self.global_palette:\n                from copy import copy\n\n                palette = copy(self.global_palette)\n            self.palette = palette\n        else:\n            if self.mode == \"P\":\n                if (\n                    LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY\n                    or palette\n                ):\n                    self.pyaccess = None\n                    if \"transparency\" in self.info:\n                        self.im.putpalettealpha(self.info[\"transparency\"], 0)\n                        self.im = self.im.convert(\"RGBA\", Image.Dither.FLOYDSTEINBERG)\n                        self.mode = \"RGBA\"\n                        del self.info[\"transparency\"]\n                    else:\n                        self.mode = \"RGB\"\n                        self.im = self.im.convert(\"RGB\", Image.Dither.FLOYDSTEINBERG)\n\n        def _rgb(color):\n            if self._frame_palette:\n                color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])\n            else:\n                color = (color, color, color)\n            return color\n\n        self.dispose_extent = frame_dispose_extent\n        try:\n            if self.disposal_method < 2:\n                # do not dispose or none specified\n                self.dispose = None\n            elif self.disposal_method == 2:\n                # replace with background colour\n\n                # only dispose the extent in this frame\n                x0, y0, x1, y1 = self.dispose_extent\n                dispose_size = (x1 - x0, y1 - y0)\n\n                Image._decompression_bomb_check(dispose_size)\n\n                # by convention, attempt to use transparency first\n                dispose_mode = \"P\"\n                color = self.info.get(\"transparency\", frame_transparency)\n                if color is not None:\n                    if self.mode in (\"RGB\", \"RGBA\"):\n                        dispose_mode = \"RGBA\"\n                        color = _rgb(color) + (0,)\n                else:\n                    color = self.info.get(\"background\", 0)\n                    if self.mode in (\"RGB\", \"RGBA\"):\n                        dispose_mode = \"RGB\"\n                        color = _rgb(color)\n                self.dispose = Image.core.fill(dispose_mode, dispose_size, color)\n            else:\n                # replace with previous contents\n                if self.im is not None:\n                    # only dispose the extent in this frame\n                    self.dispose = self._crop(self.im, self.dispose_extent)\n                elif frame_transparency is not None:\n                    x0, y0, x1, y1 = self.dispose_extent\n                    dispose_size = (x1 - x0, y1 - y0)\n\n                    Image._decompression_bomb_check(dispose_size)\n                    dispose_mode = \"P\"\n                    color = frame_transparency\n                    if self.mode in (\"RGB\", \"RGBA\"):\n                        dispose_mode = \"RGBA\"\n                        color = _rgb(frame_transparency) + (0,)\n                    self.dispose = Image.core.fill(dispose_mode, dispose_size, color)\n        except AttributeError:\n            pass\n\n        if interlace is not None:\n            transparency = -1\n            if frame_transparency is not None:\n                if frame == 0:\n                    if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS:\n                        self.info[\"transparency\"] = frame_transparency\n                elif self.mode not in (\"RGB\", \"RGBA\"):\n                    transparency = frame_transparency\n            self.tile = [\n                (\n                    \"gif\",\n                    (x0, y0, x1, y1),\n                    self.__offset,\n                    (bits, interlace, transparency),\n                )\n            ]\n\n        if info.get(\"comment\"):\n            self.info[\"comment\"] = info[\"comment\"]\n        for k in [\"duration\", \"extension\"]:\n            if k in info:\n                self.info[k] = info[k]\n            elif k in self.info:\n                del self.info[k]\n\n    def load_prepare(self):\n        temp_mode = \"P\" if self._frame_palette else \"L\"\n        self._prev_im = None\n        if self.__frame == 0:\n            if self._frame_transparency is not None:\n                self.im = Image.core.fill(\n                    temp_mode, self.size, self._frame_transparency\n                )\n        elif self.mode in (\"RGB\", \"RGBA\"):\n            self._prev_im = self.im\n            if self._frame_palette:\n                self.im = Image.core.fill(\"P\", self.size, self._frame_transparency or 0)\n                self.im.putpalette(*self._frame_palette.getdata())\n            else:\n                self.im = None\n        self.mode = temp_mode\n        self._frame_palette = None\n\n        super().load_prepare()\n\n    def load_end(self):\n        if self.__frame == 0:\n            if self.mode == \"P\" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:\n                if self._frame_transparency is not None:\n                    self.im.putpalettealpha(self._frame_transparency, 0)\n                    self.mode = \"RGBA\"\n                else:\n                    self.mode = \"RGB\"\n                self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)\n            return\n        if not self._prev_im:\n            return\n        if self._frame_transparency is not None:\n            self.im.putpalettealpha(self._frame_transparency, 0)\n            frame_im = self.im.convert(\"RGBA\")\n        else:\n            frame_im = self.im.convert(\"RGB\")\n        frame_im = self._crop(frame_im, self.dispose_extent)\n\n        self.im = self._prev_im\n        self.mode = self.im.mode\n        if frame_im.mode == \"RGBA\":\n            self.im.paste(frame_im, self.dispose_extent, frame_im)\n        else:\n            self.im.paste(frame_im, self.dispose_extent)\n\n    def tell(self):\n        return self.__frame\n\n\n# --------------------------------------------------------------------\n# Write GIF files\n\n\nRAWMODE = {\"1\": \"L\", \"L\": \"L\", \"P\": \"P\"}\n\n\ndef _normalize_mode(im):\n    \"\"\"\n    Takes an image (or frame), returns an image in a mode that is appropriate\n    for saving in a Gif.\n\n    It may return the original image, or it may return an image converted to\n    palette or 'L' mode.\n\n    :param im: Image object\n    :returns: Image object\n    \"\"\"\n    if im.mode in RAWMODE:\n        im.load()\n        return im\n    if Image.getmodebase(im.mode) == \"RGB\":\n        im = im.convert(\"P\", palette=Image.Palette.ADAPTIVE)\n        if im.palette.mode == \"RGBA\":\n            for rgba in im.palette.colors.keys():\n                if rgba[3] == 0:\n                    im.info[\"transparency\"] = im.palette.colors[rgba]\n                    break\n        return im\n    return im.convert(\"L\")\n\n\ndef _normalize_palette(im, palette, info):\n    \"\"\"\n    Normalizes the palette for image.\n      - Sets the palette to the incoming palette, if provided.\n      - Ensures that there's a palette for L mode images\n      - Optimizes the palette if necessary/desired.\n\n    :param im: Image object\n    :param palette: bytes object containing the source palette, or ....\n    :param info: encoderinfo\n    :returns: Image object\n    \"\"\"\n    source_palette = None\n    if palette:\n        # a bytes palette\n        if isinstance(palette, (bytes, bytearray, list)):\n            source_palette = bytearray(palette[:768])\n        if isinstance(palette, ImagePalette.ImagePalette):\n            source_palette = bytearray(palette.palette)\n\n    if im.mode == \"P\":\n        if not source_palette:\n            source_palette = im.im.getpalette(\"RGB\")[:768]\n    else:  # L-mode\n        if not source_palette:\n            source_palette = bytearray(i // 3 for i in range(768))\n        im.palette = ImagePalette.ImagePalette(\"RGB\", palette=source_palette)\n\n    if palette:\n        used_palette_colors = []\n        for i in range(0, len(source_palette), 3):\n            source_color = tuple(source_palette[i : i + 3])\n            index = im.palette.colors.get(source_color)\n            if index in used_palette_colors:\n                index = None\n            used_palette_colors.append(index)\n        for i, index in enumerate(used_palette_colors):\n            if index is None:\n                for j in range(len(used_palette_colors)):\n                    if j not in used_palette_colors:\n                        used_palette_colors[i] = j\n                        break\n        im = im.remap_palette(used_palette_colors)\n    else:\n        used_palette_colors = _get_optimize(im, info)\n        if used_palette_colors is not None:\n            return im.remap_palette(used_palette_colors, source_palette)\n\n    im.palette.palette = source_palette\n    return im\n\n\ndef _write_single_frame(im, fp, palette):\n    im_out = _normalize_mode(im)\n    for k, v in im_out.info.items():\n        im.encoderinfo.setdefault(k, v)\n    im_out = _normalize_palette(im_out, palette, im.encoderinfo)\n\n    for s in _get_global_header(im_out, im.encoderinfo):\n        fp.write(s)\n\n    # local image header\n    flags = 0\n    if get_interlace(im):\n        flags = flags | 64\n    _write_local_header(fp, im, (0, 0), flags)\n\n    im_out.encoderconfig = (8, get_interlace(im))\n    ImageFile._save(im_out, fp, [(\"gif\", (0, 0) + im.size, 0, RAWMODE[im_out.mode])])\n\n    fp.write(b\"\\0\")  # end of image data\n\n\ndef _write_multiple_frames(im, fp, palette):\n\n    duration = im.encoderinfo.get(\"duration\")\n    disposal = im.encoderinfo.get(\"disposal\", im.info.get(\"disposal\"))\n\n    im_frames = []\n    frame_count = 0\n    background_im = None\n    for imSequence in itertools.chain([im], im.encoderinfo.get(\"append_images\", [])):\n        for im_frame in ImageSequence.Iterator(imSequence):\n            # a copy is required here since seek can still mutate the image\n            im_frame = _normalize_mode(im_frame.copy())\n            if frame_count == 0:\n                for k, v in im_frame.info.items():\n                    if k == \"transparency\":\n                        continue\n                    im.encoderinfo.setdefault(k, v)\n\n            encoderinfo = im.encoderinfo.copy()\n            im_frame = _normalize_palette(im_frame, palette, encoderinfo)\n            if \"transparency\" in im_frame.info:\n                encoderinfo.setdefault(\"transparency\", im_frame.info[\"transparency\"])\n            if isinstance(duration, (list, tuple)):\n                encoderinfo[\"duration\"] = duration[frame_count]\n            elif duration is None and \"duration\" in im_frame.info:\n                encoderinfo[\"duration\"] = im_frame.info[\"duration\"]\n            if isinstance(disposal, (list, tuple)):\n                encoderinfo[\"disposal\"] = disposal[frame_count]\n            frame_count += 1\n\n            if im_frames:\n                # delta frame\n                previous = im_frames[-1]\n                if encoderinfo.get(\"disposal\") == 2:\n                    if background_im is None:\n                        color = im.encoderinfo.get(\n                            \"transparency\", im.info.get(\"transparency\", (0, 0, 0))\n                        )\n                        background = _get_background(im_frame, color)\n                        background_im = Image.new(\"P\", im_frame.size, background)\n                        background_im.putpalette(im_frames[0][\"im\"].palette)\n                    base_im = background_im\n                else:\n                    base_im = previous[\"im\"]\n                if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):\n                    delta = ImageChops.subtract_modulo(im_frame, base_im)\n                else:\n                    delta = ImageChops.subtract_modulo(\n                        im_frame.convert(\"RGB\"), base_im.convert(\"RGB\")\n                    )\n                bbox = delta.getbbox()\n                if not bbox:\n                    # This frame is identical to the previous frame\n                    if encoderinfo.get(\"duration\"):\n                        previous[\"encoderinfo\"][\"duration\"] += encoderinfo[\"duration\"]\n                    continue\n            else:\n                bbox = None\n            im_frames.append({\"im\": im_frame, \"bbox\": bbox, \"encoderinfo\": encoderinfo})\n\n    if len(im_frames) > 1:\n        for frame_data in im_frames:\n            im_frame = frame_data[\"im\"]\n            if not frame_data[\"bbox\"]:\n                # global header\n                for s in _get_global_header(im_frame, frame_data[\"encoderinfo\"]):\n                    fp.write(s)\n                offset = (0, 0)\n            else:\n                # compress difference\n                if not palette:\n                    frame_data[\"encoderinfo\"][\"include_color_table\"] = True\n\n                im_frame = im_frame.crop(frame_data[\"bbox\"])\n                offset = frame_data[\"bbox\"][:2]\n            _write_frame_data(fp, im_frame, offset, frame_data[\"encoderinfo\"])\n        return True\n    elif \"duration\" in im.encoderinfo and isinstance(\n        im.encoderinfo[\"duration\"], (list, tuple)\n    ):\n        # Since multiple frames will not be written, add together the frame durations\n        im.encoderinfo[\"duration\"] = sum(im.encoderinfo[\"duration\"])\n\n\ndef _save_all(im, fp, filename):\n    _save(im, fp, filename, save_all=True)\n\n\ndef _save(im, fp, filename, save_all=False):\n    # header\n    if \"palette\" in im.encoderinfo or \"palette\" in im.info:\n        palette = im.encoderinfo.get(\"palette\", im.info.get(\"palette\"))\n    else:\n        palette = None\n        im.encoderinfo[\"optimize\"] = im.encoderinfo.get(\"optimize\", True)\n\n    if not save_all or not _write_multiple_frames(im, fp, palette):\n        _write_single_frame(im, fp, palette)\n\n    fp.write(b\";\")  # end of file\n\n    if hasattr(fp, \"flush\"):\n        fp.flush()\n\n\ndef get_interlace(im):\n    interlace = im.encoderinfo.get(\"interlace\", 1)\n\n    # workaround for @PIL153\n    if min(im.size) < 16:\n        interlace = 0\n\n    return interlace\n\n\ndef _write_local_header(fp, im, offset, flags):\n    transparent_color_exists = False\n    try:\n        if \"transparency\" in im.encoderinfo:\n            transparency = im.encoderinfo[\"transparency\"]\n        else:\n            transparency = im.info[\"transparency\"]\n        transparency = int(transparency)\n    except (KeyError, ValueError):\n        pass\n    else:\n        # optimize the block away if transparent color is not used\n        transparent_color_exists = True\n\n        used_palette_colors = _get_optimize(im, im.encoderinfo)\n        if used_palette_colors is not None:\n            # adjust the transparency index after optimize\n            try:\n                transparency = used_palette_colors.index(transparency)\n            except ValueError:\n                transparent_color_exists = False\n\n    if \"duration\" in im.encoderinfo:\n        duration = int(im.encoderinfo[\"duration\"] / 10)\n    else:\n        duration = 0\n\n    disposal = int(im.encoderinfo.get(\"disposal\", 0))\n\n    if transparent_color_exists or duration != 0 or disposal:\n        packed_flag = 1 if transparent_color_exists else 0\n        packed_flag |= disposal << 2\n        if not transparent_color_exists:\n            transparency = 0\n\n        fp.write(\n            b\"!\"\n            + o8(249)  # extension intro\n            + o8(4)  # length\n            + o8(packed_flag)  # packed fields\n            + o16(duration)  # duration\n            + o8(transparency)  # transparency index\n            + o8(0)\n        )\n\n    include_color_table = im.encoderinfo.get(\"include_color_table\")\n    if include_color_table:\n        palette_bytes = _get_palette_bytes(im)\n        color_table_size = _get_color_table_size(palette_bytes)\n        if color_table_size:\n            flags = flags | 128  # local color table flag\n            flags = flags | color_table_size\n\n    fp.write(\n        b\",\"\n        + o16(offset[0])  # offset\n        + o16(offset[1])\n        + o16(im.size[0])  # size\n        + o16(im.size[1])\n        + o8(flags)  # flags\n    )\n    if include_color_table and color_table_size:\n        fp.write(_get_header_palette(palette_bytes))\n    fp.write(o8(8))  # bits\n\n\ndef _save_netpbm(im, fp, filename):\n\n    # Unused by default.\n    # To use, uncomment the register_save call at the end of the file.\n    #\n    # If you need real GIF compression and/or RGB quantization, you\n    # can use the external NETPBM/PBMPLUS utilities.  See comments\n    # below for information on how to enable this.\n    tempfile = im._dump()\n\n    try:\n        with open(filename, \"wb\") as f:\n            if im.mode != \"RGB\":\n                subprocess.check_call(\n                    [\"ppmtogif\", tempfile], stdout=f, stderr=subprocess.DEVNULL\n                )\n            else:\n                # Pipe ppmquant output into ppmtogif\n                # \"ppmquant 256 %s | ppmtogif > %s\" % (tempfile, filename)\n                quant_cmd = [\"ppmquant\", \"256\", tempfile]\n                togif_cmd = [\"ppmtogif\"]\n                quant_proc = subprocess.Popen(\n                    quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL\n                )\n                togif_proc = subprocess.Popen(\n                    togif_cmd,\n                    stdin=quant_proc.stdout,\n                    stdout=f,\n                    stderr=subprocess.DEVNULL,\n                )\n\n                # Allow ppmquant to receive SIGPIPE if ppmtogif exits\n                quant_proc.stdout.close()\n\n                retcode = quant_proc.wait()\n                if retcode:\n                    raise subprocess.CalledProcessError(retcode, quant_cmd)\n\n                retcode = togif_proc.wait()\n                if retcode:\n                    raise subprocess.CalledProcessError(retcode, togif_cmd)\n    finally:\n        try:\n            os.unlink(tempfile)\n        except OSError:\n            pass\n\n\n# Force optimization so that we can test performance against\n# cases where it took lots of memory and time previously.\n_FORCE_OPTIMIZE = False\n\n\ndef _get_optimize(im, info):\n    \"\"\"\n    Palette optimization is a potentially expensive operation.\n\n    This function determines if the palette should be optimized using\n    some heuristics, then returns the list of palette entries in use.\n\n    :param im: Image object\n    :param info: encoderinfo\n    :returns: list of indexes of palette entries in use, or None\n    \"\"\"\n    if im.mode in (\"P\", \"L\") and info and info.get(\"optimize\", 0):\n        # Potentially expensive operation.\n\n        # The palette saves 3 bytes per color not used, but palette\n        # lengths are restricted to 3*(2**N) bytes. Max saving would\n        # be 768 -> 6 bytes if we went all the way down to 2 colors.\n        # * If we're over 128 colors, we can't save any space.\n        # * If there aren't any holes, it's not worth collapsing.\n        # * If we have a 'large' image, the palette is in the noise.\n\n        # create the new palette if not every color is used\n        optimise = _FORCE_OPTIMIZE or im.mode == \"L\"\n        if optimise or im.width * im.height < 512 * 512:\n            # check which colors are used\n            used_palette_colors = []\n            for i, count in enumerate(im.histogram()):\n                if count:\n                    used_palette_colors.append(i)\n\n            if optimise or max(used_palette_colors) >= len(used_palette_colors):\n                return used_palette_colors\n\n            num_palette_colors = len(im.palette.palette) // Image.getmodebands(\n                im.palette.mode\n            )\n            current_palette_size = 1 << (num_palette_colors - 1).bit_length()\n            if (\n                # check that the palette would become smaller when saved\n                len(used_palette_colors) <= current_palette_size // 2\n                # check that the palette is not already the smallest possible size\n                and current_palette_size > 2\n            ):\n                return used_palette_colors\n\n\ndef _get_color_table_size(palette_bytes):\n    # calculate the palette size for the header\n    if not palette_bytes:\n        return 0\n    elif len(palette_bytes) < 9:\n        return 1\n    else:\n        return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1\n\n\ndef _get_header_palette(palette_bytes):\n    \"\"\"\n    Returns the palette, null padded to the next power of 2 (*3) bytes\n    suitable for direct inclusion in the GIF header\n\n    :param palette_bytes: Unpadded palette bytes, in RGBRGB form\n    :returns: Null padded palette\n    \"\"\"\n    color_table_size = _get_color_table_size(palette_bytes)\n\n    # add the missing amount of bytes\n    # the palette has to be 2<<n in size\n    actual_target_size_diff = (2 << color_table_size) - len(palette_bytes) // 3\n    if actual_target_size_diff > 0:\n        palette_bytes += o8(0) * 3 * actual_target_size_diff\n    return palette_bytes\n\n\ndef _get_palette_bytes(im):\n    \"\"\"\n    Gets the palette for inclusion in the gif header\n\n    :param im: Image object\n    :returns: Bytes, len<=768 suitable for inclusion in gif header\n    \"\"\"\n    return im.palette.palette\n\n\ndef _get_background(im, info_background):\n    background = 0\n    if info_background:\n        background = info_background\n        if isinstance(background, tuple):\n            # WebPImagePlugin stores an RGBA value in info[\"background\"]\n            # So it must be converted to the same format as GifImagePlugin's\n            # info[\"background\"] - a global color table index\n            try:\n                background = im.palette.getcolor(background, im)\n            except ValueError as e:\n                if str(e) == \"cannot allocate more than 256 colors\":\n                    # If all 256 colors are in use,\n                    # then there is no need for the background color\n                    return 0\n                else:\n                    raise\n    return background\n\n\ndef _get_global_header(im, info):\n    \"\"\"Return a list of strings representing a GIF header\"\"\"\n\n    # Header Block\n    # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp\n\n    version = b\"87a\"\n    if im.info.get(\"version\") == b\"89a\" or (\n        info\n        and (\n            \"transparency\" in info\n            or \"loop\" in info\n            or info.get(\"duration\")\n            or info.get(\"comment\")\n        )\n    ):\n        version = b\"89a\"\n\n    background = _get_background(im, info.get(\"background\"))\n\n    palette_bytes = _get_palette_bytes(im)\n    color_table_size = _get_color_table_size(palette_bytes)\n\n    header = [\n        b\"GIF\"  # signature\n        + version  # version\n        + o16(im.size[0])  # canvas width\n        + o16(im.size[1]),  # canvas height\n        # Logical Screen Descriptor\n        # size of global color table + global color table flag\n        o8(color_table_size + 128),  # packed fields\n        # background + reserved/aspect\n        o8(background) + o8(0),\n        # Global Color Table\n        _get_header_palette(palette_bytes),\n    ]\n    if \"loop\" in info:\n        header.append(\n            b\"!\"\n            + o8(255)  # extension intro\n            + o8(11)\n            + b\"NETSCAPE2.0\"\n            + o8(3)\n            + o8(1)\n            + o16(info[\"loop\"])  # number of loops\n            + o8(0)\n        )\n    if info.get(\"comment\"):\n        comment_block = b\"!\" + o8(254)  # extension intro\n\n        comment = info[\"comment\"]\n        if isinstance(comment, str):\n            comment = comment.encode()\n        for i in range(0, len(comment), 255):\n            subblock = comment[i : i + 255]\n            comment_block += o8(len(subblock)) + subblock\n\n        comment_block += o8(0)\n        header.append(comment_block)\n    return header\n\n\ndef _write_frame_data(fp, im_frame, offset, params):\n    try:\n        im_frame.encoderinfo = params\n\n        # local image header\n        _write_local_header(fp, im_frame, offset, 0)\n\n        ImageFile._save(\n            im_frame, fp, [(\"gif\", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])]\n        )\n\n        fp.write(b\"\\0\")  # end of image data\n    finally:\n        del im_frame.encoderinfo\n\n\n# --------------------------------------------------------------------\n# Legacy GIF utilities\n\n\ndef getheader(im, palette=None, info=None):\n    \"\"\"\n    Legacy Method to get Gif data from image.\n\n    Warning:: May modify image data.\n\n    :param im: Image object\n    :param palette: bytes object containing the source palette, or ....\n    :param info: encoderinfo\n    :returns: tuple of(list of header items, optimized palette)\n\n    \"\"\"\n    used_palette_colors = _get_optimize(im, info)\n\n    if info is None:\n        info = {}\n\n    if \"background\" not in info and \"background\" in im.info:\n        info[\"background\"] = im.info[\"background\"]\n\n    im_mod = _normalize_palette(im, palette, info)\n    im.palette = im_mod.palette\n    im.im = im_mod.im\n    header = _get_global_header(im, info)\n\n    return header, used_palette_colors\n\n\ndef getdata(im, offset=(0, 0), **params):\n    \"\"\"\n    Legacy Method\n\n    Return a list of strings representing this image.\n    The first string is a local image header, the rest contains\n    encoded image data.\n\n    To specify duration, add the time in milliseconds,\n    e.g. ``getdata(im_frame, duration=1000)``\n\n    :param im: Image object\n    :param offset: Tuple of (x, y) pixels. Defaults to (0, 0)\n    :param \\\\**params: e.g. duration or other encoder info parameters\n    :returns: List of bytes containing GIF encoded frame data\n\n    \"\"\"\n\n    class Collector:\n        data = []\n\n        def write(self, data):\n            self.data.append(data)\n\n    im.load()  # make sure raster data is available\n\n    fp = Collector()\n\n    _write_frame_data(fp, im, offset, params)\n\n    return fp.data\n\n\n# --------------------------------------------------------------------\n# Registry\n\nImage.register_open(GifImageFile.format, GifImageFile, _accept)\nImage.register_save(GifImageFile.format, _save)\nImage.register_save_all(GifImageFile.format, _save_all)\nImage.register_extension(GifImageFile.format, \".gif\")\nImage.register_mime(GifImageFile.format, \"image/gif\")\n\n#\n# Uncomment the following line if you wish to use NETPBM/PBMPLUS\n# instead of the built-in \"uncompressed\" GIF encoder\n\n# Image.register_save(GifImageFile.format, _save_netpbm)\n"}
A user will ask you to solve a task. You should first draft your thinking process (inner monologue). Then, generate the solution.

Your response format must follow the template below:
<think>
Your thoughts or/and draft, like working through an exercise on scratch paper. Be as casual and as long as you want until you are confident to generate a correct solution.
</think>
<solution>
Final solution presented to the user.
</solution>
system
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Saving GIF with Non-Opaque RGBA Background Raises ValueError

**Description:**
When attempting to save a GIF image with a non-opaque RGBA background, a `ValueError` is raised. This occurs because the GIF palette does not support non-opaque colors.

**Example Code:**
```python
from PIL import Image

# Create an image with a transparent background
image = Image.new("RGBA", (100, 100), (0, 0, 0, 0))
image.info["background"] = (0, 0, 0, 0)

# Attempt to save as GIF
image.save("temp.gif")
```

**Expected Behavior:**
The image should be saved as a GIF with a default background color or handle the transparency appropriately without raising an error.

**Actual Behavior:**
Saving the image results in the following error:
```
ValueError: cannot add non-opaque RGBA color to RGB palette
```
[/ISSUE]
--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.

--- BEGIN FILE ---
```
### src/PIL/GifImagePlugin.py
#
# The Python Imaging Library.
# $Id$
#
# GIF file handling
#
# History:
# 1995-09-01 fl   Created
# 1996-12-14 fl   Added interlace support
# 1996-12-30 fl   Added animation support
# 1997-01-05 fl   Added write support, fixed local colour map bug
# 1997-02-23 fl   Make sure to load raster data in getdata()
# 1997-07-05 fl   Support external decoder (0.4)
# 1998-07-09 fl   Handle all modes when saving (0.5)
# 1998-07-15 fl   Renamed offset attribute to avoid name clash
# 2001-04-16 fl   Added rewind support (seek to frame 0) (0.6)
# 2001-04-17 fl   Added palette optimization (0.7)
# 2002-06-06 fl   Added transparency support for save (0.8)
# 2004-02-24 fl   Disable interlacing for small images
#
# Copyright (c) 1997-2004 by Secret Labs AB
# Copyright (c) 1995-2004 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#

import itertools
import math
import os
import subprocess
from enum import IntEnum

from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16le as i16
from ._binary import o8
from ._binary import o16le as o16


class LoadingStrategy(IntEnum):
    """.. versionadded:: 9.1.0"""

    RGB_AFTER_FIRST = 0
    RGB_AFTER_DIFFERENT_PALETTE_ONLY = 1
    RGB_ALWAYS = 2


#: .. versionadded:: 9.1.0
LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST

# --------------------------------------------------------------------
# Identify/read GIF files


def _accept(prefix):
    return prefix[:6] in [b"GIF87a", b"GIF89a"]


##
# Image plugin for GIF images.  This plugin supports both GIF87 and
# GIF89 images.


class GifImageFile(ImageFile.ImageFile):

    format = "GIF"
    format_description = "Compuserve GIF"
    _close_exclusive_fp_after_loading = False

    global_palette = None

    def data(self):
        s = self.fp.read(1)
        if s and s[0]:
            return self.fp.read(s[0])
        return None

    def _is_palette_needed(self, p):
        for i in range(0, len(p), 3):
            if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
                return True
        return False

    def _open(self):

        # Screen
        s = self.fp.read(13)
        if not _accept(s):
            raise SyntaxError("not a GIF file")

        self.info["version"] = s[:6]
        self._size = i16(s, 6), i16(s, 8)
        self.tile = []
        flags = s[10]
        bits = (flags & 7) + 1

        if flags & 128:
            # get global palette
            self.info["background"] = s[11]
            # check if palette contains colour indices
            p = self.fp.read(3 << bits)
            if self._is_palette_needed(p):
                p = ImagePalette.raw("RGB", p)
                self.global_palette = self.palette = p

        self._fp = self.fp  # FIXME: hack
        self.__rewind = self.fp.tell()
        self._n_frames = None
        self._is_animated = None
        self._seek(0)  # get ready to read first frame

    @property
    def n_frames(self):
        if self._n_frames is None:
            current = self.tell()
            try:
                while True:
                    self._seek(self.tell() + 1, False)
            except EOFError:
                self._n_frames = self.tell() + 1
            self.seek(current)
        return self._n_frames

    @property
    def is_animated(self):
        if self._is_animated is None:
            if self._n_frames is not None:
                self._is_animated = self._n_frames != 1
            else:
                current = self.tell()
                if current:
                    self._is_animated = True
                else:
                    try:
                        self._seek(1, False)
                        self._is_animated = True
                    except EOFError:
                        self._is_animated = False

                    self.seek(current)
        return self._is_animated

    def seek(self, frame):
        if not self._seek_check(frame):
            return
        if frame < self.__frame:
            self.im = None
            self._seek(0)

        last_frame = self.__frame
        for f in range(self.__frame + 1, frame + 1):
            try:
                self._seek(f)
            except EOFError as e:
                self.seek(last_frame)
                raise EOFError("no more images in GIF file") from e

    def _seek(self, frame, update_image=True):

        if frame == 0:
            # rewind
            self.__offset = 0
            self.dispose = None
            self.__frame = -1
            self._fp.seek(self.__rewind)
            self.disposal_method = 0
            if "comment" in self.info:
                del self.info["comment"]
        else:
            # ensure that the previous frame was loaded
            if self.tile and update_image:
                self.load()

        if frame != self.__frame + 1:
            raise ValueError(f"cannot seek to frame {frame}")

        self.fp = self._fp
        if self.__offset:
            # backup to last frame
            self.fp.seek(self.__offset)
            while self.data():
                pass
            self.__offset = 0

        s = self.fp.read(1)
        if not s or s == b";":
            raise EOFError

        palette = None

        info = {}
        frame_transparency = None
        interlace = None
        frame_dispose_extent = None
        while True:

            if not s:
                s = self.fp.read(1)
            if not s or s == b";":
                break

            elif s == b"!":
                #
                # extensions
                #
                s = self.fp.read(1)
                block = self.data()
                if s[0] == 249:
                    #
                    # graphic control extension
                    #
                    flags = block[0]
                    if flags & 1:
                        frame_transparency = block[3]
                    info["duration"] = i16(block, 1) * 10

                    # disposal method - find the value of bits 4 - 6
                    dispose_bits = 0b00011100 & flags
                    dispose_bits = dispose_bits >> 2
                    if dispose_bits:
                        # only set the dispose if it is not
                        # unspecified. I'm not sure if this is
                        # correct, but it seems to prevent the last
                        # frame from looking odd for some animations
                        self.disposal_method = dispose_bits
                elif s[0] == 254:
                    #
                    # comment extension
                    #
                    comment = b""

                    # Read this comment block
                    while block:
                        comment += block
                        block = self.data()

                    if "comment" in info:
                        # If multiple comment blocks in frame, separate with \n
                        info["comment"] += b"\n" + comment
                    else:
                        info["comment"] = comment
                    s = None
                    continue
                elif s[0] == 255 and frame == 0:
                    #
                    # application extension
                    #
                    info["extension"] = block, self.fp.tell()
                    if block[:11] == b"NETSCAPE2.0":
                        block = self.data()
                        if len(block) >= 3 and block[0] == 1:
                            self.info["loop"] = i16(block, 1)
                while self.data():
                    pass

            elif s == b",":
                #
                # local image
                #
                s = self.fp.read(9)

                # extent
                x0, y0 = i16(s, 0), i16(s, 2)
                x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
                if (x1 > self.size[0] or y1 > self.size[1]) and update_image:
                    self._size = max(x1, self.size[0]), max(y1, self.size[1])
                    Image._decompression_bomb_check(self._size)
                frame_dispose_extent = x0, y0, x1, y1
                flags = s[8]

                interlace = (flags & 64) != 0

                if flags & 128:
                    bits = (flags & 7) + 1
                    p = self.fp.read(3 << bits)
                    if self._is_palette_needed(p):
                        palette = ImagePalette.raw("RGB", p)
                    else:
                        palette = False

                # image data
                bits = self.fp.read(1)[0]
                self.__offset = self.fp.tell()
                break

            else:
                pass
                # raise OSError, "illegal GIF tag `%x`" % s[0]
            s = None

        if interlace is None:
            # self._fp = None
            raise EOFError

        self.__frame = frame
        if not update_image:
            return

        self.tile = []

        if self.dispose:
            self.im.paste(self.dispose, self.dispose_extent)

        self._frame_palette = palette if palette is not None else self.global_palette
        self._frame_transparency = frame_transparency
        if frame == 0:
            if self._frame_palette:
                if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
                    self.mode = "RGBA" if frame_transparency is not None else "RGB"
                else:
                    self.mode = "P"
            else:
                self.mode = "L"

            if not palette and self.global_palette:
                from copy import copy

                palette = copy(self.global_palette)
            self.palette = palette
        else:
            if self.mode == "P":
                if (
                    LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
                    or palette
                ):
                    self.pyaccess = None
                    if "transparency" in self.info:
                        self.im.putpalettealpha(self.info["transparency"], 0)
                        self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
                        self.mode = "RGBA"
                        del self.info["transparency"]
                    else:
                        self.mode = "RGB"
                        self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)

        def _rgb(color):
            if self._frame_palette:
                color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
            else:
                color = (color, color, color)
            return color

        self.dispose_extent = frame_dispose_extent
        try:
            if self.disposal_method < 2:
                # do not dispose or none specified
                self.dispose = None
            elif self.disposal_method == 2:
                # replace with background colour

                # only dispose the extent in this frame
                x0, y0, x1, y1 = self.dispose_extent
                dispose_size = (x1 - x0, y1 - y0)

                Image._decompression_bomb_check(dispose_size)

                # by convention, attempt to use transparency first
                dispose_mode = "P"
                color = self.info.get("transparency", frame_transparency)
                if color is not None:
                    if self.mode in ("RGB", "RGBA"):
                        dispose_mode = "RGBA"
                        color = _rgb(color) + (0,)
                else:
                    color = self.info.get("background", 0)
                    if self.mode in ("RGB", "RGBA"):
                        dispose_mode = "RGB"
                        color = _rgb(color)
                self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
            else:
                # replace with previous contents
                if self.im is not None:
                    # only dispose the extent in this frame
                    self.dispose = self._crop(self.im, self.dispose_extent)
                elif frame_transparency is not None:
                    x0, y0, x1, y1 = self.dispose_extent
                    dispose_size = (x1 - x0, y1 - y0)

                    Image._decompression_bomb_check(dispose_size)
                    dispose_mode = "P"
                    color = frame_transparency
                    if self.mode in ("RGB", "RGBA"):
                        dispose_mode = "RGBA"
                        color = _rgb(frame_transparency) + (0,)
                    self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
        except AttributeError:
            pass

        if interlace is not None:
            transparency = -1
            if frame_transparency is not None:
                if frame == 0:
                    if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS:
                        self.info["transparency"] = frame_transparency
                elif self.mode not in ("RGB", "RGBA"):
                    transparency = frame_transparency
            self.tile = [
                (
                    "gif",
                    (x0, y0, x1, y1),
                    self.__offset,
                    (bits, interlace, transparency),
                )
            ]

        if info.get("comment"):
            self.info["comment"] = info["comment"]
        for k in ["duration", "extension"]:
            if k in info:
                self.info[k] = info[k]
            elif k in self.info:
                del self.info[k]

    def load_prepare(self):
        temp_mode = "P" if self._frame_palette else "L"
        self._prev_im = None
        if self.__frame == 0:
            if self._frame_transparency is not None:
                self.im = Image.core.fill(
                    temp_mode, self.size, self._frame_transparency
                )
        elif self.mode in ("RGB", "RGBA"):
            self._prev_im = self.im
            if self._frame_palette:
                self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
                self.im.putpalette(*self._frame_palette.getdata())
            else:
                self.im = None
        self.mode = temp_mode
        self._frame_palette = None

        super().load_prepare()

    def load_end(self):
        if self.__frame == 0:
            if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
                if self._frame_transparency is not None:
                    self.im.putpalettealpha(self._frame_transparency, 0)
                    self.mode = "RGBA"
                else:
                    self.mode = "RGB"
                self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
            return
        if not self._prev_im:
            return
        if self._frame_transparency is not None:
            self.im.putpalettealpha(self._frame_transparency, 0)
            frame_im = self.im.convert("RGBA")
        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":
            self.im.paste(frame_im, self.dispose_extent, frame_im)
        else:
            self.im.paste(frame_im, self.dispose_extent)

    def tell(self):
        return self.__frame


# --------------------------------------------------------------------
# Write GIF files


RAWMODE = {"1": "L", "L": "L", "P": "P"}


def _normalize_mode(im):
    """
    Takes an image (or frame), returns an image in a mode that is appropriate
    for saving in a Gif.

    It may return the original image, or it may return an image converted to
    palette or 'L' mode.

    :param im: Image object
    :returns: Image object
    """
    if im.mode in RAWMODE:
        im.load()
        return im
    if Image.getmodebase(im.mode) == "RGB":
        im = im.convert("P", palette=Image.Palette.ADAPTIVE)
        if im.palette.mode == "RGBA":
            for rgba in im.palette.colors.keys():
                if rgba[3] == 0:
                    im.info["transparency"] = im.palette.colors[rgba]
                    break
        return im
    return im.convert("L")


def _normalize_palette(im, palette, info):
    """
    Normalizes the palette for image.
      - Sets the palette to the incoming palette, if provided.
      - Ensures that there's a palette for L mode images
      - Optimizes the palette if necessary/desired.

    :param im: Image object
    :param palette: bytes object containing the source palette, or ....
    :param info: encoderinfo
    :returns: Image object
    """
    source_palette = None
    if palette:
        # a bytes palette
        if isinstance(palette, (bytes, bytearray, list)):
            source_palette = bytearray(palette[:768])
        if isinstance(palette, ImagePalette.ImagePalette):
            source_palette = bytearray(palette.palette)

    if im.mode == "P":
        if not source_palette:
            source_palette = im.im.getpalette("RGB")[:768]
    else:  # L-mode
        if not source_palette:
            source_palette = bytearray(i // 3 for i in range(768))
        im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)

    if palette:
        used_palette_colors = []
        for i in range(0, len(source_palette), 3):
            source_color = tuple(source_palette[i : i + 3])
            index = im.palette.colors.get(source_color)
            if index in used_palette_colors:
                index = None
            used_palette_colors.append(index)
        for i, index in enumerate(used_palette_colors):
            if index is None:
                for j in range(len(used_palette_colors)):
                    if j not in used_palette_colors:
                        used_palette_colors[i] = j
                        break
        im = im.remap_palette(used_palette_colors)
    else:
        used_palette_colors = _get_optimize(im, info)
        if used_palette_colors is not None:
            return im.remap_palette(used_palette_colors, source_palette)

    im.palette.palette = source_palette
    return im


def _write_single_frame(im, fp, palette):
    im_out = _normalize_mode(im)
    for k, v in im_out.info.items():
        im.encoderinfo.setdefault(k, v)
    im_out = _normalize_palette(im_out, palette, im.encoderinfo)

    for s in _get_global_header(im_out, im.encoderinfo):
        fp.write(s)

    # local image header
    flags = 0
    if get_interlace(im):
        flags = flags | 64
    _write_local_header(fp, im, (0, 0), flags)

    im_out.encoderconfig = (8, get_interlace(im))
    ImageFile._save(im_out, fp, [("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])])

    fp.write(b"\0")  # end of image data


def _write_multiple_frames(im, fp, palette):

    duration = im.encoderinfo.get("duration")
    disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))

    im_frames = []
    frame_count = 0
    background_im = None
    for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
        for im_frame in ImageSequence.Iterator(imSequence):
            # a copy is required here since seek can still mutate the image
            im_frame = _normalize_mode(im_frame.copy())
            if frame_count == 0:
                for k, v in im_frame.info.items():
                    if k == "transparency":
                        continue
                    im.encoderinfo.setdefault(k, v)

            encoderinfo = im.encoderinfo.copy()
            im_frame = _normalize_palette(im_frame, palette, encoderinfo)
            if "transparency" in im_frame.info:
                encoderinfo.setdefault("transparency", im_frame.info["transparency"])
            if isinstance(duration, (list, tuple)):
                encoderinfo["duration"] = duration[frame_count]
            elif duration is None and "duration" in im_frame.info:
                encoderinfo["duration"] = im_frame.info["duration"]
            if isinstance(disposal, (list, tuple)):
                encoderinfo["disposal"] = disposal[frame_count]
            frame_count += 1

            if im_frames:
                # delta frame
                previous = im_frames[-1]
                if encoderinfo.get("disposal") == 2:
                    if background_im is None:
                        color = im.encoderinfo.get(
                            "transparency", im.info.get("transparency", (0, 0, 0))
                        )
                        background = _get_background(im_frame, color)
                        background_im = Image.new("P", im_frame.size, background)
                        background_im.putpalette(im_frames[0]["im"].palette)
                    base_im = background_im
                else:
                    base_im = previous["im"]
                if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):
                    delta = ImageChops.subtract_modulo(im_frame, base_im)
                else:
                    delta = ImageChops.subtract_modulo(
                        im_frame.convert("RGB"), base_im.convert("RGB")
                    )
                bbox = delta.getbbox()
                if not bbox:
                    # This frame is identical to the previous frame
                    if encoderinfo.get("duration"):
                        previous["encoderinfo"]["duration"] += encoderinfo["duration"]
                    continue
            else:
                bbox = None
            im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})

    if len(im_frames) > 1:
        for frame_data in im_frames:
            im_frame = frame_data["im"]
            if not frame_data["bbox"]:
                # global header
                for s in _get_global_header(im_frame, frame_data["encoderinfo"]):
                    fp.write(s)
                offset = (0, 0)
            else:
                # compress difference
                if not palette:
                    frame_data["encoderinfo"]["include_color_table"] = True

                im_frame = im_frame.crop(frame_data["bbox"])
                offset = frame_data["bbox"][:2]
            _write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"])
        return True
    elif "duration" in im.encoderinfo and isinstance(
        im.encoderinfo["duration"], (list, tuple)
    ):
        # Since multiple frames will not be written, add together the frame durations
        im.encoderinfo["duration"] = sum(im.encoderinfo["duration"])


def _save_all(im, fp, filename):
    _save(im, fp, filename, save_all=True)


def _save(im, fp, filename, save_all=False):
    # header
    if "palette" in im.encoderinfo or "palette" in im.info:
        palette = im.encoderinfo.get("palette", im.info.get("palette"))
    else:
        palette = None
        im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)

    if not save_all or not _write_multiple_frames(im, fp, palette):
        _write_single_frame(im, fp, palette)

    fp.write(b";")  # end of file

    if hasattr(fp, "flush"):
        fp.flush()


def get_interlace(im):
    interlace = im.encoderinfo.get("interlace", 1)

    # workaround for @PIL153
    if min(im.size) < 16:
        interlace = 0

    return interlace


def _write_local_header(fp, im, offset, flags):
    transparent_color_exists = False
    try:
        if "transparency" in im.encoderinfo:
            transparency = im.encoderinfo["transparency"]
        else:
            transparency = im.info["transparency"]
        transparency = int(transparency)
    except (KeyError, ValueError):
        pass
    else:
        # optimize the block away if transparent color is not used
        transparent_color_exists = True

        used_palette_colors = _get_optimize(im, im.encoderinfo)
        if used_palette_colors is not None:
            # adjust the transparency index after optimize
            try:
                transparency = used_palette_colors.index(transparency)
            except ValueError:
                transparent_color_exists = False

    if "duration" in im.encoderinfo:
        duration = int(im.encoderinfo["duration"] / 10)
    else:
        duration = 0

    disposal = int(im.encoderinfo.get("disposal", 0))

    if transparent_color_exists or duration != 0 or disposal:
        packed_flag = 1 if transparent_color_exists else 0
        packed_flag |= disposal << 2
        if not transparent_color_exists:
            transparency = 0

        fp.write(
            b"!"
            + o8(249)  # extension intro
            + o8(4)  # length
            + o8(packed_flag)  # packed fields
            + o16(duration)  # duration
            + o8(transparency)  # transparency index
            + o8(0)
        )

    include_color_table = im.encoderinfo.get("include_color_table")
    if include_color_table:
        palette_bytes = _get_palette_bytes(im)
        color_table_size = _get_color_table_size(palette_bytes)
        if color_table_size:
            flags = flags | 128  # local color table flag
            flags = flags | color_table_size

    fp.write(
        b","
        + o16(offset[0])  # offset
        + o16(offset[1])
        + o16(im.size[0])  # size
        + o16(im.size[1])
        + o8(flags)  # flags
    )
    if include_color_table and color_table_size:
        fp.write(_get_header_palette(palette_bytes))
    fp.write(o8(8))  # bits


def _save_netpbm(im, fp, filename):

    # Unused by default.
    # To use, uncomment the register_save call at the end of the file.
    #
    # If you need real GIF compression and/or RGB quantization, you
    # can use the external NETPBM/PBMPLUS utilities.  See comments
    # below for information on how to enable this.
    tempfile = im._dump()

    try:
        with open(filename, "wb") as f:
            if im.mode != "RGB":
                subprocess.check_call(
                    ["ppmtogif", tempfile], stdout=f, stderr=subprocess.DEVNULL
                )
            else:
                # Pipe ppmquant output into ppmtogif
                # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename)
                quant_cmd = ["ppmquant", "256", tempfile]
                togif_cmd = ["ppmtogif"]
                quant_proc = subprocess.Popen(
                    quant_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
                )
                togif_proc = subprocess.Popen(
                    togif_cmd,
                    stdin=quant_proc.stdout,
                    stdout=f,
                    stderr=subprocess.DEVNULL,
                )

                # Allow ppmquant to receive SIGPIPE if ppmtogif exits
                quant_proc.stdout.close()

                retcode = quant_proc.wait()
                if retcode:
                    raise subprocess.CalledProcessError(retcode, quant_cmd)

                retcode = togif_proc.wait()
                if retcode:
                    raise subprocess.CalledProcessError(retcode, togif_cmd)
    finally:
        try:
            os.unlink(tempfile)
        except OSError:
            pass


# Force optimization so that we can test performance against
# cases where it took lots of memory and time previously.
_FORCE_OPTIMIZE = False


def _get_optimize(im, info):
    """
    Palette optimization is a potentially expensive operation.

    This function determines if the palette should be optimized using
    some heuristics, then returns the list of palette entries in use.

    :param im: Image object
    :param info: encoderinfo
    :returns: list of indexes of palette entries in use, or None
    """
    if im.mode in ("P", "L") and info and info.get("optimize", 0):
        # Potentially expensive operation.

        # The palette saves 3 bytes per color not used, but palette
        # lengths are restricted to 3*(2**N) bytes. Max saving would
        # be 768 -> 6 bytes if we went all the way down to 2 colors.
        # * If we're over 128 colors, we can't save any space.
        # * If there aren't any holes, it's not worth collapsing.
        # * If we have a 'large' image, the palette is in the noise.

        # create the new palette if not every color is used
        optimise = _FORCE_OPTIMIZE or im.mode == "L"
        if optimise or im.width * im.height < 512 * 512:
            # check which colors are used
            used_palette_colors = []
            for i, count in enumerate(im.histogram()):
                if count:
                    used_palette_colors.append(i)

            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


def _get_color_table_size(palette_bytes):
    # calculate the palette size for the header
    if not palette_bytes:
        return 0
    elif len(palette_bytes) < 9:
        return 1
    else:
        return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1


def _get_header_palette(palette_bytes):
    """
    Returns the palette, null padded to the next power of 2 (*3) bytes
    suitable for direct inclusion in the GIF header

    :param palette_bytes: Unpadded palette bytes, in RGBRGB form
    :returns: Null padded palette
    """
    color_table_size = _get_color_table_size(palette_bytes)

    # add the missing amount of bytes
    # the palette has to be 2<<n in size
    actual_target_size_diff = (2 << color_table_size) - len(palette_bytes) // 3
    if actual_target_size_diff > 0:
        palette_bytes += o8(0) * 3 * actual_target_size_diff
    return palette_bytes


def _get_palette_bytes(im):
    """
    Gets the palette for inclusion in the gif header

    :param im: Image object
    :returns: Bytes, len<=768 suitable for inclusion in gif header
    """
    return im.palette.palette


def _get_background(im, info_background):
    background = 0
    if info_background:
        background = info_background
        if isinstance(background, tuple):
            # WebPImagePlugin stores an RGBA value in info["background"]
            # So it must be converted to the same format as GifImagePlugin's
            # info["background"] - a global color table index
            try:
                background = im.palette.getcolor(background, im)
            except ValueError as e:
                if str(e) == "cannot allocate more than 256 colors":
                    # If all 256 colors are in use,
                    # then there is no need for the background color
                    return 0
                else:
                    raise
    return background


def _get_global_header(im, info):
    """Return a list of strings representing a GIF header"""

    # Header Block
    # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp

    version = b"87a"
    if im.info.get("version") == b"89a" or (
        info
        and (
            "transparency" in info
            or "loop" in info
            or info.get("duration")
            or info.get("comment")
        )
    ):
        version = b"89a"

    background = _get_background(im, info.get("background"))

    palette_bytes = _get_palette_bytes(im)
    color_table_size = _get_color_table_size(palette_bytes)

    header = [
        b"GIF"  # signature
        + version  # version
        + o16(im.size[0])  # canvas width
        + o16(im.size[1]),  # canvas height
        # Logical Screen Descriptor
        # size of global color table + global color table flag
        o8(color_table_size + 128),  # packed fields
        # background + reserved/aspect
        o8(background) + o8(0),
        # Global Color Table
        _get_header_palette(palette_bytes),
    ]
    if "loop" in info:
        header.append(
            b"!"
            + o8(255)  # extension intro
            + o8(11)
            + b"NETSCAPE2.0"
            + o8(3)
            + o8(1)
            + o16(info["loop"])  # number of loops
            + o8(0)
        )
    if info.get("comment"):
        comment_block = b"!" + o8(254)  # extension intro

        comment = info["comment"]
        if isinstance(comment, str):
            comment = comment.encode()
        for i in range(0, len(comment), 255):
            subblock = comment[i : i + 255]
            comment_block += o8(len(subblock)) + subblock

        comment_block += o8(0)
        header.append(comment_block)
    return header


def _write_frame_data(fp, im_frame, offset, params):
    try:
        im_frame.encoderinfo = params

        # local image header
        _write_local_header(fp, im_frame, offset, 0)

        ImageFile._save(
            im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])]
        )

        fp.write(b"\0")  # end of image data
    finally:
        del im_frame.encoderinfo


# --------------------------------------------------------------------
# Legacy GIF utilities


def getheader(im, palette=None, info=None):
    """
    Legacy Method to get Gif data from image.

    Warning:: May modify image data.

    :param im: Image object
    :param palette: bytes object containing the source palette, or ....
    :param info: encoderinfo
    :returns: tuple of(list of header items, optimized palette)

    """
    used_palette_colors = _get_optimize(im, info)

    if info is None:
        info = {}

    if "background" not in info and "background" in im.info:
        info["background"] = im.info["background"]

    im_mod = _normalize_palette(im, palette, info)
    im.palette = im_mod.palette
    im.im = im_mod.im
    header = _get_global_header(im, info)

    return header, used_palette_colors


def getdata(im, offset=(0, 0), **params):
    """
    Legacy Method

    Return a list of strings representing this image.
    The first string is a local image header, the rest contains
    encoded image data.

    To specify duration, add the time in milliseconds,
    e.g. ``getdata(im_frame, duration=1000)``

    :param im: Image object
    :param offset: Tuple of (x, y) pixels. Defaults to (0, 0)
    :param \\**params: e.g. duration or other encoder info parameters
    :returns: List of bytes containing GIF encoded frame data

    """

    class Collector:
        data = []

        def write(self, data):
            self.data.append(data)

    im.load()  # make sure raster data is available

    fp = Collector()

    _write_frame_data(fp, im, offset, params)

    return fp.data


# --------------------------------------------------------------------
# Registry

Image.register_open(GifImageFile.format, GifImageFile, _accept)
Image.register_save(GifImageFile.format, _save)
Image.register_save_all(GifImageFile.format, _save_all)
Image.register_extension(GifImageFile.format, ".gif")
Image.register_mime(GifImageFile.format, "image/gif")

#
# Uncomment the following line if you wish to use NETPBM/PBMPLUS
# instead of the built-in "uncompressed" GIF encoder

# Image.register_save(GifImageFile.format, _save_netpbm)

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
user