{
    "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_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_dispose2_background_frame(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im_list = [Image.new(\"RGBA\", (1, 20))]\n\n    different_frame = Image.new(\"RGBA\", (1, 20))\n    different_frame.putpixel((0, 10), (255, 0, 0, 255))\n    im_list.append(different_frame)\n\n    # Frame that matches the background\n    im_list.append(Image.new(\"RGBA\", (1, 20)))\n\n    im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)\n\n    with Image.open(out) as im:\n        assert im.n_frames == 3\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_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",
            "header": {
                "file": {
                    "path": "Tests/test_file_gif.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "926f5c1ee",
                "new_commit_hash": "2cbaf2805",
                "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": 677,
                            "length": 6
                        },
                        "new_range": {
                            "start": 677,
                            "length": 24
                        },
                        "section": "def test_dispose2_background(tmp_path):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        assert im.getpixel((0, 0)) == (255, 0, 0)",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "def test_dispose2_background_frame(tmp_path):",
                                "type": "added"
                            },
                            {
                                "content": "    out = str(tmp_path / \"temp.gif\")",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    im_list = [Image.new(\"RGBA\", (1, 20))]",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    different_frame = Image.new(\"RGBA\", (1, 20))",
                                "type": "added"
                            },
                            {
                                "content": "    different_frame.putpixel((0, 10), (255, 0, 0, 255))",
                                "type": "added"
                            },
                            {
                                "content": "    im_list.append(different_frame)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    # Frame that matches the background",
                                "type": "added"
                            },
                            {
                                "content": "    im_list.append(Image.new(\"RGBA\", (1, 20)))",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    with Image.open(out) as im:",
                                "type": "added"
                            },
                            {
                                "content": "        assert im.n_frames == 3",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "def test_transparency_in_second_frame(tmp_path):",
                                "type": "context"
                            },
                            {
                                "content": "    out = str(tmp_path / \"temp.gif\")",
                                "type": "context"
                            },
                            {
                                "content": "    with Image.open(\"Tests/images/different_transparency.gif\") as im:",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [],
                    "added_entities": [
                        {
                            "file_name": "Tests/test_file_gif.py",
                            "type": "function",
                            "name": "test_dispose2_background_frame",
                            "content": "def test_dispose2_background_frame(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im_list = [Image.new(\"RGBA\", (1, 20))]\n\n    different_frame = Image.new(\"RGBA\", (1, 20))\n    different_frame.putpixel((0, 10), (255, 0, 0, 255))\n    im_list.append(different_frame)\n\n    # Frame that matches the background\n    im_list.append(Image.new(\"RGBA\", (1, 20)))\n\n    im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)\n\n    with Image.open(out) as im:\n        assert im.n_frames == 3",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 680,
                            "end_lineno": 695,
                            "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 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 _getbbox(base_im, im_frame):\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    return delta.getbbox()\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                bbox = _getbbox(previous[\"im\"], im_frame)\n                if not bbox:\n                    # This frame is identical to the previous frame\n                    if duration:\n                        previous[\"encoderinfo\"][\"duration\"] += encoderinfo[\"duration\"]\n                    continue\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                    bbox = _getbbox(background_im, im_frame)\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",
            "header": {
                "file": {
                    "path": "src/PIL/GifImagePlugin.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "dd1b21f2e",
                "new_commit_hash": "367958048",
                "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": 565,
                            "length": 6
                        },
                        "new_range": {
                            "start": 565,
                            "length": 16
                        },
                        "section": "def _write_single_frame(im, fp, palette):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "    fp.write(b\"\\0\")  # end of image data",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "def _getbbox(base_im, im_frame):",
                                "type": "added"
                            },
                            {
                                "content": "    if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):",
                                "type": "added"
                            },
                            {
                                "content": "        delta = ImageChops.subtract_modulo(im_frame, base_im)",
                                "type": "added"
                            },
                            {
                                "content": "    else:",
                                "type": "added"
                            },
                            {
                                "content": "        delta = ImageChops.subtract_modulo(",
                                "type": "added"
                            },
                            {
                                "content": "            im_frame.convert(\"RGB\"), base_im.convert(\"RGB\")",
                                "type": "added"
                            },
                            {
                                "content": "        )",
                                "type": "added"
                            },
                            {
                                "content": "    return delta.getbbox()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "def _write_multiple_frames(im, fp, palette):",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    duration = im.encoderinfo.get(\"duration\")",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [],
                    "added_entities": [
                        {
                            "file_name": "src/PIL/GifImagePlugin.py",
                            "type": "function",
                            "name": "_getbbox",
                            "content": "def _getbbox(base_im, im_frame):\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    return delta.getbbox()",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 568,
                            "end_lineno": 575,
                            "parent": null
                        }
                    ],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 598,
                            "length": 6
                        },
                        "new_range": {
                            "start": 608,
                            "length": 12
                        },
                        "section": "def _write_multiple_frames(im, fp, palette):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "            if im_frames:",
                                "type": "context"
                            },
                            {
                                "content": "                # delta frame",
                                "type": "context"
                            },
                            {
                                "content": "                previous = im_frames[-1]",
                                "type": "context"
                            },
                            {
                                "content": "                bbox = _getbbox(previous[\"im\"], im_frame)",
                                "type": "added"
                            },
                            {
                                "content": "                if not bbox:",
                                "type": "added"
                            },
                            {
                                "content": "                    # This frame is identical to the previous frame",
                                "type": "added"
                            },
                            {
                                "content": "                    if duration:",
                                "type": "added"
                            },
                            {
                                "content": "                        previous[\"encoderinfo\"][\"duration\"] += encoderinfo[\"duration\"]",
                                "type": "added"
                            },
                            {
                                "content": "                    continue",
                                "type": "added"
                            },
                            {
                                "content": "                if encoderinfo.get(\"disposal\") == 2:",
                                "type": "context"
                            },
                            {
                                "content": "                    if background_im is None:",
                                "type": "context"
                            },
                            {
                                "content": "                        color = im.encoderinfo.get(",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "src/PIL/GifImagePlugin.py",
                            "type": "function",
                            "name": "_write_multiple_frames",
                            "content": "def _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                bbox = _getbbox(previous[\"im\"], im_frame)\n                if not bbox:\n                    # This frame is identical to the previous frame\n                    if duration:\n                        previous[\"encoderinfo\"][\"duration\"] += encoderinfo[\"duration\"]\n                    continue\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                    bbox = _getbbox(background_im, im_frame)\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\"])",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 578,
                            "end_lineno": 651,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 606,
                            "length": 21
                        },
                        "new_range": {
                            "start": 622,
                            "length": 7
                        },
                        "section": "def _write_multiple_frames(im, fp, palette):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "                        background = _get_background(im_frame, color)",
                                "type": "context"
                            },
                            {
                                "content": "                        background_im = Image.new(\"P\", im_frame.size, background)",
                                "type": "context"
                            },
                            {
                                "content": "                        background_im.putpalette(im_frames[0][\"im\"].palette)",
                                "type": "context"
                            },
                            {
                                "content": "                    base_im = background_im",
                                "type": "deleted"
                            },
                            {
                                "content": "                else:",
                                "type": "deleted"
                            },
                            {
                                "content": "                    base_im = previous[\"im\"]",
                                "type": "deleted"
                            },
                            {
                                "content": "                if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):",
                                "type": "deleted"
                            },
                            {
                                "content": "                    delta = ImageChops.subtract_modulo(im_frame, base_im)",
                                "type": "deleted"
                            },
                            {
                                "content": "                else:",
                                "type": "deleted"
                            },
                            {
                                "content": "                    delta = ImageChops.subtract_modulo(",
                                "type": "deleted"
                            },
                            {
                                "content": "                        im_frame.convert(\"RGB\"), base_im.convert(\"RGB\")",
                                "type": "deleted"
                            },
                            {
                                "content": "                    )",
                                "type": "deleted"
                            },
                            {
                                "content": "                bbox = delta.getbbox()",
                                "type": "deleted"
                            },
                            {
                                "content": "                if not bbox:",
                                "type": "deleted"
                            },
                            {
                                "content": "                    # This frame is identical to the previous frame",
                                "type": "deleted"
                            },
                            {
                                "content": "                    if duration:",
                                "type": "deleted"
                            },
                            {
                                "content": "                        previous[\"encoderinfo\"][\"duration\"] += encoderinfo[\"duration\"]",
                                "type": "deleted"
                            },
                            {
                                "content": "                    continue",
                                "type": "deleted"
                            },
                            {
                                "content": "                    bbox = _getbbox(background_im, im_frame)",
                                "type": "added"
                            },
                            {
                                "content": "            else:",
                                "type": "context"
                            },
                            {
                                "content": "                bbox = None",
                                "type": "context"
                            },
                            {
                                "content": "            im_frames.append({\"im\": im_frame, \"bbox\": bbox, \"encoderinfo\": encoderinfo})",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "src/PIL/GifImagePlugin.py",
                            "type": "function",
                            "name": "_write_multiple_frames",
                            "content": "def _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 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\"])",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 568,
                            "end_lineno": 649,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "4ab837ae23103b841d2e5fa7ac91a8ff92279627^",
    "new_commit_hash": "4ab837ae23103b841d2e5fa7ac91a8ff92279627",
    "commit_message": "Only compare to previous when checking for duplicate frames while saving",
    "commit_date": "2022-12-08T11:35:48+11:00",
    "metadata": {}
}
{
    "repo_name": "pillow",
    "new_commit_hash": "4ab837ae23103b841d2e5fa7ac91a8ff92279627",
    "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_dispose2_background_frame(tmp_path):\n    out = str(tmp_path / \"temp.gif\")\n\n    im_list = [Image.new(\"RGBA\", (1, 20))]\n\n    different_frame = Image.new(\"RGBA\", (1, 20))\n    different_frame.putpixel((0, 10), (255, 0, 0, 255))\n    im_list.append(different_frame)\n\n    # Frame that matches the background\n    im_list.append(Image.new(\"RGBA\", (1, 20)))\n\n    im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)\n\n    with Image.open(out) as im:\n        assert im.n_frames == 3\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_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"
    ],
    "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 15ms\nInstalled 12 packages in 199ms\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 896ms\nPrepared 1 package in 32.84s\nInstalled 1 package in 0.92ms\n + pillow==9.4.0.dev0 (from file:///home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_4ab837ae23103b841d2e5fa7ac91a8ff92279627)\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_4ab837ae23103b841d2e5fa7ac91a8ff92279627\nconfigfile: setup.cfg\nplugins: cov-6.0.0\ncollected 82 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 [ 63%]\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                                           [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_dispose2_background_frame\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_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[32m======================== \u001b[32m\u001b[1m80 passed\u001b[0m, \u001b[33m2 skipped\u001b[0m\u001b[32m in 1.08s\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_4ab837ae23103b841d2e5fa7ac91a8ff92279627\nconfigfile: setup.cfg\nplugins: cov-6.0.0\ncollected 82 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[31mF\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[31m [ 63%]\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[31m                                           [100%]\u001b[0m\n\n=================================== FAILURES ===================================\n\u001b[31m\u001b[1m________________________ test_dispose2_background_frame ________________________\u001b[0m\n\ntmp_path = PosixPath('/tmp/pytest-of-gcpuser/pytest-229/test_dispose2_background_frame0')\n\n    def test_dispose2_background_frame(tmp_path):\n        out = str(tmp_path / \"temp.gif\")\n    \n        im_list = [Image.new(\"RGBA\", (1, 20))]\n    \n        different_frame = Image.new(\"RGBA\", (1, 20))\n        different_frame.putpixel((0, 10), (255, 0, 0, 255))\n        im_list.append(different_frame)\n    \n        # Frame that matches the background\n        im_list.append(Image.new(\"RGBA\", (1, 20)))\n    \n        im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)\n    \n        with Image.open(out) as im:\n>           assert im.n_frames == 3\n\u001b[1m\u001b[31mE           assert 2 == 3\u001b[0m\n\u001b[1m\u001b[31mE            +  where 2 = <PIL.GifImagePlugin.GifImageFile image mode=P size=1x20 at 0x7F11604DE1F0>.n_frames\u001b[0m\n\n\u001b[1m\u001b[31mr2e_tests/test_1.py\u001b[0m:695: AssertionError\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_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_dispose2_background_frame\u001b[0m - assert 2 == 3\n\u001b[31m=================== \u001b[31m\u001b[1m1 failed\u001b[0m, \u001b[32m79 passed\u001b[0m, \u001b[33m2 skipped\u001b[0m\u001b[31m in 0.90s\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 ---
**Analysis:**

- **Key Parts:**
  - **Commit Message:** "Only compare to previous when checking for duplicate frames while saving."
  - **Test Failure:** The test `test_dispose2_background_frame` expected 3 frames but only 2 were saved in the old commit.
  
- **Main Problem:** When saving GIFs with duplicate frames, the saving process incorrectly identifies duplicates by comparing frames beyond just the immediate previous frame, leading to some duplicate frames being omitted.

- **Expected Behavior:** All frames, including duplicates identical to the previous frame, should be saved correctly, maintaining the intended number of frames.

- **Actual Behavior:** Duplicate frames identical to the previous frame are not saved, resulting in fewer frames than expected in the output GIF.

- **Issue Description:** The GIF saving functionality fails to correctly handle duplicate frames by only comparing them to the immediate previous frame. This causes some duplicate frames to be skipped unintentionally, leading to an incorrect frame count in the saved GIF.

---

**Drafted GitHub Issue:**

[ISSUE]
**Title:** Duplicate Frames Skipped When Saving GIFs, Resulting in Incorrect Frame Count

**Description:**
When saving GIF images that include duplicate frames identical to the previous frame, some of these duplicates are not saved. This results in the output GIF having fewer frames than intended, which can lead to unexpected behavior in applications relying on the correct frame sequence.

**Reproduction Steps:**
```python
from PIL import Image

out = "temp.gif"

# Create frames
base_frame = Image.new("RGBA", (1, 20))
different_frame = Image.new("RGBA", (1, 20))
different_frame.putpixel((0, 10), (255, 0, 0, 255))
duplicate_frame = Image.new("RGBA", (1, 20))

# Save GIF with duplicate frame
base_frame.save(out, save_all=True, append_images=[different_frame, duplicate_frame], disposal=2)

# Load and check number of frames
with Image.open(out) as im:
    print(im.n_frames)  # Expected: 3
```

**Expected Behavior:**
All three frames, including the duplicate of the base frame, should be saved in the output GIF. The `n_frames` property should return `3`.

**Actual Behavior:**
Only two frames are saved in the output GIF, with `n_frames` returning `2`. The duplicate frame is omitted, resulting in an incorrect frame count.
[/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 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.
**Analysis:**

- **Key Parts:**
  - **Commit Message:** "Only compare to previous when checking for duplicate frames while saving."
  - **Test Failure:** The test `test_dispose2_background_frame` expected 3 frames but only 2 were saved in the old commit.
  
- **Main Problem:** When saving GIFs with duplicate frames, the saving process incorrectly identifies duplicates by comparing frames beyond just the immediate previous frame, leading to some duplicate frames being omitted.

- **Expected Behavior:** All frames, including duplicates identical to the previous frame, should be saved correctly, maintaining the intended number of frames.

- **Actual Behavior:** Duplicate frames identical to the previous frame are not saved, resulting in fewer frames than expected in the output GIF.

- **Issue Description:** The GIF saving functionality fails to correctly handle duplicate frames by only comparing them to the immediate previous frame. This causes some duplicate frames to be skipped unintentionally, leading to an incorrect frame count in the saved GIF.

---

**Drafted GitHub Issue:**

[ISSUE]
**Title:** Duplicate Frames Skipped When Saving GIFs, Resulting in Incorrect Frame Count

**Description:**
When saving GIF images that include duplicate frames identical to the previous frame, some of these duplicates are not saved. This results in the output GIF having fewer frames than intended, which can lead to unexpected behavior in applications relying on the correct frame sequence.

**Reproduction Steps:**
```python
from PIL import Image

out = "temp.gif"

# Create frames
base_frame = Image.new("RGBA", (1, 20))
different_frame = Image.new("RGBA", (1, 20))
different_frame.putpixel((0, 10), (255, 0, 0, 255))
duplicate_frame = Image.new("RGBA", (1, 20))

# Save GIF with duplicate frame
base_frame.save(out, save_all=True, append_images=[different_frame, duplicate_frame], disposal=2)

# Load and check number of frames
with Image.open(out) as im:
    print(im.n_frames)  # Expected: 3
```

**Expected Behavior:**
All three frames, including the duplicate of the base frame, should be saved in the output GIF. The `n_frames` property should return `3`.

**Actual Behavior:**
Only two frames are saved in the output GIF, with `n_frames` returning `2`. The duplicate frame is omitted, resulting in an incorrect frame count.
[/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_dispose2_background_frame\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_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_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 dd1b21f2e..367958048 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -565,6 +565,16 @@ def _write_single_frame(im, fp, palette):
     fp.write(b"\0")  # end of image data
 
 
+def _getbbox(base_im, im_frame):
+    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")
+        )
+    return delta.getbbox()
+
+
 def _write_multiple_frames(im, fp, palette):
 
     duration = im.encoderinfo.get("duration")
@@ -598,6 +608,12 @@ def _write_multiple_frames(im, fp, palette):
             if im_frames:
                 # delta frame
                 previous = im_frames[-1]
+                bbox = _getbbox(previous["im"], im_frame)
+                if not bbox:
+                    # This frame is identical to the previous frame
+                    if duration:
+                        previous["encoderinfo"]["duration"] += encoderinfo["duration"]
+                    continue
                 if encoderinfo.get("disposal") == 2:
                     if background_im is None:
                         color = im.encoderinfo.get(
@@ -606,21 +622,7 @@ def _write_multiple_frames(im, fp, palette):
                         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 duration:
-                        previous["encoderinfo"]["duration"] += encoderinfo["duration"]
-                    continue
+                    bbox = _getbbox(background_im, im_frame)
             else:
                 bbox = None
             im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
 

{"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 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 ---
**Analysis:**

- **Key Parts:**
  - **Commit Message:** "Only compare to previous when checking for duplicate frames while saving."
  - **Test Failure:** The test `test_dispose2_background_frame` expected 3 frames but only 2 were saved in the old commit.
  
- **Main Problem:** When saving GIFs with duplicate frames, the saving process incorrectly identifies duplicates by comparing frames beyond just the immediate previous frame, leading to some duplicate frames being omitted.

- **Expected Behavior:** All frames, including duplicates identical to the previous frame, should be saved correctly, maintaining the intended number of frames.

- **Actual Behavior:** Duplicate frames identical to the previous frame are not saved, resulting in fewer frames than expected in the output GIF.

- **Issue Description:** The GIF saving functionality fails to correctly handle duplicate frames by only comparing them to the immediate previous frame. This causes some duplicate frames to be skipped unintentionally, leading to an incorrect frame count in the saved GIF.

---

**Drafted GitHub Issue:**

[ISSUE]
**Title:** Duplicate Frames Skipped When Saving GIFs, Resulting in Incorrect Frame Count

**Description:**
When saving GIF images that include duplicate frames identical to the previous frame, some of these duplicates are not saved. This results in the output GIF having fewer frames than intended, which can lead to unexpected behavior in applications relying on the correct frame sequence.

**Reproduction Steps:**
```python
from PIL import Image

out = "temp.gif"

# Create frames
base_frame = Image.new("RGBA", (1, 20))
different_frame = Image.new("RGBA", (1, 20))
different_frame.putpixel((0, 10), (255, 0, 0, 255))
duplicate_frame = Image.new("RGBA", (1, 20))

# Save GIF with duplicate frame
base_frame.save(out, save_all=True, append_images=[different_frame, duplicate_frame], disposal=2)

# Load and check number of frames
with Image.open(out) as im:
    print(im.n_frames)  # Expected: 3
```

**Expected Behavior:**
All three frames, including the duplicate of the base frame, should be saved in the output GIF. The `n_frames` property should return `3`.

**Actual Behavior:**
Only two frames are saved in the output GIF, with `n_frames` returning `2`. The duplicate frame is omitted, resulting in an incorrect frame count.
[/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 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