{
    "file_diffs": [
        {
            "old_file_content": "from __future__ import annotations\n\nimport warnings\nfrom collections.abc import Generator\nfrom io import BytesIO\nfrom pathlib import Path\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() -> None:\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() -> None:\n    def open() -> None:\n        im = Image.open(TEST_GIF)\n        im.load()\n\n    with pytest.warns(ResourceWarning):\n        open()\n\n\ndef test_closed_file() -> None:\n    with warnings.catch_warnings():\n        im = Image.open(TEST_GIF)\n        im.load()\n        im.close()\n\n\ndef test_seek_after_close() -> None:\n    im = Image.open(\"Tests/images/iss634.gif\")\n    assert isinstance(im, GifImagePlugin.GifImageFile)\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() -> None:\n    with warnings.catch_warnings():\n        with Image.open(TEST_GIF) as im:\n            im.load()\n\n\ndef test_invalid_file() -> None:\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() -> None:\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() -> None:\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() -> None:\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() -> None:\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() -> None:\n    def test_grayscale(optimize: int) -> int:\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: int) -> int:\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\n@pytest.mark.parametrize(\n    \"colors, size, expected_palette_length\",\n    (\n        # These do optimize the palette\n        (256, 511, 256),\n        (255, 511, 255),\n        (129, 511, 129),\n        (128, 511, 128),\n        (64, 511, 64),\n        (4, 511, 4),\n        # These don't optimize the palette\n        (128, 513, 256),\n        (64, 513, 256),\n        (4, 513, 256),\n    ),\n)\ndef test_optimize_correctness(\n    colors: int, size: int, expected_palette_length: int\n) -> None:\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\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\ndef test_optimize_full_l() -> None:\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() -> None:\n    im = Image.new(\"P\", (8, 1))\n    im.palette = ImagePalette.raw(\"RGB\", bytes((0, 0, 0) * 150))\n    for i in range(8):\n        im.putpixel((i, 0), (i + 1, 0, 0))\n\n    for optimize, colors in ((False, 256), (True, 8)):\n        out = BytesIO()\n        im.save(out, \"GIF\", optimize=optimize)\n        with Image.open(out) as reloaded:\n            assert len(reloaded.palette.palette) // 3 == colors\n\n\ndef test_full_palette_second_frame(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"P\", (1, 256))\n\n    full_palette_im = Image.new(\"P\", (1, 256))\n    for i in range(256):\n        full_palette_im.putpixel((0, i), i)\n    full_palette_im.palette = ImagePalette.ImagePalette(\n        \"RGB\", bytearray(i // 3 for i in range(768))\n    )\n    full_palette_im.palette.dirty = 1\n\n    im.save(out, save_all=True, append_images=[full_palette_im])\n\n    with Image.open(out) as reloaded:\n        reloaded.seek(1)\n\n        for i in range(256):\n            reloaded.getpixel((0, i)) == i\n\n\ndef test_roundtrip(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = hopper()\n    im.save(out)\n    with Image.open(out) as reread:\n        assert_image_similar(reread.convert(\"RGB\"), im, 50)\n\n\ndef test_roundtrip2(tmp_path: Path) -> None:\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        assert_image_similar(reread.convert(\"RGB\"), hopper(), 50)\n\n\ndef test_roundtrip_save_all(tmp_path: Path) -> None:\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        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\ndef test_roundtrip_save_all_1(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"1\", (1, 1))\n    im2 = Image.new(\"1\", (1, 1), 1)\n    im.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reloaded:\n        assert reloaded.getpixel((0, 0)) == 0\n\n        reloaded.seek(1)\n        assert reloaded.getpixel((0, 0)) == 255\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: str, mode: str) -> None:\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: Path) -> None:\n    important_headers = [\"background\", \"version\", \"duration\", \"loop\"]\n    # Multiframe image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\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        for header in important_headers:\n            assert info[header] == reread.info[header]\n\n\ndef test_palette_handling(tmp_path: Path) -> None:\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        assert_image_similar(im, reloaded.convert(\"RGB\"), 10)\n\n\ndef test_palette_434(tmp_path: Path) -> None:\n    # see https://github.com/python-pillow/Pillow/issues/434\n\n    def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:\n        out = str(tmp_path / \"temp.gif\")\n        im.copy().save(out, \"GIF\", **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        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: Path) -> None:\n    with Image.open(TEST_GIF) as img:\n        img = img.convert(\"RGB\")\n\n        tempfile = str(tmp_path / \"temp.gif\")\n        b = BytesIO()\n        GifImagePlugin._save_netpbm(img, b, 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: Path) -> None:\n    with Image.open(TEST_GIF) as img:\n        img = img.convert(\"L\")\n\n        tempfile = str(tmp_path / \"temp.gif\")\n        b = BytesIO()\n        GifImagePlugin._save_netpbm(img, b, tempfile)\n        with Image.open(tempfile) as reloaded:\n            assert_image_similar(img, reloaded.convert(\"L\"), 0)\n\n\ndef test_seek() -> None:\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() -> None:\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() -> None:\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: str, n_frames: int) -> None:\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() -> None:\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() -> None:\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() -> None:\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() -> 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() -> None:\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() -> None:\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() -> None:\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(\n    loading_strategy: GifImagePlugin.LoadingStrategy,\n    expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]],\n) -> None:\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() -> None:\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() -> None:\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() -> None:\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: Path) -> None:\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        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: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    # Four colors: white, gray, 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            # Check that frame transparency wasn't added unnecessarily\n            assert getattr(img, \"_frame_transparency\") is None\n\n\ndef test_dispose2_diff(tmp_path: Path) -> None:\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: Path) -> None:\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: Path) -> None:\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_dispose2_previous_frame(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"P\", (100, 100))\n    im.info[\"transparency\"] = 0\n    d = ImageDraw.Draw(im)\n    d.rectangle([(0, 0), (100, 50)], 1)\n    im.putpalette((0, 0, 0, 255, 0, 0))\n\n    im2 = Image.new(\"P\", (100, 100))\n    im2.putpalette((0, 0, 0))\n\n    im.save(out, save_all=True, append_images=[im2], disposal=[0, 2])\n\n    with Image.open(out) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 255)\n\n\ndef test_transparency_in_second_frame(tmp_path: Path) -> None:\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() -> None:\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: Path) -> None:\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: Path) -> None:\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: Path) -> None:\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        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        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: Path) -> None:\n    duration_list = [100, 500, 500]\n\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/transparent_dispose.gif\") as im:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(im)\n        ] == duration_list\n\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reloaded:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(reloaded)\n        ] == duration_list\n\n\ndef test_roundtrip_info_duration_combined(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/duplicate_frame.gif\") as im:\n        assert [frame.info[\"duration\"] for frame in ImageSequence.Iterator(im)] == [\n            1000,\n            1000,\n            1000,\n        ]\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reloaded:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(reloaded)\n        ] == [1000, 2000]\n\n\ndef test_identical_frames(tmp_path: Path) -> None:\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        # 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\",\n    (\n        [1000, 1500, 2000],\n        (1000, 1500, 2000),\n        # One more duration than the number of frames\n        [1000, 1500, 2000, 4000],\n        1500,\n    ),\n)\ndef test_identical_frames_to_single_frame(\n    duration: int | list[int], tmp_path: Path\n) -> None:\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\"] == 4500\n\n\ndef test_loop_none(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.save(out, loop=None)\n    with Image.open(out) as reread:\n        assert \"loop\" not in reread.info\n\n\ndef test_number_of_loops(tmp_path: Path) -> None:\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: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"background\"] = 1\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"background\"] == im.info[\"background\"]\n\n\ndef test_webp_background(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    # Test opaque WebP background\n    if features.check(\"webp\"):\n        with Image.open(\"Tests/images/hopper.webp\") as im:\n            assert im.info[\"background\"] == (255, 255, 255, 255)\n            im.save(out)\n\n    # Test non-opaque WebP background\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"background\"] = (0, 0, 0, 0)\n    im.save(out)\n\n\ndef test_comment(tmp_path: Path) -> None:\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: Path) -> None:\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() -> None:\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() -> None:\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: Path) -> None:\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: Path) -> None:\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: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    def assert_version_after_save(im: Image.Image, version: bytes) -> None:\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: Path) -> None:\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: list[Image.Image]) -> Generator[Image.Image, None, None]:\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_append_different_size_image(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"RGB\", (100, 100))\n    bigger_im = Image.new(\"RGB\", (200, 200), \"#f00\")\n\n    im.save(out, save_all=True, append_images=[bigger_im])\n\n    with Image.open(out) as reread:\n        assert reread.size == (100, 100)\n\n        reread.seek(1)\n        assert reread.size == (100, 100)\n\n\ndef test_transparent_optimize(tmp_path: Path) -> None:\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_removed_transparency(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"RGB\", (256, 1))\n\n    for x in range(256):\n        im.putpixel((x, 0), (x, 0, 0))\n\n    im.info[\"transparency\"] = (255, 255, 255)\n    with pytest.warns(UserWarning):\n        im.save(out)\n\n    with Image.open(out) as reloaded:\n        assert \"transparency\" not in reloaded.info\n\n\ndef test_rgb_transparency(tmp_path: Path) -> None:\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    with pytest.warns(UserWarning):\n        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: Path) -> None:\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_background_outside_palettte(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/background_outside_palette.gif\") as im:\n        im.seek(1)\n        assert im.info[\"background\"] == 255\n\n\ndef test_bbox(tmp_path: Path) -> None:\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_bbox_alpha(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"RGBA\", (1, 2), (255, 0, 0, 255))\n    im.putpixel((0, 1), (255, 0, 0, 0))\n    im2 = Image.new(\"RGBA\", (1, 2), (255, 0, 0, 0))\n    im.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 2\n\n\ndef test_palette_save_L(tmp_path: Path) -> None:\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 = im.getpalette()\n    assert palette is not None\n\n    out = str(tmp_path / \"temp.gif\")\n    im_l.save(out, palette=bytes(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: Path) -> None:\n    im = Image.new(\"P\", (1, 2))\n    im.putpixel((0, 1), 1)\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))\n\n    with Image.open(out) as reloaded:\n        reloaded_rgb = reloaded.convert(\"RGB\")\n\n        assert reloaded_rgb.getpixel((0, 0)) == (1, 2, 3)\n        assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)\n\n\ndef test_palette_save_duplicate_entries(tmp_path: Path) -> None:\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: Path) -> None:\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: Path) -> None:\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: Path) -> None:\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() -> None:\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() -> None:\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\n@pytest.mark.parametrize(\n    \"test_file, loading_strategy\",\n    (\n        (\"test_extents.gif\", GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST),\n        (\n            \"test_extents.gif\",\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,\n        ),\n        (\n            \"test_extents_transparency.gif\",\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,\n        ),\n    ),\n)\ndef test_extents(\n    test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy\n) -> None:\n    GifImagePlugin.LOADING_STRATEGY = loading_strategy\n    try:\n        with Image.open(\"Tests/images/\" + test_file) 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            im.load()\n            assert im.im.size == (150, 150)\n    finally:\n        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST\n\n\ndef test_missing_background() -> None:\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: Path) -> None:\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\n\ndef test_optimizing_p_rgba(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im1 = Image.new(\"P\", (100, 100))\n    d = ImageDraw.Draw(im1)\n    d.ellipse([(40, 40), (60, 60)], fill=1)\n    data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254\n    im1.putpalette(data, \"RGBA\")\n\n    im2 = Image.new(\"P\", (100, 100))\n    im2.putpalette(data, \"RGBA\")\n\n    im1.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reloaded:\n        assert reloaded.n_frames == 2\n",
            "new_file_content": "from __future__ import annotations\n\nimport warnings\nfrom collections.abc import Generator\nfrom io import BytesIO\nfrom pathlib import Path\nfrom typing import Any\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() -> None:\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() -> None:\n    def open() -> None:\n        im = Image.open(TEST_GIF)\n        im.load()\n\n    with pytest.warns(ResourceWarning):\n        open()\n\n\ndef test_closed_file() -> None:\n    with warnings.catch_warnings():\n        im = Image.open(TEST_GIF)\n        im.load()\n        im.close()\n\n\ndef test_seek_after_close() -> None:\n    im = Image.open(\"Tests/images/iss634.gif\")\n    assert isinstance(im, GifImagePlugin.GifImageFile)\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() -> None:\n    with warnings.catch_warnings():\n        with Image.open(TEST_GIF) as im:\n            im.load()\n\n\ndef test_invalid_file() -> None:\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() -> None:\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() -> None:\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() -> None:\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() -> None:\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() -> None:\n    def test_grayscale(optimize: int) -> int:\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: int) -> int:\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\n@pytest.mark.parametrize(\n    \"colors, size, expected_palette_length\",\n    (\n        # These do optimize the palette\n        (256, 511, 256),\n        (255, 511, 255),\n        (129, 511, 129),\n        (128, 511, 128),\n        (64, 511, 64),\n        (4, 511, 4),\n        # These don't optimize the palette\n        (128, 513, 256),\n        (64, 513, 256),\n        (4, 513, 256),\n    ),\n)\ndef test_optimize_correctness(\n    colors: int, size: int, expected_palette_length: int\n) -> None:\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\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\ndef test_optimize_full_l() -> None:\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() -> None:\n    im = Image.new(\"P\", (8, 1))\n    im.palette = ImagePalette.raw(\"RGB\", bytes((0, 0, 0) * 150))\n    for i in range(8):\n        im.putpixel((i, 0), (i + 1, 0, 0))\n\n    for optimize, colors in ((False, 256), (True, 8)):\n        out = BytesIO()\n        im.save(out, \"GIF\", optimize=optimize)\n        with Image.open(out) as reloaded:\n            assert len(reloaded.palette.palette) // 3 == colors\n\n\ndef test_full_palette_second_frame(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"P\", (1, 256))\n\n    full_palette_im = Image.new(\"P\", (1, 256))\n    for i in range(256):\n        full_palette_im.putpixel((0, i), i)\n    full_palette_im.palette = ImagePalette.ImagePalette(\n        \"RGB\", bytearray(i // 3 for i in range(768))\n    )\n    full_palette_im.palette.dirty = 1\n\n    im.save(out, save_all=True, append_images=[full_palette_im])\n\n    with Image.open(out) as reloaded:\n        reloaded.seek(1)\n\n        for i in range(256):\n            reloaded.getpixel((0, i)) == i\n\n\ndef test_roundtrip(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = hopper()\n    im.save(out)\n    with Image.open(out) as reread:\n        assert_image_similar(reread.convert(\"RGB\"), im, 50)\n\n\ndef test_roundtrip2(tmp_path: Path) -> None:\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        assert_image_similar(reread.convert(\"RGB\"), hopper(), 50)\n\n\ndef test_roundtrip_save_all(tmp_path: Path) -> None:\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        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\ndef test_roundtrip_save_all_1(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"1\", (1, 1))\n    im2 = Image.new(\"1\", (1, 1), 1)\n    im.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reloaded:\n        assert reloaded.getpixel((0, 0)) == 0\n\n        reloaded.seek(1)\n        assert reloaded.getpixel((0, 0)) == 255\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: str, mode: str) -> None:\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: Path) -> None:\n    important_headers = [\"background\", \"version\", \"duration\", \"loop\"]\n    # Multiframe image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\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        for header in important_headers:\n            assert info[header] == reread.info[header]\n\n\ndef test_palette_handling(tmp_path: Path) -> None:\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        assert_image_similar(im, reloaded.convert(\"RGB\"), 10)\n\n\ndef test_palette_434(tmp_path: Path) -> None:\n    # see https://github.com/python-pillow/Pillow/issues/434\n\n    def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:\n        out = str(tmp_path / \"temp.gif\")\n        im.copy().save(out, \"GIF\", **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        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: Path) -> None:\n    with Image.open(TEST_GIF) as img:\n        img = img.convert(\"RGB\")\n\n        tempfile = str(tmp_path / \"temp.gif\")\n        b = BytesIO()\n        GifImagePlugin._save_netpbm(img, b, 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: Path) -> None:\n    with Image.open(TEST_GIF) as img:\n        img = img.convert(\"L\")\n\n        tempfile = str(tmp_path / \"temp.gif\")\n        b = BytesIO()\n        GifImagePlugin._save_netpbm(img, b, tempfile)\n        with Image.open(tempfile) as reloaded:\n            assert_image_similar(img, reloaded.convert(\"L\"), 0)\n\n\ndef test_seek() -> None:\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() -> None:\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() -> None:\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: str, n_frames: int) -> None:\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() -> None:\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() -> None:\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() -> None:\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() -> 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() -> None:\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() -> None:\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() -> None:\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(\n    loading_strategy: GifImagePlugin.LoadingStrategy,\n    expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]],\n) -> None:\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() -> None:\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() -> None:\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() -> None:\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: Path) -> None:\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        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: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    # Four colors: white, gray, 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            # Check that frame transparency wasn't added unnecessarily\n            assert getattr(img, \"_frame_transparency\") is None\n\n\ndef test_dispose2_diff(tmp_path: Path) -> None:\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: Path) -> None:\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: Path) -> None:\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_dispose2_previous_frame(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"P\", (100, 100))\n    im.info[\"transparency\"] = 0\n    d = ImageDraw.Draw(im)\n    d.rectangle([(0, 0), (100, 50)], 1)\n    im.putpalette((0, 0, 0, 255, 0, 0))\n\n    im2 = Image.new(\"P\", (100, 100))\n    im2.putpalette((0, 0, 0))\n\n    im.save(out, save_all=True, append_images=[im2], disposal=[0, 2])\n\n    with Image.open(out) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 255)\n\n\ndef test_transparency_in_second_frame(tmp_path: Path) -> None:\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() -> None:\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: Path) -> None:\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: Path) -> None:\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: Path) -> None:\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        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        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: Path) -> None:\n    duration_list = [100, 500, 500]\n\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/transparent_dispose.gif\") as im:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(im)\n        ] == duration_list\n\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reloaded:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(reloaded)\n        ] == duration_list\n\n\ndef test_roundtrip_info_duration_combined(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/duplicate_frame.gif\") as im:\n        assert [frame.info[\"duration\"] for frame in ImageSequence.Iterator(im)] == [\n            1000,\n            1000,\n            1000,\n        ]\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reloaded:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(reloaded)\n        ] == [1000, 2000]\n\n\ndef test_identical_frames(tmp_path: Path) -> None:\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        # 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\",\n    (\n        [1000, 1500, 2000],\n        (1000, 1500, 2000),\n        # One more duration than the number of frames\n        [1000, 1500, 2000, 4000],\n        1500,\n    ),\n)\ndef test_identical_frames_to_single_frame(\n    duration: int | list[int], tmp_path: Path\n) -> None:\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\"] == 4500\n\n\ndef test_loop_none(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.save(out, loop=None)\n    with Image.open(out) as reread:\n        assert \"loop\" not in reread.info\n\n\ndef test_number_of_loops(tmp_path: Path) -> None:\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: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"background\"] = 1\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"background\"] == im.info[\"background\"]\n\n\ndef test_webp_background(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    # Test opaque WebP background\n    if features.check(\"webp\"):\n        with Image.open(\"Tests/images/hopper.webp\") as im:\n            assert im.info[\"background\"] == (255, 255, 255, 255)\n            im.save(out)\n\n    # Test non-opaque WebP background\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"background\"] = (0, 0, 0, 0)\n    im.save(out)\n\n\ndef test_comment(tmp_path: Path) -> None:\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: Path) -> None:\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() -> None:\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() -> None:\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: Path) -> None:\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: Path) -> None:\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: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    def assert_version_after_save(im: Image.Image, version: bytes) -> None:\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: Path) -> None:\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: list[Image.Image]) -> Generator[Image.Image, None, None]:\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_append_different_size_image(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"RGB\", (100, 100))\n    bigger_im = Image.new(\"RGB\", (200, 200), \"#f00\")\n\n    im.save(out, save_all=True, append_images=[bigger_im])\n\n    with Image.open(out) as reread:\n        assert reread.size == (100, 100)\n\n        reread.seek(1)\n        assert reread.size == (100, 100)\n\n\ndef test_transparent_optimize(tmp_path: Path) -> None:\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_removed_transparency(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"RGB\", (256, 1))\n\n    for x in range(256):\n        im.putpixel((x, 0), (x, 0, 0))\n\n    im.info[\"transparency\"] = (255, 255, 255)\n    with pytest.warns(UserWarning):\n        im.save(out)\n\n    with Image.open(out) as reloaded:\n        assert \"transparency\" not in reloaded.info\n\n\ndef test_rgb_transparency(tmp_path: Path) -> None:\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    with pytest.warns(UserWarning):\n        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: Path) -> None:\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_background_outside_palettte(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/background_outside_palette.gif\") as im:\n        im.seek(1)\n        assert im.info[\"background\"] == 255\n\n\ndef test_bbox(tmp_path: Path) -> None:\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_bbox_alpha(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"RGBA\", (1, 2), (255, 0, 0, 255))\n    im.putpixel((0, 1), (255, 0, 0, 0))\n    im2 = Image.new(\"RGBA\", (1, 2), (255, 0, 0, 0))\n    im.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 2\n\n\ndef test_palette_save_L(tmp_path: Path) -> None:\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 = im.getpalette()\n    assert palette is not None\n\n    out = str(tmp_path / \"temp.gif\")\n    im_l.save(out, palette=bytes(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: Path) -> None:\n    im = Image.new(\"P\", (1, 2))\n    im.putpixel((0, 1), 1)\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))\n\n    with Image.open(out) as reloaded:\n        reloaded_rgb = reloaded.convert(\"RGB\")\n\n        assert reloaded_rgb.getpixel((0, 0)) == (1, 2, 3)\n        assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)\n\n\ndef test_palette_save_duplicate_entries(tmp_path: Path) -> None:\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: Path) -> None:\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: Path) -> None:\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: Path) -> None:\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() -> None:\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() -> None:\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\n@pytest.mark.parametrize(\n    \"test_file, loading_strategy\",\n    (\n        (\"test_extents.gif\", GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST),\n        (\n            \"test_extents.gif\",\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,\n        ),\n        (\n            \"test_extents_transparency.gif\",\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,\n        ),\n    ),\n)\ndef test_extents(\n    test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy\n) -> None:\n    GifImagePlugin.LOADING_STRATEGY = loading_strategy\n    try:\n        with Image.open(\"Tests/images/\" + test_file) 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            im.load()\n            assert im.im.size == (150, 150)\n    finally:\n        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST\n\n\ndef test_missing_background() -> None:\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: Path) -> None:\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\n\n@pytest.mark.parametrize(\"params\", ({}, {\"disposal\": 2, \"optimize\": False}))\ndef test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im1 = Image.new(\"P\", (100, 100))\n    d = ImageDraw.Draw(im1)\n    d.ellipse([(40, 40), (60, 60)], fill=1)\n    data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254\n    im1.putpalette(data, \"RGBA\")\n\n    im2 = Image.new(\"P\", (100, 100))\n    im2.putpalette(data, \"RGBA\")\n\n    im1.save(out, save_all=True, append_images=[im2], **params)\n\n    with Image.open(out) as reloaded:\n        assert reloaded.n_frames == 2\n",
            "header": {
                "file": {
                    "path": "Tests/test_file_gif.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "16c8466f3",
                "new_commit_hash": "4eb8c0d02",
                "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": 4,
                            "length": 6
                        },
                        "new_range": {
                            "start": 4,
                            "length": 7
                        },
                        "section": "import warnings"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "from collections.abc import Generator",
                                "type": "context"
                            },
                            {
                                "content": "from io import BytesIO",
                                "type": "context"
                            },
                            {
                                "content": "from pathlib import Path",
                                "type": "context"
                            },
                            {
                                "content": "from typing import Any",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "import pytest",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [],
                    "added_entities": [
                        {
                            "file_name": "Tests/test_file_gif.py",
                            "type": "statement",
                            "name": "",
                            "content": "from typing import Any",
                            "ast_type_str": "ImportFrom",
                            "start_lineno": 7,
                            "end_lineno": 7,
                            "parent": null
                        }
                    ],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 1431,
                            "length": 7
                        },
                        "new_range": {
                            "start": 1432,
                            "length": 8
                        },
                        "section": "def test_saving_rgba(tmp_path: Path) -> None:"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        assert reloaded_rgba.load()[0, 0][3] == 0",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "def test_optimizing_p_rgba(tmp_path: Path) -> None:",
                                "type": "deleted"
                            },
                            {
                                "content": "@pytest.mark.parametrize(\"params\", ({}, {\"disposal\": 2, \"optimize\": False}))",
                                "type": "added"
                            },
                            {
                                "content": "def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:",
                                "type": "added"
                            },
                            {
                                "content": "    out = str(tmp_path / \"temp.gif\")",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    im1 = Image.new(\"P\", (100, 100))",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [],
                    "added_entities": [
                        {
                            "file_name": "Tests/test_file_gif.py",
                            "type": "function",
                            "name": "test_p_rgba",
                            "content": "def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im1 = Image.new(\"P\", (100, 100))\n    d = ImageDraw.Draw(im1)\n    d.ellipse([(40, 40), (60, 60)], fill=1)\n    data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254\n    im1.putpalette(data, \"RGBA\")\n\n    im2 = Image.new(\"P\", (100, 100))\n    im2.putpalette(data, \"RGBA\")\n\n    im1.save(out, save_all=True, append_images=[im2], **params)\n\n    with Image.open(out) as reloaded:\n        assert reloaded.n_frames == 2",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 1436,
                            "end_lineno": 1451,
                            "parent": null
                        }
                    ],
                    "deleted_entities": [
                        {
                            "file_name": "Tests/test_file_gif.py",
                            "type": "function",
                            "name": "test_optimizing_p_rgba",
                            "content": "def test_optimizing_p_rgba(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im1 = Image.new(\"P\", (100, 100))\n    d = ImageDraw.Draw(im1)\n    d.ellipse([(40, 40), (60, 60)], fill=1)\n    data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254\n    im1.putpalette(data, \"RGBA\")\n\n    im2 = Image.new(\"P\", (100, 100))\n    im2.putpalette(data, \"RGBA\")\n\n    im1.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reloaded:\n        assert reloaded.n_frames == 2",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 1434,
                            "end_lineno": 1449,
                            "parent": null
                        }
                    ]
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 1443,
                            "length": 7
                        },
                        "new_range": {
                            "start": 1445,
                            "length": 7
                        },
                        "section": "def test_optimizing_p_rgba(tmp_path: Path) -> None:"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "    im2 = Image.new(\"P\", (100, 100))",
                                "type": "context"
                            },
                            {
                                "content": "    im2.putpalette(data, \"RGBA\")",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    im1.save(out, save_all=True, append_images=[im2])",
                                "type": "deleted"
                            },
                            {
                                "content": "    im1.save(out, save_all=True, append_images=[im2], **params)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    with Image.open(out) as reloaded:",
                                "type": "context"
                            },
                            {
                                "content": "        assert reloaded.n_frames == 2",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [],
                    "added_entities": [
                        {
                            "file_name": "Tests/test_file_gif.py",
                            "type": "function",
                            "name": "test_p_rgba",
                            "content": "def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im1 = Image.new(\"P\", (100, 100))\n    d = ImageDraw.Draw(im1)\n    d.ellipse([(40, 40), (60, 60)], fill=1)\n    data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254\n    im1.putpalette(data, \"RGBA\")\n\n    im2 = Image.new(\"P\", (100, 100))\n    im2.putpalette(data, \"RGBA\")\n\n    im1.save(out, save_all=True, append_images=[im2], **params)\n\n    with Image.open(out) as reloaded:\n        assert reloaded.n_frames == 2",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 1436,
                            "end_lineno": 1451,
                            "parent": null
                        }
                    ],
                    "deleted_entities": [
                        {
                            "file_name": "Tests/test_file_gif.py",
                            "type": "function",
                            "name": "test_optimizing_p_rgba",
                            "content": "def test_optimizing_p_rgba(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im1 = Image.new(\"P\", (100, 100))\n    d = ImageDraw.Draw(im1)\n    d.ellipse([(40, 40), (60, 60)], fill=1)\n    data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254\n    im1.putpalette(data, \"RGBA\")\n\n    im2 = Image.new(\"P\", (100, 100))\n    im2.putpalette(data, \"RGBA\")\n\n    im1.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reloaded:\n        assert reloaded.n_frames == 2",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 1434,
                            "end_lineno": 1449,
                            "parent": null
                        }
                    ]
                }
            ]
        },
        {
            "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#\nfrom __future__ import annotations\n\nimport itertools\nimport math\nimport os\nimport subprocess\nfrom enum import IntEnum\nfrom functools import cached_property\nfrom typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union\n\nfrom . import (\n    Image,\n    ImageChops,\n    ImageFile,\n    ImageMath,\n    ImageOps,\n    ImagePalette,\n    ImageSequence,\n)\nfrom ._binary import i16le as i16\nfrom ._binary import o8\nfrom ._binary import o16le as o16\n\nif TYPE_CHECKING:\n    from . import _imaging\n    from ._typing import Buffer\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: bytes) -> bool:\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    format = \"GIF\"\n    format_description = \"Compuserve GIF\"\n    _close_exclusive_fp_after_loading = False\n\n    global_palette = None\n\n    def data(self) -> bytes | None:\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: bytes) -> bool:\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) -> None:\n        # Screen\n        s = self.fp.read(13)\n        if not _accept(s):\n            msg = \"not a GIF file\"\n            raise SyntaxError(msg)\n\n        self.info[\"version\"] = s[:6]\n        self._size = i16(s, 6), i16(s, 8)\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: int | None = None\n        self._seek(0)  # get ready to read first frame\n\n    @property\n    def n_frames(self) -> int:\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    @cached_property\n    def is_animated(self) -> bool:\n        if self._n_frames is not None:\n            return self._n_frames != 1\n\n        current = self.tell()\n        if current:\n            return True\n\n        try:\n            self._seek(1, False)\n            is_animated = True\n        except EOFError:\n            is_animated = False\n\n        self.seek(current)\n        return is_animated\n\n    def seek(self, frame: int) -> None:\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                msg = \"no more images in GIF file\"\n                raise EOFError(msg) from e\n\n    def _seek(self, frame: int, update_image: bool = True) -> None:\n        if frame == 0:\n            # rewind\n            self.__offset = 0\n            self.dispose: _imaging.ImagingCore | None = 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            msg = f\"cannot seek to frame {frame}\"\n            raise ValueError(msg)\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            msg = \"no more images in GIF file\"\n            raise EOFError(msg)\n\n        palette: ImagePalette.ImagePalette | Literal[False] | None = None\n\n        info: dict[str, Any] = {}\n        frame_transparency = None\n        interlace = None\n        frame_dispose_extent = None\n        while True:\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 and block is not None:\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 and block is not None:\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 block and 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            s = None\n\n        if interlace is None:\n            msg = \"image not found in GIF frame\"\n            raise EOFError(msg)\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 palette:\n                self.palette = palette\n            elif self.global_palette:\n                from copy import copy\n\n                self.palette = copy(self.global_palette)\n            else:\n                self.palette = None\n        else:\n            if self.mode == \"P\":\n                if (\n                    LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY\n                    or palette\n                ):\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: int) -> tuple[int, int, int]:\n            if self._frame_palette:\n                if color * 3 + 3 > len(self._frame_palette.palette):\n                    color = 0\n                return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])\n            else:\n                return (color, color, color)\n\n        self.dispose = None\n        self.dispose_extent = frame_dispose_extent\n        if self.dispose_extent and self.disposal_method >= 2:\n            try:\n                if 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(\n                            dispose_mode, dispose_size, color\n                        )\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                ImageFile._Tile(\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) -> None:\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(\"RGB\", *self._frame_palette.getdata())\n            else:\n                self._im = None\n        if not self._prev_im and self._im is not None and self.size != self.im.size:\n            expanded_im = Image.core.fill(self.im.mode, self.size)\n            if self._frame_palette:\n                expanded_im.putpalette(\"RGB\", *self._frame_palette.getdata())\n            expanded_im.paste(self.im, (0, 0) + self.im.size)\n\n            self.im = expanded_im\n        self._mode = temp_mode\n        self._frame_palette = None\n\n        super().load_prepare()\n\n    def load_end(self) -> None:\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.size != self._prev_im.size:\n            if self._frame_transparency is not None:\n                expanded_im = Image.core.fill(\"RGBA\", self.size)\n            else:\n                expanded_im = Image.core.fill(\"P\", self.size)\n                expanded_im.putpalette(\"RGB\", \"RGB\", self.im.getpalette())\n                expanded_im = expanded_im.convert(\"RGB\")\n            expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size)\n\n            self._prev_im = expanded_im\n            assert self._prev_im is not None\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\n        assert self.dispose_extent is not None\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) -> int:\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: Image.Image) -> Image.Image:\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        assert im.palette is not None\n        if im.palette.mode == \"RGBA\":\n            for rgba in im.palette.colors:\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\n_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette]\n\n\ndef _normalize_palette(\n    im: Image.Image, palette: _Palette | None, info: dict[str, Any]\n) -> Image.Image:\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            im_palette = im.getpalette(None)\n            assert im_palette is not None\n            source_palette = bytearray(im_palette)\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    assert source_palette is not None\n\n    if palette:\n        used_palette_colors: list[int | None] = []\n        assert im.palette is not None\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        dest_map: list[int] = []\n        for index in used_palette_colors:\n            assert index is not None\n            dest_map.append(index)\n        im = im.remap_palette(dest_map)\n    else:\n        optimized_palette_colors = _get_optimize(im, info)\n        if optimized_palette_colors is not None:\n            im = im.remap_palette(optimized_palette_colors, source_palette)\n            if \"transparency\" in info:\n                try:\n                    info[\"transparency\"] = optimized_palette_colors.index(\n                        info[\"transparency\"]\n                    )\n                except ValueError:\n                    del info[\"transparency\"]\n            return im\n\n    assert im.palette is not None\n    im.palette.palette = source_palette\n    return im\n\n\ndef _write_single_frame(\n    im: Image.Image,\n    fp: IO[bytes],\n    palette: _Palette | None,\n) -> None:\n    im_out = _normalize_mode(im)\n    for k, v in im_out.info.items():\n        if isinstance(k, str):\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(\n        im_out, fp, [ImageFile._Tile(\"gif\", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]\n    )\n\n    fp.write(b\"\\0\")  # end of image data\n\n\ndef _getbbox(\n    base_im: Image.Image, im_frame: Image.Image\n) -> tuple[Image.Image, tuple[int, int, int, int] | None]:\n    palette_bytes = [\n        bytes(im.palette.palette) if im.palette else b\"\" for im in (base_im, im_frame)\n    ]\n    if palette_bytes[0] != palette_bytes[1]:\n        im_frame = im_frame.convert(\"RGBA\")\n        base_im = base_im.convert(\"RGBA\")\n    delta = ImageChops.subtract_modulo(im_frame, base_im)\n    return delta, delta.getbbox(alpha_only=False)\n\n\nclass _Frame(NamedTuple):\n    im: Image.Image\n    bbox: tuple[int, int, int, int] | None\n    encoderinfo: dict[str, Any]\n\n\ndef _write_multiple_frames(\n    im: Image.Image, fp: IO[bytes], palette: _Palette | None\n) -> bool:\n    duration = im.encoderinfo.get(\"duration\")\n    disposal = im.encoderinfo.get(\"disposal\", im.info.get(\"disposal\"))\n\n    im_frames: list[_Frame] = []\n    previous_im: Image.Image | None = None\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                    if isinstance(k, str):\n                        im.encoderinfo.setdefault(k, v)\n\n            encoderinfo = im.encoderinfo.copy()\n            if \"transparency\" in im_frame.info:\n                encoderinfo.setdefault(\"transparency\", im_frame.info[\"transparency\"])\n            im_frame = _normalize_palette(im_frame, palette, encoderinfo)\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            diff_frame = None\n            if im_frames and previous_im:\n                # delta frame\n                delta, bbox = _getbbox(previous_im, im_frame)\n                if not bbox:\n                    # This frame is identical to the previous frame\n                    if encoderinfo.get(\"duration\"):\n                        im_frames[-1].encoderinfo[\"duration\"] += encoderinfo[\"duration\"]\n                    continue\n                if im_frames[-1].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                        assert im_frames[0].im.palette is not None\n                        background_im.putpalette(im_frames[0].im.palette)\n                    bbox = _getbbox(background_im, im_frame)[1]\n                elif encoderinfo.get(\"optimize\") and im_frame.mode != \"1\":\n                    if \"transparency\" not in encoderinfo:\n                        assert im_frame.palette is not None\n                        try:\n                            encoderinfo[\"transparency\"] = (\n                                im_frame.palette._new_color_index(im_frame)\n                            )\n                        except ValueError:\n                            pass\n                    if \"transparency\" in encoderinfo:\n                        # When the delta is zero, fill the image with transparency\n                        diff_frame = im_frame.copy()\n                        fill = Image.new(\"P\", delta.size, encoderinfo[\"transparency\"])\n                        if delta.mode == \"RGBA\":\n                            r, g, b, a = delta.split()\n                            mask = ImageMath.lambda_eval(\n                                lambda args: args[\"convert\"](\n                                    args[\"max\"](\n                                        args[\"max\"](\n                                            args[\"max\"](args[\"r\"], args[\"g\"]), args[\"b\"]\n                                        ),\n                                        args[\"a\"],\n                                    )\n                                    * 255,\n                                    \"1\",\n                                ),\n                                r=r,\n                                g=g,\n                                b=b,\n                                a=a,\n                            )\n                        else:\n                            if delta.mode == \"P\":\n                                # Convert to L without considering palette\n                                delta_l = Image.new(\"L\", delta.size)\n                                delta_l.putdata(delta.getdata())\n                                delta = delta_l\n                            mask = ImageMath.lambda_eval(\n                                lambda args: args[\"convert\"](args[\"im\"] * 255, \"1\"),\n                                im=delta,\n                            )\n                        diff_frame.paste(fill, mask=ImageOps.invert(mask))\n            else:\n                bbox = None\n            previous_im = im_frame\n            im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))\n\n    if len(im_frames) == 1:\n        if \"duration\" in im.encoderinfo:\n            # Since multiple frames will not be written, use the combined duration\n            im.encoderinfo[\"duration\"] = im_frames[0].encoderinfo[\"duration\"]\n        return False\n\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\n\ndef _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:\n    _save(im, fp, filename, save_all=True)\n\n\ndef _save(\n    im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False\n) -> None:\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.setdefault(\"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: Image.Image) -> int:\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(\n    fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int\n) -> None:\n    try:\n        transparency = im.encoderinfo[\"transparency\"]\n    except KeyError:\n        transparency = None\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 transparency is not None or duration != 0 or disposal:\n        packed_flag = 1 if transparency is not None else 0\n        packed_flag |= disposal << 2\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 or 0)  # 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: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:\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                assert quant_proc.stdout is not None\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: Image.Image, info: dict[str, Any]) -> list[int] | None:\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\"):\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            assert im.palette is not None\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    return None\n\n\ndef _get_color_table_size(palette_bytes: bytes) -> int:\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: bytes) -> 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: Image.Image) -> bytes:\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    if not im.palette:\n        return b\"\"\n\n    palette = bytes(im.palette.palette)\n    if im.palette.mode == \"RGBA\":\n        palette = b\"\".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3))\n    return palette\n\n\ndef _get_background(\n    im: Image.Image,\n    info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None,\n) -> int:\n    background = 0\n    if info_background:\n        if isinstance(info_background, tuple):\n            # WebPImagePlugin stores an RGBA value in info[\"background\"]\n            # So it must be converted to the same format as GifImagePlugin's\n            # info[\"background\"] - a global color table index\n            assert im.palette is not None\n            try:\n                background = im.palette.getcolor(info_background, im)\n            except ValueError as e:\n                if str(e) not in (\n                    # If all 256 colors are in use,\n                    # then there is no need for the background color\n                    \"cannot allocate more than 256 colors\",\n                    # Ignore non-opaque WebP background\n                    \"cannot add non-opaque RGBA color to RGB palette\",\n                ):\n                    raise\n        else:\n            background = info_background\n    return background\n\n\ndef _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]:\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 info.get(\"loop\") is not None\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 info.get(\"loop\") is not None:\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(\n    fp: IO[bytes],\n    im_frame: Image.Image,\n    offset: tuple[int, int],\n    params: dict[str, Any],\n) -> None:\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,\n            fp,\n            [ImageFile._Tile(\"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(\n    im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None\n) -> tuple[list[bytes], list[int] | 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    if info is None:\n        info = {}\n\n    used_palette_colors = _get_optimize(im, 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(\n    im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any\n) -> list[bytes]:\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    from io import BytesIO\n\n    class Collector(BytesIO):\n        data = []\n\n        def write(self, data: Buffer) -> int:\n            self.data.append(data)\n            return len(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#\nfrom __future__ import annotations\n\nimport itertools\nimport math\nimport os\nimport subprocess\nfrom enum import IntEnum\nfrom functools import cached_property\nfrom typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union\n\nfrom . import (\n    Image,\n    ImageChops,\n    ImageFile,\n    ImageMath,\n    ImageOps,\n    ImagePalette,\n    ImageSequence,\n)\nfrom ._binary import i16le as i16\nfrom ._binary import o8\nfrom ._binary import o16le as o16\n\nif TYPE_CHECKING:\n    from . import _imaging\n    from ._typing import Buffer\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: bytes) -> bool:\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    format = \"GIF\"\n    format_description = \"Compuserve GIF\"\n    _close_exclusive_fp_after_loading = False\n\n    global_palette = None\n\n    def data(self) -> bytes | None:\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: bytes) -> bool:\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) -> None:\n        # Screen\n        s = self.fp.read(13)\n        if not _accept(s):\n            msg = \"not a GIF file\"\n            raise SyntaxError(msg)\n\n        self.info[\"version\"] = s[:6]\n        self._size = i16(s, 6), i16(s, 8)\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: int | None = None\n        self._seek(0)  # get ready to read first frame\n\n    @property\n    def n_frames(self) -> int:\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    @cached_property\n    def is_animated(self) -> bool:\n        if self._n_frames is not None:\n            return self._n_frames != 1\n\n        current = self.tell()\n        if current:\n            return True\n\n        try:\n            self._seek(1, False)\n            is_animated = True\n        except EOFError:\n            is_animated = False\n\n        self.seek(current)\n        return is_animated\n\n    def seek(self, frame: int) -> None:\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                msg = \"no more images in GIF file\"\n                raise EOFError(msg) from e\n\n    def _seek(self, frame: int, update_image: bool = True) -> None:\n        if frame == 0:\n            # rewind\n            self.__offset = 0\n            self.dispose: _imaging.ImagingCore | None = 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            msg = f\"cannot seek to frame {frame}\"\n            raise ValueError(msg)\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            msg = \"no more images in GIF file\"\n            raise EOFError(msg)\n\n        palette: ImagePalette.ImagePalette | Literal[False] | None = None\n\n        info: dict[str, Any] = {}\n        frame_transparency = None\n        interlace = None\n        frame_dispose_extent = None\n        while True:\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 and block is not None:\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 and block is not None:\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 block and 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            s = None\n\n        if interlace is None:\n            msg = \"image not found in GIF frame\"\n            raise EOFError(msg)\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 palette:\n                self.palette = palette\n            elif self.global_palette:\n                from copy import copy\n\n                self.palette = copy(self.global_palette)\n            else:\n                self.palette = None\n        else:\n            if self.mode == \"P\":\n                if (\n                    LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY\n                    or palette\n                ):\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: int) -> tuple[int, int, int]:\n            if self._frame_palette:\n                if color * 3 + 3 > len(self._frame_palette.palette):\n                    color = 0\n                return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])\n            else:\n                return (color, color, color)\n\n        self.dispose = None\n        self.dispose_extent = frame_dispose_extent\n        if self.dispose_extent and self.disposal_method >= 2:\n            try:\n                if 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(\n                            dispose_mode, dispose_size, color\n                        )\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                ImageFile._Tile(\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) -> None:\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(\"RGB\", *self._frame_palette.getdata())\n            else:\n                self._im = None\n        if not self._prev_im and self._im is not None and self.size != self.im.size:\n            expanded_im = Image.core.fill(self.im.mode, self.size)\n            if self._frame_palette:\n                expanded_im.putpalette(\"RGB\", *self._frame_palette.getdata())\n            expanded_im.paste(self.im, (0, 0) + self.im.size)\n\n            self.im = expanded_im\n        self._mode = temp_mode\n        self._frame_palette = None\n\n        super().load_prepare()\n\n    def load_end(self) -> None:\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.size != self._prev_im.size:\n            if self._frame_transparency is not None:\n                expanded_im = Image.core.fill(\"RGBA\", self.size)\n            else:\n                expanded_im = Image.core.fill(\"P\", self.size)\n                expanded_im.putpalette(\"RGB\", \"RGB\", self.im.getpalette())\n                expanded_im = expanded_im.convert(\"RGB\")\n            expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size)\n\n            self._prev_im = expanded_im\n            assert self._prev_im is not None\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\n        assert self.dispose_extent is not None\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) -> int:\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: Image.Image) -> Image.Image:\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        assert im.palette is not None\n        if im.palette.mode == \"RGBA\":\n            for rgba in im.palette.colors:\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\n_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette]\n\n\ndef _normalize_palette(\n    im: Image.Image, palette: _Palette | None, info: dict[str, Any]\n) -> Image.Image:\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            im_palette = im.getpalette(None)\n            assert im_palette is not None\n            source_palette = bytearray(im_palette)\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    assert source_palette is not None\n\n    if palette:\n        used_palette_colors: list[int | None] = []\n        assert im.palette is not None\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        dest_map: list[int] = []\n        for index in used_palette_colors:\n            assert index is not None\n            dest_map.append(index)\n        im = im.remap_palette(dest_map)\n    else:\n        optimized_palette_colors = _get_optimize(im, info)\n        if optimized_palette_colors is not None:\n            im = im.remap_palette(optimized_palette_colors, source_palette)\n            if \"transparency\" in info:\n                try:\n                    info[\"transparency\"] = optimized_palette_colors.index(\n                        info[\"transparency\"]\n                    )\n                except ValueError:\n                    del info[\"transparency\"]\n            return im\n\n    assert im.palette is not None\n    im.palette.palette = source_palette\n    return im\n\n\ndef _write_single_frame(\n    im: Image.Image,\n    fp: IO[bytes],\n    palette: _Palette | None,\n) -> None:\n    im_out = _normalize_mode(im)\n    for k, v in im_out.info.items():\n        if isinstance(k, str):\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(\n        im_out, fp, [ImageFile._Tile(\"gif\", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]\n    )\n\n    fp.write(b\"\\0\")  # end of image data\n\n\ndef _getbbox(\n    base_im: Image.Image, im_frame: Image.Image\n) -> tuple[Image.Image, tuple[int, int, int, int] | None]:\n    palette_bytes = [\n        bytes(im.palette.palette) if im.palette else b\"\" for im in (base_im, im_frame)\n    ]\n    if palette_bytes[0] != palette_bytes[1]:\n        im_frame = im_frame.convert(\"RGBA\")\n        base_im = base_im.convert(\"RGBA\")\n    delta = ImageChops.subtract_modulo(im_frame, base_im)\n    return delta, delta.getbbox(alpha_only=False)\n\n\nclass _Frame(NamedTuple):\n    im: Image.Image\n    bbox: tuple[int, int, int, int] | None\n    encoderinfo: dict[str, Any]\n\n\ndef _write_multiple_frames(\n    im: Image.Image, fp: IO[bytes], palette: _Palette | None\n) -> bool:\n    duration = im.encoderinfo.get(\"duration\")\n    disposal = im.encoderinfo.get(\"disposal\", im.info.get(\"disposal\"))\n\n    im_frames: list[_Frame] = []\n    previous_im: Image.Image | None = None\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                    if isinstance(k, str):\n                        im.encoderinfo.setdefault(k, v)\n\n            encoderinfo = im.encoderinfo.copy()\n            if \"transparency\" in im_frame.info:\n                encoderinfo.setdefault(\"transparency\", im_frame.info[\"transparency\"])\n            im_frame = _normalize_palette(im_frame, palette, encoderinfo)\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            diff_frame = None\n            if im_frames and previous_im:\n                # delta frame\n                delta, bbox = _getbbox(previous_im, im_frame)\n                if not bbox:\n                    # This frame is identical to the previous frame\n                    if encoderinfo.get(\"duration\"):\n                        im_frames[-1].encoderinfo[\"duration\"] += encoderinfo[\"duration\"]\n                    continue\n                if im_frames[-1].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                        first_palette = im_frames[0].im.palette\n                        assert first_palette is not None\n                        background_im.putpalette(first_palette, first_palette.mode)\n                    bbox = _getbbox(background_im, im_frame)[1]\n                elif encoderinfo.get(\"optimize\") and im_frame.mode != \"1\":\n                    if \"transparency\" not in encoderinfo:\n                        assert im_frame.palette is not None\n                        try:\n                            encoderinfo[\"transparency\"] = (\n                                im_frame.palette._new_color_index(im_frame)\n                            )\n                        except ValueError:\n                            pass\n                    if \"transparency\" in encoderinfo:\n                        # When the delta is zero, fill the image with transparency\n                        diff_frame = im_frame.copy()\n                        fill = Image.new(\"P\", delta.size, encoderinfo[\"transparency\"])\n                        if delta.mode == \"RGBA\":\n                            r, g, b, a = delta.split()\n                            mask = ImageMath.lambda_eval(\n                                lambda args: args[\"convert\"](\n                                    args[\"max\"](\n                                        args[\"max\"](\n                                            args[\"max\"](args[\"r\"], args[\"g\"]), args[\"b\"]\n                                        ),\n                                        args[\"a\"],\n                                    )\n                                    * 255,\n                                    \"1\",\n                                ),\n                                r=r,\n                                g=g,\n                                b=b,\n                                a=a,\n                            )\n                        else:\n                            if delta.mode == \"P\":\n                                # Convert to L without considering palette\n                                delta_l = Image.new(\"L\", delta.size)\n                                delta_l.putdata(delta.getdata())\n                                delta = delta_l\n                            mask = ImageMath.lambda_eval(\n                                lambda args: args[\"convert\"](args[\"im\"] * 255, \"1\"),\n                                im=delta,\n                            )\n                        diff_frame.paste(fill, mask=ImageOps.invert(mask))\n            else:\n                bbox = None\n            previous_im = im_frame\n            im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))\n\n    if len(im_frames) == 1:\n        if \"duration\" in im.encoderinfo:\n            # Since multiple frames will not be written, use the combined duration\n            im.encoderinfo[\"duration\"] = im_frames[0].encoderinfo[\"duration\"]\n        return False\n\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\n\ndef _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:\n    _save(im, fp, filename, save_all=True)\n\n\ndef _save(\n    im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False\n) -> None:\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.setdefault(\"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: Image.Image) -> int:\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(\n    fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int\n) -> None:\n    try:\n        transparency = im.encoderinfo[\"transparency\"]\n    except KeyError:\n        transparency = None\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 transparency is not None or duration != 0 or disposal:\n        packed_flag = 1 if transparency is not None else 0\n        packed_flag |= disposal << 2\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 or 0)  # 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: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:\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                assert quant_proc.stdout is not None\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: Image.Image, info: dict[str, Any]) -> list[int] | None:\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\"):\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            assert im.palette is not None\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    return None\n\n\ndef _get_color_table_size(palette_bytes: bytes) -> int:\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: bytes) -> 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: Image.Image) -> bytes:\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    if not im.palette:\n        return b\"\"\n\n    palette = bytes(im.palette.palette)\n    if im.palette.mode == \"RGBA\":\n        palette = b\"\".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3))\n    return palette\n\n\ndef _get_background(\n    im: Image.Image,\n    info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None,\n) -> int:\n    background = 0\n    if info_background:\n        if isinstance(info_background, tuple):\n            # WebPImagePlugin stores an RGBA value in info[\"background\"]\n            # So it must be converted to the same format as GifImagePlugin's\n            # info[\"background\"] - a global color table index\n            assert im.palette is not None\n            try:\n                background = im.palette.getcolor(info_background, im)\n            except ValueError as e:\n                if str(e) not in (\n                    # If all 256 colors are in use,\n                    # then there is no need for the background color\n                    \"cannot allocate more than 256 colors\",\n                    # Ignore non-opaque WebP background\n                    \"cannot add non-opaque RGBA color to RGB palette\",\n                ):\n                    raise\n        else:\n            background = info_background\n    return background\n\n\ndef _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]:\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 info.get(\"loop\") is not None\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 info.get(\"loop\") is not None:\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(\n    fp: IO[bytes],\n    im_frame: Image.Image,\n    offset: tuple[int, int],\n    params: dict[str, Any],\n) -> None:\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,\n            fp,\n            [ImageFile._Tile(\"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(\n    im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None\n) -> tuple[list[bytes], list[int] | 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    if info is None:\n        info = {}\n\n    used_palette_colors = _get_optimize(im, 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(\n    im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any\n) -> list[bytes]:\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    from io import BytesIO\n\n    class Collector(BytesIO):\n        data = []\n\n        def write(self, data: Buffer) -> int:\n            self.data.append(data)\n            return len(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": "a7c4f8b2c",
                "new_commit_hash": "47022d584",
                "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": 695,
                            "length": 8
                        },
                        "new_range": {
                            "start": 695,
                            "length": 9
                        },
                        "section": "def _write_multiple_frames("
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "                        )",
                                "type": "context"
                            },
                            {
                                "content": "                        background = _get_background(im_frame, color)",
                                "type": "context"
                            },
                            {
                                "content": "                        background_im = Image.new(\"P\", im_frame.size, background)",
                                "type": "context"
                            },
                            {
                                "content": "                        assert im_frames[0].im.palette is not None",
                                "type": "deleted"
                            },
                            {
                                "content": "                        background_im.putpalette(im_frames[0].im.palette)",
                                "type": "deleted"
                            },
                            {
                                "content": "                        first_palette = im_frames[0].im.palette",
                                "type": "added"
                            },
                            {
                                "content": "                        assert first_palette is not None",
                                "type": "added"
                            },
                            {
                                "content": "                        background_im.putpalette(first_palette, first_palette.mode)",
                                "type": "added"
                            },
                            {
                                "content": "                    bbox = _getbbox(background_im, im_frame)[1]",
                                "type": "context"
                            },
                            {
                                "content": "                elif encoderinfo.get(\"optimize\") and im_frame.mode != \"1\":",
                                "type": "context"
                            },
                            {
                                "content": "                    if \"transparency\" not in 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(\n    im: Image.Image, fp: IO[bytes], palette: _Palette | None\n) -> bool:\n    duration = im.encoderinfo.get(\"duration\")\n    disposal = im.encoderinfo.get(\"disposal\", im.info.get(\"disposal\"))\n\n    im_frames: list[_Frame] = []\n    previous_im: Image.Image | None = None\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                    if isinstance(k, str):\n                        im.encoderinfo.setdefault(k, v)\n\n            encoderinfo = im.encoderinfo.copy()\n            if \"transparency\" in im_frame.info:\n                encoderinfo.setdefault(\"transparency\", im_frame.info[\"transparency\"])\n            im_frame = _normalize_palette(im_frame, palette, encoderinfo)\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            diff_frame = None\n            if im_frames and previous_im:\n                # delta frame\n                delta, bbox = _getbbox(previous_im, im_frame)\n                if not bbox:\n                    # This frame is identical to the previous frame\n                    if encoderinfo.get(\"duration\"):\n                        im_frames[-1].encoderinfo[\"duration\"] += encoderinfo[\"duration\"]\n                    continue\n                if im_frames[-1].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                        assert im_frames[0].im.palette is not None\n                        background_im.putpalette(im_frames[0].im.palette)\n                    bbox = _getbbox(background_im, im_frame)[1]\n                elif encoderinfo.get(\"optimize\") and im_frame.mode != \"1\":\n                    if \"transparency\" not in encoderinfo:\n                        assert im_frame.palette is not None\n                        try:\n                            encoderinfo[\"transparency\"] = (\n                                im_frame.palette._new_color_index(im_frame)\n                            )\n                        except ValueError:\n                            pass\n                    if \"transparency\" in encoderinfo:\n                        # When the delta is zero, fill the image with transparency\n                        diff_frame = im_frame.copy()\n                        fill = Image.new(\"P\", delta.size, encoderinfo[\"transparency\"])\n                        if delta.mode == \"RGBA\":\n                            r, g, b, a = delta.split()\n                            mask = ImageMath.lambda_eval(\n                                lambda args: args[\"convert\"](\n                                    args[\"max\"](\n                                        args[\"max\"](\n                                            args[\"max\"](args[\"r\"], args[\"g\"]), args[\"b\"]\n                                        ),\n                                        args[\"a\"],\n                                    )\n                                    * 255,\n                                    \"1\",\n                                ),\n                                r=r,\n                                g=g,\n                                b=b,\n                                a=a,\n                            )\n                        else:\n                            if delta.mode == \"P\":\n                                # Convert to L without considering palette\n                                delta_l = Image.new(\"L\", delta.size)\n                                delta_l.putdata(delta.getdata())\n                                delta = delta_l\n                            mask = ImageMath.lambda_eval(\n                                lambda args: args[\"convert\"](args[\"im\"] * 255, \"1\"),\n                                im=delta,\n                            )\n                        diff_frame.paste(fill, mask=ImageOps.invert(mask))\n            else:\n                bbox = None\n            previous_im = im_frame\n            im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))\n\n    if len(im_frames) == 1:\n        if \"duration\" in im.encoderinfo:\n            # Since multiple frames will not be written, use the combined duration\n            im.encoderinfo[\"duration\"] = im_frames[0].encoderinfo[\"duration\"]\n        return False\n\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",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 649,
                            "end_lineno": 769,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "8af2d7640ead3ddeba9fdc598dc8e72d580dfdfb^",
    "new_commit_hash": "8af2d7640ead3ddeba9fdc598dc8e72d580dfdfb",
    "commit_message": "Pass palette mode to putpalette",
    "commit_date": "2024-10-24T23:26:13+11:00",
    "metadata": {}
}
{
    "repo_name": "pillow",
    "new_commit_hash": "8af2d7640ead3ddeba9fdc598dc8e72d580dfdfb",
    "test_file_codes": [
        "from __future__ import annotations\n\nimport warnings\nfrom collections.abc import Generator\nfrom io import BytesIO\nfrom pathlib import Path\nfrom typing import Any\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() -> None:\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() -> None:\n    def open() -> None:\n        im = Image.open(TEST_GIF)\n        im.load()\n\n    with pytest.warns(ResourceWarning):\n        open()\n\n\ndef test_closed_file() -> None:\n    with warnings.catch_warnings():\n        im = Image.open(TEST_GIF)\n        im.load()\n        im.close()\n\n\ndef test_seek_after_close() -> None:\n    im = Image.open(\"Tests/images/iss634.gif\")\n    assert isinstance(im, GifImagePlugin.GifImageFile)\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() -> None:\n    with warnings.catch_warnings():\n        with Image.open(TEST_GIF) as im:\n            im.load()\n\n\ndef test_invalid_file() -> None:\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() -> None:\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() -> None:\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() -> None:\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() -> None:\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() -> None:\n    def test_grayscale(optimize: int) -> int:\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: int) -> int:\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\n@pytest.mark.parametrize(\n    \"colors, size, expected_palette_length\",\n    (\n        # These do optimize the palette\n        (256, 511, 256),\n        (255, 511, 255),\n        (129, 511, 129),\n        (128, 511, 128),\n        (64, 511, 64),\n        (4, 511, 4),\n        # These don't optimize the palette\n        (128, 513, 256),\n        (64, 513, 256),\n        (4, 513, 256),\n    ),\n)\ndef test_optimize_correctness(\n    colors: int, size: int, expected_palette_length: int\n) -> None:\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\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\ndef test_optimize_full_l() -> None:\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() -> None:\n    im = Image.new(\"P\", (8, 1))\n    im.palette = ImagePalette.raw(\"RGB\", bytes((0, 0, 0) * 150))\n    for i in range(8):\n        im.putpixel((i, 0), (i + 1, 0, 0))\n\n    for optimize, colors in ((False, 256), (True, 8)):\n        out = BytesIO()\n        im.save(out, \"GIF\", optimize=optimize)\n        with Image.open(out) as reloaded:\n            assert len(reloaded.palette.palette) // 3 == colors\n\n\ndef test_full_palette_second_frame(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"P\", (1, 256))\n\n    full_palette_im = Image.new(\"P\", (1, 256))\n    for i in range(256):\n        full_palette_im.putpixel((0, i), i)\n    full_palette_im.palette = ImagePalette.ImagePalette(\n        \"RGB\", bytearray(i // 3 for i in range(768))\n    )\n    full_palette_im.palette.dirty = 1\n\n    im.save(out, save_all=True, append_images=[full_palette_im])\n\n    with Image.open(out) as reloaded:\n        reloaded.seek(1)\n\n        for i in range(256):\n            reloaded.getpixel((0, i)) == i\n\n\ndef test_roundtrip(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = hopper()\n    im.save(out)\n    with Image.open(out) as reread:\n        assert_image_similar(reread.convert(\"RGB\"), im, 50)\n\n\ndef test_roundtrip2(tmp_path: Path) -> None:\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        assert_image_similar(reread.convert(\"RGB\"), hopper(), 50)\n\n\ndef test_roundtrip_save_all(tmp_path: Path) -> None:\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        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\ndef test_roundtrip_save_all_1(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"1\", (1, 1))\n    im2 = Image.new(\"1\", (1, 1), 1)\n    im.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reloaded:\n        assert reloaded.getpixel((0, 0)) == 0\n\n        reloaded.seek(1)\n        assert reloaded.getpixel((0, 0)) == 255\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: str, mode: str) -> None:\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: Path) -> None:\n    important_headers = [\"background\", \"version\", \"duration\", \"loop\"]\n    # Multiframe image\n    with Image.open(\"Tests/images/dispose_bgnd.gif\") as im:\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        for header in important_headers:\n            assert info[header] == reread.info[header]\n\n\ndef test_palette_handling(tmp_path: Path) -> None:\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        assert_image_similar(im, reloaded.convert(\"RGB\"), 10)\n\n\ndef test_palette_434(tmp_path: Path) -> None:\n    # see https://github.com/python-pillow/Pillow/issues/434\n\n    def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:\n        out = str(tmp_path / \"temp.gif\")\n        im.copy().save(out, \"GIF\", **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        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: Path) -> None:\n    with Image.open(TEST_GIF) as img:\n        img = img.convert(\"RGB\")\n\n        tempfile = str(tmp_path / \"temp.gif\")\n        b = BytesIO()\n        GifImagePlugin._save_netpbm(img, b, 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: Path) -> None:\n    with Image.open(TEST_GIF) as img:\n        img = img.convert(\"L\")\n\n        tempfile = str(tmp_path / \"temp.gif\")\n        b = BytesIO()\n        GifImagePlugin._save_netpbm(img, b, tempfile)\n        with Image.open(tempfile) as reloaded:\n            assert_image_similar(img, reloaded.convert(\"L\"), 0)\n\n\ndef test_seek() -> None:\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() -> None:\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() -> None:\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: str, n_frames: int) -> None:\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() -> None:\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() -> None:\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() -> None:\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() -> 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() -> None:\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() -> None:\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() -> None:\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(\n    loading_strategy: GifImagePlugin.LoadingStrategy,\n    expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]],\n) -> None:\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() -> None:\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() -> None:\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() -> None:\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: Path) -> None:\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        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: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    # Four colors: white, gray, 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            # Check that frame transparency wasn't added unnecessarily\n            assert getattr(img, \"_frame_transparency\") is None\n\n\ndef test_dispose2_diff(tmp_path: Path) -> None:\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: Path) -> None:\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: Path) -> None:\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_dispose2_previous_frame(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"P\", (100, 100))\n    im.info[\"transparency\"] = 0\n    d = ImageDraw.Draw(im)\n    d.rectangle([(0, 0), (100, 50)], 1)\n    im.putpalette((0, 0, 0, 255, 0, 0))\n\n    im2 = Image.new(\"P\", (100, 100))\n    im2.putpalette((0, 0, 0))\n\n    im.save(out, save_all=True, append_images=[im2], disposal=[0, 2])\n\n    with Image.open(out) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 255)\n\n\ndef test_transparency_in_second_frame(tmp_path: Path) -> None:\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() -> None:\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: Path) -> None:\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: Path) -> None:\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: Path) -> None:\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        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        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: Path) -> None:\n    duration_list = [100, 500, 500]\n\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/transparent_dispose.gif\") as im:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(im)\n        ] == duration_list\n\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reloaded:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(reloaded)\n        ] == duration_list\n\n\ndef test_roundtrip_info_duration_combined(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    with Image.open(\"Tests/images/duplicate_frame.gif\") as im:\n        assert [frame.info[\"duration\"] for frame in ImageSequence.Iterator(im)] == [\n            1000,\n            1000,\n            1000,\n        ]\n        im.save(out, save_all=True)\n\n    with Image.open(out) as reloaded:\n        assert [\n            frame.info[\"duration\"] for frame in ImageSequence.Iterator(reloaded)\n        ] == [1000, 2000]\n\n\ndef test_identical_frames(tmp_path: Path) -> None:\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        # 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\",\n    (\n        [1000, 1500, 2000],\n        (1000, 1500, 2000),\n        # One more duration than the number of frames\n        [1000, 1500, 2000, 4000],\n        1500,\n    ),\n)\ndef test_identical_frames_to_single_frame(\n    duration: int | list[int], tmp_path: Path\n) -> None:\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\"] == 4500\n\n\ndef test_loop_none(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.save(out, loop=None)\n    with Image.open(out) as reread:\n        assert \"loop\" not in reread.info\n\n\ndef test_number_of_loops(tmp_path: Path) -> None:\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: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"background\"] = 1\n    im.save(out)\n    with Image.open(out) as reread:\n        assert reread.info[\"background\"] == im.info[\"background\"]\n\n\ndef test_webp_background(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    # Test opaque WebP background\n    if features.check(\"webp\"):\n        with Image.open(\"Tests/images/hopper.webp\") as im:\n            assert im.info[\"background\"] == (255, 255, 255, 255)\n            im.save(out)\n\n    # Test non-opaque WebP background\n    im = Image.new(\"L\", (100, 100), \"#000\")\n    im.info[\"background\"] = (0, 0, 0, 0)\n    im.save(out)\n\n\ndef test_comment(tmp_path: Path) -> None:\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: Path) -> None:\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() -> None:\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() -> None:\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: Path) -> None:\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: Path) -> None:\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: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    def assert_version_after_save(im: Image.Image, version: bytes) -> None:\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: Path) -> None:\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: list[Image.Image]) -> Generator[Image.Image, None, None]:\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_append_different_size_image(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"RGB\", (100, 100))\n    bigger_im = Image.new(\"RGB\", (200, 200), \"#f00\")\n\n    im.save(out, save_all=True, append_images=[bigger_im])\n\n    with Image.open(out) as reread:\n        assert reread.size == (100, 100)\n\n        reread.seek(1)\n        assert reread.size == (100, 100)\n\n\ndef test_transparent_optimize(tmp_path: Path) -> None:\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_removed_transparency(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n    im = Image.new(\"RGB\", (256, 1))\n\n    for x in range(256):\n        im.putpixel((x, 0), (x, 0, 0))\n\n    im.info[\"transparency\"] = (255, 255, 255)\n    with pytest.warns(UserWarning):\n        im.save(out)\n\n    with Image.open(out) as reloaded:\n        assert \"transparency\" not in reloaded.info\n\n\ndef test_rgb_transparency(tmp_path: Path) -> None:\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    with pytest.warns(UserWarning):\n        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: Path) -> None:\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_background_outside_palettte(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/background_outside_palette.gif\") as im:\n        im.seek(1)\n        assert im.info[\"background\"] == 255\n\n\ndef test_bbox(tmp_path: Path) -> None:\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_bbox_alpha(tmp_path: Path) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im = Image.new(\"RGBA\", (1, 2), (255, 0, 0, 255))\n    im.putpixel((0, 1), (255, 0, 0, 0))\n    im2 = Image.new(\"RGBA\", (1, 2), (255, 0, 0, 0))\n    im.save(out, save_all=True, append_images=[im2])\n\n    with Image.open(out) as reread:\n        assert reread.n_frames == 2\n\n\ndef test_palette_save_L(tmp_path: Path) -> None:\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 = im.getpalette()\n    assert palette is not None\n\n    out = str(tmp_path / \"temp.gif\")\n    im_l.save(out, palette=bytes(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: Path) -> None:\n    im = Image.new(\"P\", (1, 2))\n    im.putpixel((0, 1), 1)\n\n    out = str(tmp_path / \"temp.gif\")\n    im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))\n\n    with Image.open(out) as reloaded:\n        reloaded_rgb = reloaded.convert(\"RGB\")\n\n        assert reloaded_rgb.getpixel((0, 0)) == (1, 2, 3)\n        assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)\n\n\ndef test_palette_save_duplicate_entries(tmp_path: Path) -> None:\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: Path) -> None:\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: Path) -> None:\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: Path) -> None:\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() -> None:\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() -> None:\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\n@pytest.mark.parametrize(\n    \"test_file, loading_strategy\",\n    (\n        (\"test_extents.gif\", GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST),\n        (\n            \"test_extents.gif\",\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,\n        ),\n        (\n            \"test_extents_transparency.gif\",\n            GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,\n        ),\n    ),\n)\ndef test_extents(\n    test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy\n) -> None:\n    GifImagePlugin.LOADING_STRATEGY = loading_strategy\n    try:\n        with Image.open(\"Tests/images/\" + test_file) 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            im.load()\n            assert im.im.size == (150, 150)\n    finally:\n        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST\n\n\ndef test_missing_background() -> None:\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: Path) -> None:\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\n\n@pytest.mark.parametrize(\"params\", ({}, {\"disposal\": 2, \"optimize\": False}))\ndef test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:\n    out = str(tmp_path / \"temp.gif\")\n\n    im1 = Image.new(\"P\", (100, 100))\n    d = ImageDraw.Draw(im1)\n    d.ellipse([(40, 40), (60, 60)], fill=1)\n    data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254\n    im1.putpalette(data, \"RGBA\")\n\n    im2 = Image.new(\"P\", (100, 100))\n    im2.putpalette(data, \"RGBA\")\n\n    im1.save(out, save_all=True, append_images=[im2], **params)\n\n    with Image.open(out) as reloaded:\n        assert reloaded.n_frames == 2\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": "warning: `VIRTUAL_ENV=/home/gcpuser/r2e-edits-internal/.venv` does not match the project environment path `.venv` and will be ignored\nUsing CPython 3.9.21\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 12 packages in 11ms\nInstalled 12 packages in 115ms\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 1.24s\nPrepared 1 package in 34.23s\nInstalled 1 package in 0.96ms\n + pillow==11.1.0.dev0 (from file:///home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_8af2d7640ead3ddeba9fdc598dc8e72d580dfdfb)\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_8af2d7640ead3ddeba9fdc598dc8e72d580dfdfb\nconfigfile: pyproject.toml\nplugins: cov-6.0.0\ncollected 105 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[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 [ 49%]\n\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\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[256-511-256]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[255-511-255]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[129-511-129]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[128-511-128]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[64-511-64]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[4-511-4]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[128-513-256]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[64-513-256]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[4-513-256]\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_full_palette_second_frame\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_roundtrip_save_all_1\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_dispose2_previous_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_roundtrip_info_duration_combined\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[duration0]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[duration1]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[duration2]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[1500]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_loop_none\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_number_of_loops\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_background\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_webp_background\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_comment\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_comment_over_255\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_zero_comment_subblocks\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_read_multiple_comment_blocks\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_empty_string_comment\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_retain_comment_in_subsequent_frames\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_version\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_append_images\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_append_different_size_image\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_removed_transparency\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_background_outside_palettte\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_bbox_alpha\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[test_extents.gif-LoadingStrategy.RGB_AFTER_FIRST]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_extents[test_extents.gif-LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_extents[test_extents_transparency.gif-LoadingStrategy.RGB_AFTER_FIRST]\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[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_p_rgba[params0]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_p_rgba[params1]\u001b[0m\n\u001b[33mSKIPPED\u001b[0m [1] r2e_tests/test_1.py:376: Netpbm not available\n\u001b[33mSKIPPED\u001b[0m [1] r2e_tests/test_1.py:388: Netpbm not available\n\u001b[32m======================== \u001b[32m\u001b[1m103 passed\u001b[0m, \u001b[33m2 skipped\u001b[0m\u001b[32m in 1.16s\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_8af2d7640ead3ddeba9fdc598dc8e72d580dfdfb\nconfigfile: pyproject.toml\nplugins: cov-6.0.0\ncollected 105 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[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 [ 49%]\n\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\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[31m                    [100%]\u001b[0m\n\n=================================== FAILURES ===================================\n\u001b[31m\u001b[1m_____________________________ test_p_rgba[params1] _____________________________\u001b[0m\n\ntmp_path = PosixPath('/tmp/pytest-of-gcpuser/pytest-448/test_p_rgba_params1_0')\nparams = {'disposal': 2, 'optimize': False}\n\n    @pytest.mark.parametrize(\"params\", ({}, {\"disposal\": 2, \"optimize\": False}))\n    def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:\n        out = str(tmp_path / \"temp.gif\")\n    \n        im1 = Image.new(\"P\", (100, 100))\n        d = ImageDraw.Draw(im1)\n        d.ellipse([(40, 40), (60, 60)], fill=1)\n        data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254\n        im1.putpalette(data, \"RGBA\")\n    \n        im2 = Image.new(\"P\", (100, 100))\n        im2.putpalette(data, \"RGBA\")\n    \n>       im1.save(out, save_all=True, append_images=[im2], **params)\n\n\u001b[1m\u001b[31mr2e_tests/test_1.py\u001b[0m:1448: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\u001b[1m\u001b[31msrc/PIL/Image.py\u001b[0m:2605: in save\n    save_handler(self, fp, filename)\n\u001b[1m\u001b[31msrc/PIL/GifImagePlugin.py\u001b[0m:773: in _save_all\n    _save(im, fp, filename, save_all=True)\n\u001b[1m\u001b[31msrc/PIL/GifImagePlugin.py\u001b[0m:786: in _save\n    if not save_all or not _write_multiple_frames(im, fp, palette):\n\u001b[1m\u001b[31msrc/PIL/GifImagePlugin.py\u001b[0m:699: in _write_multiple_frames\n    background_im.putpalette(im_frames[0].im.palette)\n\u001b[1m\u001b[31msrc/PIL/Image.py\u001b[0m:2103: in putpalette\n    self.load()  # install new palette\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <PIL.Image.Image image mode=P size=100x100 at 0x77AFCA2F8E50>\n\n    def load(self) -> core.PixelAccess | None:\n        \"\"\"\n        Allocates storage for the image and loads the pixel data.  In\n        normal cases, you don't need to call this method, since the\n        Image class automatically loads an opened image when it is\n        accessed for the first time.\n    \n        If the file associated with the image was opened by Pillow, then this\n        method will close it. The exception to this is if the image has\n        multiple frames, in which case the file will be left open for seek\n        operations. See :ref:`file-handling` for more information.\n    \n        :returns: An image access object.\n        :rtype: :py:class:`.PixelAccess`\n        \"\"\"\n        if self._im is not None and self.palette and self.palette.dirty:\n            # realize palette\n            mode, arr = self.palette.getdata()\n>           self.im.putpalette(self.palette.mode, mode, arr)\n\u001b[1m\u001b[31mE           ValueError: invalid palette size\u001b[0m\n\n\u001b[1m\u001b[31msrc/PIL/Image.py\u001b[0m:909: ValueError\n==================================== PASSES ====================================\n\u001b[36m\u001b[1m=========================== short test summary info ============================\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_sanity\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_unclosed_file\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_closed_file\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_seek_after_close\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_context_manager\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_invalid_file\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_l_mode_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_l_mode_after_rgb\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_palette_not_needed_for_second_frame\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_strategy\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[256-511-256]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[255-511-255]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[129-511-129]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[128-511-128]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[64-511-64]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[4-511-4]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[128-513-256]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[64-513-256]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_optimize_correctness[4-513-256]\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_full_palette_second_frame\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_roundtrip_save_all_1\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_dispose2_previous_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_roundtrip_info_duration_combined\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[duration0]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[duration1]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[duration2]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_identical_frames_to_single_frame[1500]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_loop_none\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_number_of_loops\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_background\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_webp_background\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_comment\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_comment_over_255\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_zero_comment_subblocks\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_read_multiple_comment_blocks\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_empty_string_comment\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_retain_comment_in_subsequent_frames\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_version\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_append_images\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_append_different_size_image\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_removed_transparency\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_background_outside_palettte\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_bbox_alpha\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[test_extents.gif-LoadingStrategy.RGB_AFTER_FIRST]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_extents[test_extents.gif-LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_extents[test_extents_transparency.gif-LoadingStrategy.RGB_AFTER_FIRST]\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[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_p_rgba[params0]\u001b[0m\n\u001b[33mSKIPPED\u001b[0m [1] r2e_tests/test_1.py:376: Netpbm not available\n\u001b[33mSKIPPED\u001b[0m [1] r2e_tests/test_1.py:388: Netpbm not available\n\u001b[31mFAILED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_p_rgba[params1]\u001b[0m - ValueError: invalid palette size\n\u001b[31m=================== \u001b[31m\u001b[1m1 failed\u001b[0m, \u001b[32m102 passed\u001b[0m, \u001b[33m2 skipped\u001b[0m\u001b[31m in 1.09s\u001b[0m\u001b[31m ===================\u001b[0m\n",
    "old_commit_res_stderr": ""
}
Tests/test_file_gif.py src/PIL/GifImagePlugin.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** ValueError: Invalid palette size when saving multi-frame P-mode GIF with specific parameters

**Description:**
When attempting to save a multi-frame GIF image in mode "P" with the parameters `disposal=2` and `optimize=False`, a `ValueError` is raised indicating an invalid palette size. This issue occurs during the palette assignment for the background image, preventing the GIF from being saved successfully.

**Example Code:**
```python
from PIL import Image, ImageDraw, ImageSequence

# Create the first palette-based image
im1 = Image.new("P", (100, 100))
draw = ImageDraw.Draw(im1)
draw.ellipse([(40, 40), (60, 60)], fill=1)
palette_data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254
im1.putpalette(palette_data, "RGBA")

# Create a second palette-based image
im2 = Image.new("P", (100, 100))
im2.putpalette(palette_data, "RGBA")

# Attempt to save the GIF with specific parameters
out = "temp.gif"
im1.save(out, save_all=True, append_images=[im2], disposal=2, optimize=False)
```

**Expected Behavior:**
The GIF should be saved successfully without any errors, correctly applying the specified disposal method and optimization settings.

**Actual Behavior:**
A `ValueError` is raised with the message "invalid palette size," preventing the GIF from being saved.

**Error Message:**
```
ValueError: invalid palette size
```
[/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.
#
from __future__ import annotations

import itertools
import math
import os
import subprocess
from enum import IntEnum
from functools import cached_property
from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union

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

if TYPE_CHECKING:
    from . import _imaging
    from ._typing import Buffer


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: bytes) -> bool:
    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) -> bytes | None:
        s = self.fp.read(1)
        if s and s[0]:
            return self.fp.read(s[0])
        return None

    def _is_palette_needed(self, p: bytes) -> bool:
        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) -> None:
        # Screen
        s = self.fp.read(13)
        if not _accept(s):
            msg = "not a GIF file"
            raise SyntaxError(msg)

        self.info["version"] = s[:6]
        self._size = i16(s, 6), i16(s, 8)
        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: int | None = None
        self._seek(0)  # get ready to read first frame

    @property
    def n_frames(self) -> int:
        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

    @cached_property
    def is_animated(self) -> bool:
        if self._n_frames is not None:
            return self._n_frames != 1

        current = self.tell()
        if current:
            return True

        try:
            self._seek(1, False)
            is_animated = True
        except EOFError:
            is_animated = False

        self.seek(current)
        return is_animated

    def seek(self, frame: int) -> None:
        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)
                msg = "no more images in GIF file"
                raise EOFError(msg) from e

    def _seek(self, frame: int, update_image: bool = True) -> None:
        if frame == 0:
            # rewind
            self.__offset = 0
            self.dispose: _imaging.ImagingCore | None = 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:
            msg = f"cannot seek to frame {frame}"
            raise ValueError(msg)

        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";":
            msg = "no more images in GIF file"
            raise EOFError(msg)

        palette: ImagePalette.ImagePalette | Literal[False] | None = None

        info: dict[str, Any] = {}
        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 and block is not None:
                    #
                    # 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 and block is not None:
                    #
                    # application extension
                    #
                    info["extension"] = block, self.fp.tell()
                    if block[:11] == b"NETSCAPE2.0":
                        block = self.data()
                        if block and 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
            s = None

        if interlace is None:
            msg = "image not found in GIF frame"
            raise EOFError(msg)

        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 palette:
                self.palette = palette
            elif self.global_palette:
                from copy import copy

                self.palette = copy(self.global_palette)
            else:
                self.palette = None
        else:
            if self.mode == "P":
                if (
                    LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
                    or palette
                ):
                    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: int) -> tuple[int, int, int]:
            if self._frame_palette:
                if color * 3 + 3 > len(self._frame_palette.palette):
                    color = 0
                return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
            else:
                return (color, color, color)

        self.dispose = None
        self.dispose_extent = frame_dispose_extent
        if self.dispose_extent and self.disposal_method >= 2:
            try:
                if 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 = [
                ImageFile._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) -> None:
        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("RGB", *self._frame_palette.getdata())
            else:
                self._im = None
        if not self._prev_im and self._im is not None and self.size != self.im.size:
            expanded_im = Image.core.fill(self.im.mode, self.size)
            if self._frame_palette:
                expanded_im.putpalette("RGB", *self._frame_palette.getdata())
            expanded_im.paste(self.im, (0, 0) + self.im.size)

            self.im = expanded_im
        self._mode = temp_mode
        self._frame_palette = None

        super().load_prepare()

    def load_end(self) -> None:
        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.size != self._prev_im.size:
            if self._frame_transparency is not None:
                expanded_im = Image.core.fill("RGBA", self.size)
            else:
                expanded_im = Image.core.fill("P", self.size)
                expanded_im.putpalette("RGB", "RGB", self.im.getpalette())
                expanded_im = expanded_im.convert("RGB")
            expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size)

            self._prev_im = expanded_im
            assert self._prev_im is not None
        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")

        assert self.dispose_extent is not None
        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) -> int:
        return self.__frame


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


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


def _normalize_mode(im: Image.Image) -> Image.Image:
    """
    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)
        assert im.palette is not None
        if im.palette.mode == "RGBA":
            for rgba in im.palette.colors:
                if rgba[3] == 0:
                    im.info["transparency"] = im.palette.colors[rgba]
                    break
        return im
    return im.convert("L")


_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette]


def _normalize_palette(
    im: Image.Image, palette: _Palette | None, info: dict[str, Any]
) -> Image.Image:
    """
    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:
            im_palette = im.getpalette(None)
            assert im_palette is not None
            source_palette = bytearray(im_palette)
    else:  # L-mode
        if not source_palette:
            source_palette = bytearray(i // 3 for i in range(768))
        im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
    assert source_palette is not None

    if palette:
        used_palette_colors: list[int | None] = []
        assert im.palette is not None
        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
        dest_map: list[int] = []
        for index in used_palette_colors:
            assert index is not None
            dest_map.append(index)
        im = im.remap_palette(dest_map)
    else:
        optimized_palette_colors = _get_optimize(im, info)
        if optimized_palette_colors is not None:
            im = im.remap_palette(optimized_palette_colors, source_palette)
            if "transparency" in info:
                try:
                    info["transparency"] = optimized_palette_colors.index(
                        info["transparency"]
                    )
                except ValueError:
                    del info["transparency"]
            return im

    assert im.palette is not None
    im.palette.palette = source_palette
    return im


def _write_single_frame(
    im: Image.Image,
    fp: IO[bytes],
    palette: _Palette | None,
) -> None:
    im_out = _normalize_mode(im)
    for k, v in im_out.info.items():
        if isinstance(k, str):
            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, [ImageFile._Tile("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]
    )

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


def _getbbox(
    base_im: Image.Image, im_frame: Image.Image
) -> tuple[Image.Image, tuple[int, int, int, int] | None]:
    palette_bytes = [
        bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame)
    ]
    if palette_bytes[0] != palette_bytes[1]:
        im_frame = im_frame.convert("RGBA")
        base_im = base_im.convert("RGBA")
    delta = ImageChops.subtract_modulo(im_frame, base_im)
    return delta, delta.getbbox(alpha_only=False)


class _Frame(NamedTuple):
    im: Image.Image
    bbox: tuple[int, int, int, int] | None
    encoderinfo: dict[str, Any]


def _write_multiple_frames(
    im: Image.Image, fp: IO[bytes], palette: _Palette | None
) -> bool:
    duration = im.encoderinfo.get("duration")
    disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))

    im_frames: list[_Frame] = []
    previous_im: Image.Image | None = None
    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
                    if isinstance(k, str):
                        im.encoderinfo.setdefault(k, v)

            encoderinfo = im.encoderinfo.copy()
            if "transparency" in im_frame.info:
                encoderinfo.setdefault("transparency", im_frame.info["transparency"])
            im_frame = _normalize_palette(im_frame, palette, encoderinfo)
            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

            diff_frame = None
            if im_frames and previous_im:
                # delta frame
                delta, bbox = _getbbox(previous_im, im_frame)
                if not bbox:
                    # This frame is identical to the previous frame
                    if encoderinfo.get("duration"):
                        im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"]
                    continue
                if im_frames[-1].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)
                        assert im_frames[0].im.palette is not None
                        background_im.putpalette(im_frames[0].im.palette)
                    bbox = _getbbox(background_im, im_frame)[1]
                elif encoderinfo.get("optimize") and im_frame.mode != "1":
                    if "transparency" not in encoderinfo:
                        assert im_frame.palette is not None
                        try:
                            encoderinfo["transparency"] = (
                                im_frame.palette._new_color_index(im_frame)
                            )
                        except ValueError:
                            pass
                    if "transparency" in encoderinfo:
                        # When the delta is zero, fill the image with transparency
                        diff_frame = im_frame.copy()
                        fill = Image.new("P", delta.size, encoderinfo["transparency"])
                        if delta.mode == "RGBA":
                            r, g, b, a = delta.split()
                            mask = ImageMath.lambda_eval(
                                lambda args: args["convert"](
                                    args["max"](
                                        args["max"](
                                            args["max"](args["r"], args["g"]), args["b"]
                                        ),
                                        args["a"],
                                    )
                                    * 255,
                                    "1",
                                ),
                                r=r,
                                g=g,
                                b=b,
                                a=a,
                            )
                        else:
                            if delta.mode == "P":
                                # Convert to L without considering palette
                                delta_l = Image.new("L", delta.size)
                                delta_l.putdata(delta.getdata())
                                delta = delta_l
                            mask = ImageMath.lambda_eval(
                                lambda args: args["convert"](args["im"] * 255, "1"),
                                im=delta,
                            )
                        diff_frame.paste(fill, mask=ImageOps.invert(mask))
            else:
                bbox = None
            previous_im = im_frame
            im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))

    if len(im_frames) == 1:
        if "duration" in im.encoderinfo:
            # Since multiple frames will not be written, use the combined duration
            im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"]
        return False

    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


def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
    _save(im, fp, filename, save_all=True)


def _save(
    im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
) -> None:
    # 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.setdefault("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: Image.Image) -> int:
    interlace = im.encoderinfo.get("interlace", 1)

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

    return interlace


def _write_local_header(
    fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int
) -> None:
    try:
        transparency = im.encoderinfo["transparency"]
    except KeyError:
        transparency = None

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

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

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

        fp.write(
            b"!"
            + o8(249)  # extension intro
            + o8(4)  # length
            + o8(packed_flag)  # packed fields
            + o16(duration)  # duration
            + o8(transparency or 0)  # 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: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
    # 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
                assert quant_proc.stdout is not None
                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: Image.Image, info: dict[str, Any]) -> list[int] | None:
    """
    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"):
        # 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

            assert im.palette is not None
            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
    return None


def _get_color_table_size(palette_bytes: bytes) -> int:
    # 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: bytes) -> 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: Image.Image) -> bytes:
    """
    Gets the palette for inclusion in the gif header

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

    palette = bytes(im.palette.palette)
    if im.palette.mode == "RGBA":
        palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3))
    return palette


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


def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]:
    """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 info.get("loop") is not None
            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 info.get("loop") is not None:
        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: IO[bytes],
    im_frame: Image.Image,
    offset: tuple[int, int],
    params: dict[str, Any],
) -> None:
    try:
        im_frame.encoderinfo = params

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

        ImageFile._save(
            im_frame,
            fp,
            [ImageFile._Tile("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: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None
) -> tuple[list[bytes], list[int] | 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)

    """
    if info is None:
        info = {}

    used_palette_colors = _get_optimize(im, 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: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any
) -> list[bytes]:
    """
    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

    """
    from io import BytesIO

    class Collector(BytesIO):
        data = []

        def write(self, data: Buffer) -> int:
            self.data.append(data)
            return len(data)

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

    fp = Collector()

    _write_frame_data(fp, im, offset, params)

    return fp.data


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

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

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

# Image.register_save(GifImageFile.format, _save_netpbm)

```
--- END FILE ---

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

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

Here is an example:

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

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
[ISSUE]
**Title:** ValueError: Invalid palette size when saving multi-frame P-mode GIF with specific parameters

**Description:**
When attempting to save a multi-frame GIF image in mode "P" with the parameters `disposal=2` and `optimize=False`, a `ValueError` is raised indicating an invalid palette size. This issue occurs during the palette assignment for the background image, preventing the GIF from being saved successfully.

**Example Code:**
```python
from PIL import Image, ImageDraw, ImageSequence

# Create the first palette-based image
im1 = Image.new("P", (100, 100))
draw = ImageDraw.Draw(im1)
draw.ellipse([(40, 40), (60, 60)], fill=1)
palette_data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254
im1.putpalette(palette_data, "RGBA")

# Create a second palette-based image
im2 = Image.new("P", (100, 100))
im2.putpalette(palette_data, "RGBA")

# Attempt to save the GIF with specific parameters
out = "temp.gif"
im1.save(out, save_all=True, append_images=[im2], disposal=2, optimize=False)
```

**Expected Behavior:**
The GIF should be saved successfully without any errors, correctly applying the specified disposal method and optimization settings.

**Actual Behavior:**
A `ValueError` is raised with the message "invalid palette size," preventing the GIF from being saved.

**Error Message:**
```
ValueError: invalid palette size
```
[/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[256-511-256]\u001b[0m": "PASSED",
    "\u001b[1mtest_optimize_correctness[255-511-255]\u001b[0m": "PASSED",
    "\u001b[1mtest_optimize_correctness[129-511-129]\u001b[0m": "PASSED",
    "\u001b[1mtest_optimize_correctness[128-511-128]\u001b[0m": "PASSED",
    "\u001b[1mtest_optimize_correctness[64-511-64]\u001b[0m": "PASSED",
    "\u001b[1mtest_optimize_correctness[4-511-4]\u001b[0m": "PASSED",
    "\u001b[1mtest_optimize_correctness[128-513-256]\u001b[0m": "PASSED",
    "\u001b[1mtest_optimize_correctness[64-513-256]\u001b[0m": "PASSED",
    "\u001b[1mtest_optimize_correctness[4-513-256]\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_full_palette_second_frame\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_roundtrip_save_all_1\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_dispose2_previous_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_roundtrip_info_duration_combined\u001b[0m": "PASSED",
    "\u001b[1mtest_identical_frames\u001b[0m": "PASSED",
    "\u001b[1mtest_identical_frames_to_single_frame[duration0]\u001b[0m": "PASSED",
    "\u001b[1mtest_identical_frames_to_single_frame[duration1]\u001b[0m": "PASSED",
    "\u001b[1mtest_identical_frames_to_single_frame[duration2]\u001b[0m": "PASSED",
    "\u001b[1mtest_identical_frames_to_single_frame[1500]\u001b[0m": "PASSED",
    "\u001b[1mtest_loop_none\u001b[0m": "PASSED",
    "\u001b[1mtest_number_of_loops\u001b[0m": "PASSED",
    "\u001b[1mtest_background\u001b[0m": "PASSED",
    "\u001b[1mtest_webp_background\u001b[0m": "PASSED",
    "\u001b[1mtest_comment\u001b[0m": "PASSED",
    "\u001b[1mtest_comment_over_255\u001b[0m": "PASSED",
    "\u001b[1mtest_zero_comment_subblocks\u001b[0m": "PASSED",
    "\u001b[1mtest_read_multiple_comment_blocks\u001b[0m": "PASSED",
    "\u001b[1mtest_empty_string_comment\u001b[0m": "PASSED",
    "\u001b[1mtest_retain_comment_in_subsequent_frames\u001b[0m": "PASSED",
    "\u001b[1mtest_version\u001b[0m": "PASSED",
    "\u001b[1mtest_append_images\u001b[0m": "PASSED",
    "\u001b[1mtest_append_different_size_image\u001b[0m": "PASSED",
    "\u001b[1mtest_transparent_optimize\u001b[0m": "PASSED",
    "\u001b[1mtest_removed_transparency\u001b[0m": "PASSED",
    "\u001b[1mtest_rgb_transparency\u001b[0m": "PASSED",
    "\u001b[1mtest_rgba_transparency\u001b[0m": "PASSED",
    "\u001b[1mtest_background_outside_palettte\u001b[0m": "PASSED",
    "\u001b[1mtest_bbox\u001b[0m": "PASSED",
    "\u001b[1mtest_bbox_alpha\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[test_extents.gif-LoadingStrategy.RGB_AFTER_FIRST]\u001b[0m": "PASSED",
    "\u001b[1mtest_extents[test_extents.gif-LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY]\u001b[0m": "PASSED",
    "\u001b[1mtest_extents[test_extents_transparency.gif-LoadingStrategy.RGB_AFTER_FIRST]\u001b[0m": "PASSED",
    "\u001b[1mtest_missing_background\u001b[0m": "PASSED",
    "\u001b[1mtest_saving_rgba\u001b[0m": "PASSED",
    "\u001b[1mtest_p_rgba[params0]\u001b[0m": "PASSED",
    "\u001b[1mtest_p_rgba[params1]\u001b[0m": "PASSED"
}
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index a7c4f8b2c..47022d584 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -695,8 +695,9 @@ def _write_multiple_frames(
                         )
                         background = _get_background(im_frame, color)
                         background_im = Image.new("P", im_frame.size, background)
-                        assert im_frames[0].im.palette is not None
-                        background_im.putpalette(im_frames[0].im.palette)
+                        first_palette = im_frames[0].im.palette
+                        assert first_palette is not None
+                        background_im.putpalette(first_palette, first_palette.mode)
                     bbox = _getbbox(background_im, im_frame)[1]
                 elif encoderinfo.get("optimize") and im_frame.mode != "1":
                     if "transparency" not in 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#\nfrom __future__ import annotations\n\nimport itertools\nimport math\nimport os\nimport subprocess\nfrom enum import IntEnum\nfrom functools import cached_property\nfrom typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union\n\nfrom . import (\n    Image,\n    ImageChops,\n    ImageFile,\n    ImageMath,\n    ImageOps,\n    ImagePalette,\n    ImageSequence,\n)\nfrom ._binary import i16le as i16\nfrom ._binary import o8\nfrom ._binary import o16le as o16\n\nif TYPE_CHECKING:\n    from . import _imaging\n    from ._typing import Buffer\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: bytes) -> bool:\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    format = \"GIF\"\n    format_description = \"Compuserve GIF\"\n    _close_exclusive_fp_after_loading = False\n\n    global_palette = None\n\n    def data(self) -> bytes | None:\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: bytes) -> bool:\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) -> None:\n        # Screen\n        s = self.fp.read(13)\n        if not _accept(s):\n            msg = \"not a GIF file\"\n            raise SyntaxError(msg)\n\n        self.info[\"version\"] = s[:6]\n        self._size = i16(s, 6), i16(s, 8)\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: int | None = None\n        self._seek(0)  # get ready to read first frame\n\n    @property\n    def n_frames(self) -> int:\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    @cached_property\n    def is_animated(self) -> bool:\n        if self._n_frames is not None:\n            return self._n_frames != 1\n\n        current = self.tell()\n        if current:\n            return True\n\n        try:\n            self._seek(1, False)\n            is_animated = True\n        except EOFError:\n            is_animated = False\n\n        self.seek(current)\n        return is_animated\n\n    def seek(self, frame: int) -> None:\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                msg = \"no more images in GIF file\"\n                raise EOFError(msg) from e\n\n    def _seek(self, frame: int, update_image: bool = True) -> None:\n        if frame == 0:\n            # rewind\n            self.__offset = 0\n            self.dispose: _imaging.ImagingCore | None = 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            msg = f\"cannot seek to frame {frame}\"\n            raise ValueError(msg)\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            msg = \"no more images in GIF file\"\n            raise EOFError(msg)\n\n        palette: ImagePalette.ImagePalette | Literal[False] | None = None\n\n        info: dict[str, Any] = {}\n        frame_transparency = None\n        interlace = None\n        frame_dispose_extent = None\n        while True:\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 and block is not None:\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 and block is not None:\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 block and 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            s = None\n\n        if interlace is None:\n            msg = \"image not found in GIF frame\"\n            raise EOFError(msg)\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 palette:\n                self.palette = palette\n            elif self.global_palette:\n                from copy import copy\n\n                self.palette = copy(self.global_palette)\n            else:\n                self.palette = None\n        else:\n            if self.mode == \"P\":\n                if (\n                    LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY\n                    or palette\n                ):\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: int) -> tuple[int, int, int]:\n            if self._frame_palette:\n                if color * 3 + 3 > len(self._frame_palette.palette):\n                    color = 0\n                return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])\n            else:\n                return (color, color, color)\n\n        self.dispose = None\n        self.dispose_extent = frame_dispose_extent\n        if self.dispose_extent and self.disposal_method >= 2:\n            try:\n                if 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(\n                            dispose_mode, dispose_size, color\n                        )\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                ImageFile._Tile(\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) -> None:\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(\"RGB\", *self._frame_palette.getdata())\n            else:\n                self._im = None\n        if not self._prev_im and self._im is not None and self.size != self.im.size:\n            expanded_im = Image.core.fill(self.im.mode, self.size)\n            if self._frame_palette:\n                expanded_im.putpalette(\"RGB\", *self._frame_palette.getdata())\n            expanded_im.paste(self.im, (0, 0) + self.im.size)\n\n            self.im = expanded_im\n        self._mode = temp_mode\n        self._frame_palette = None\n\n        super().load_prepare()\n\n    def load_end(self) -> None:\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.size != self._prev_im.size:\n            if self._frame_transparency is not None:\n                expanded_im = Image.core.fill(\"RGBA\", self.size)\n            else:\n                expanded_im = Image.core.fill(\"P\", self.size)\n                expanded_im.putpalette(\"RGB\", \"RGB\", self.im.getpalette())\n                expanded_im = expanded_im.convert(\"RGB\")\n            expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size)\n\n            self._prev_im = expanded_im\n            assert self._prev_im is not None\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\n        assert self.dispose_extent is not None\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) -> int:\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: Image.Image) -> Image.Image:\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        assert im.palette is not None\n        if im.palette.mode == \"RGBA\":\n            for rgba in im.palette.colors:\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\n_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette]\n\n\ndef _normalize_palette(\n    im: Image.Image, palette: _Palette | None, info: dict[str, Any]\n) -> Image.Image:\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            im_palette = im.getpalette(None)\n            assert im_palette is not None\n            source_palette = bytearray(im_palette)\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    assert source_palette is not None\n\n    if palette:\n        used_palette_colors: list[int | None] = []\n        assert im.palette is not None\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        dest_map: list[int] = []\n        for index in used_palette_colors:\n            assert index is not None\n            dest_map.append(index)\n        im = im.remap_palette(dest_map)\n    else:\n        optimized_palette_colors = _get_optimize(im, info)\n        if optimized_palette_colors is not None:\n            im = im.remap_palette(optimized_palette_colors, source_palette)\n            if \"transparency\" in info:\n                try:\n                    info[\"transparency\"] = optimized_palette_colors.index(\n                        info[\"transparency\"]\n                    )\n                except ValueError:\n                    del info[\"transparency\"]\n            return im\n\n    assert im.palette is not None\n    im.palette.palette = source_palette\n    return im\n\n\ndef _write_single_frame(\n    im: Image.Image,\n    fp: IO[bytes],\n    palette: _Palette | None,\n) -> None:\n    im_out = _normalize_mode(im)\n    for k, v in im_out.info.items():\n        if isinstance(k, str):\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(\n        im_out, fp, [ImageFile._Tile(\"gif\", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]\n    )\n\n    fp.write(b\"\\0\")  # end of image data\n\n\ndef _getbbox(\n    base_im: Image.Image, im_frame: Image.Image\n) -> tuple[Image.Image, tuple[int, int, int, int] | None]:\n    palette_bytes = [\n        bytes(im.palette.palette) if im.palette else b\"\" for im in (base_im, im_frame)\n    ]\n    if palette_bytes[0] != palette_bytes[1]:\n        im_frame = im_frame.convert(\"RGBA\")\n        base_im = base_im.convert(\"RGBA\")\n    delta = ImageChops.subtract_modulo(im_frame, base_im)\n    return delta, delta.getbbox(alpha_only=False)\n\n\nclass _Frame(NamedTuple):\n    im: Image.Image\n    bbox: tuple[int, int, int, int] | None\n    encoderinfo: dict[str, Any]\n\n\ndef _write_multiple_frames(\n    im: Image.Image, fp: IO[bytes], palette: _Palette | None\n) -> bool:\n    duration = im.encoderinfo.get(\"duration\")\n    disposal = im.encoderinfo.get(\"disposal\", im.info.get(\"disposal\"))\n\n    im_frames: list[_Frame] = []\n    previous_im: Image.Image | None = None\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                    if isinstance(k, str):\n                        im.encoderinfo.setdefault(k, v)\n\n            encoderinfo = im.encoderinfo.copy()\n            if \"transparency\" in im_frame.info:\n                encoderinfo.setdefault(\"transparency\", im_frame.info[\"transparency\"])\n            im_frame = _normalize_palette(im_frame, palette, encoderinfo)\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            diff_frame = None\n            if im_frames and previous_im:\n                # delta frame\n                delta, bbox = _getbbox(previous_im, im_frame)\n                if not bbox:\n                    # This frame is identical to the previous frame\n                    if encoderinfo.get(\"duration\"):\n                        im_frames[-1].encoderinfo[\"duration\"] += encoderinfo[\"duration\"]\n                    continue\n                if im_frames[-1].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                        assert im_frames[0].im.palette is not None\n                        background_im.putpalette(im_frames[0].im.palette)\n                    bbox = _getbbox(background_im, im_frame)[1]\n                elif encoderinfo.get(\"optimize\") and im_frame.mode != \"1\":\n                    if \"transparency\" not in encoderinfo:\n                        assert im_frame.palette is not None\n                        try:\n                            encoderinfo[\"transparency\"] = (\n                                im_frame.palette._new_color_index(im_frame)\n                            )\n                        except ValueError:\n                            pass\n                    if \"transparency\" in encoderinfo:\n                        # When the delta is zero, fill the image with transparency\n                        diff_frame = im_frame.copy()\n                        fill = Image.new(\"P\", delta.size, encoderinfo[\"transparency\"])\n                        if delta.mode == \"RGBA\":\n                            r, g, b, a = delta.split()\n                            mask = ImageMath.lambda_eval(\n                                lambda args: args[\"convert\"](\n                                    args[\"max\"](\n                                        args[\"max\"](\n                                            args[\"max\"](args[\"r\"], args[\"g\"]), args[\"b\"]\n                                        ),\n                                        args[\"a\"],\n                                    )\n                                    * 255,\n                                    \"1\",\n                                ),\n                                r=r,\n                                g=g,\n                                b=b,\n                                a=a,\n                            )\n                        else:\n                            if delta.mode == \"P\":\n                                # Convert to L without considering palette\n                                delta_l = Image.new(\"L\", delta.size)\n                                delta_l.putdata(delta.getdata())\n                                delta = delta_l\n                            mask = ImageMath.lambda_eval(\n                                lambda args: args[\"convert\"](args[\"im\"] * 255, \"1\"),\n                                im=delta,\n                            )\n                        diff_frame.paste(fill, mask=ImageOps.invert(mask))\n            else:\n                bbox = None\n            previous_im = im_frame\n            im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))\n\n    if len(im_frames) == 1:\n        if \"duration\" in im.encoderinfo:\n            # Since multiple frames will not be written, use the combined duration\n            im.encoderinfo[\"duration\"] = im_frames[0].encoderinfo[\"duration\"]\n        return False\n\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\n\ndef _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:\n    _save(im, fp, filename, save_all=True)\n\n\ndef _save(\n    im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False\n) -> None:\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.setdefault(\"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: Image.Image) -> int:\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(\n    fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int\n) -> None:\n    try:\n        transparency = im.encoderinfo[\"transparency\"]\n    except KeyError:\n        transparency = None\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 transparency is not None or duration != 0 or disposal:\n        packed_flag = 1 if transparency is not None else 0\n        packed_flag |= disposal << 2\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 or 0)  # 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: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:\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                assert quant_proc.stdout is not None\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: Image.Image, info: dict[str, Any]) -> list[int] | None:\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\"):\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            assert im.palette is not None\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    return None\n\n\ndef _get_color_table_size(palette_bytes: bytes) -> int:\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: bytes) -> 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: Image.Image) -> bytes:\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    if not im.palette:\n        return b\"\"\n\n    palette = bytes(im.palette.palette)\n    if im.palette.mode == \"RGBA\":\n        palette = b\"\".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3))\n    return palette\n\n\ndef _get_background(\n    im: Image.Image,\n    info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None,\n) -> int:\n    background = 0\n    if info_background:\n        if isinstance(info_background, tuple):\n            # WebPImagePlugin stores an RGBA value in info[\"background\"]\n            # So it must be converted to the same format as GifImagePlugin's\n            # info[\"background\"] - a global color table index\n            assert im.palette is not None\n            try:\n                background = im.palette.getcolor(info_background, im)\n            except ValueError as e:\n                if str(e) not in (\n                    # If all 256 colors are in use,\n                    # then there is no need for the background color\n                    \"cannot allocate more than 256 colors\",\n                    # Ignore non-opaque WebP background\n                    \"cannot add non-opaque RGBA color to RGB palette\",\n                ):\n                    raise\n        else:\n            background = info_background\n    return background\n\n\ndef _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]:\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 info.get(\"loop\") is not None\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 info.get(\"loop\") is not None:\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(\n    fp: IO[bytes],\n    im_frame: Image.Image,\n    offset: tuple[int, int],\n    params: dict[str, Any],\n) -> None:\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,\n            fp,\n            [ImageFile._Tile(\"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(\n    im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None\n) -> tuple[list[bytes], list[int] | 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    if info is None:\n        info = {}\n\n    used_palette_colors = _get_optimize(im, 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(\n    im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any\n) -> list[bytes]:\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    from io import BytesIO\n\n    class Collector(BytesIO):\n        data = []\n\n        def write(self, data: Buffer) -> int:\n            self.data.append(data)\n            return len(data)\n\n    im.load()  # make sure raster data is available\n\n    fp = Collector()\n\n    _write_frame_data(fp, im, offset, params)\n\n    return fp.data\n\n\n# --------------------------------------------------------------------\n# Registry\n\nImage.register_open(GifImageFile.format, GifImageFile, _accept)\nImage.register_save(GifImageFile.format, _save)\nImage.register_save_all(GifImageFile.format, _save_all)\nImage.register_extension(GifImageFile.format, \".gif\")\nImage.register_mime(GifImageFile.format, \"image/gif\")\n\n#\n# Uncomment the following line if you wish to use NETPBM/PBMPLUS\n# instead of the built-in \"uncompressed\" GIF encoder\n\n# Image.register_save(GifImageFile.format, _save_netpbm)\n"}
A user will ask you to solve a task. You should first draft your thinking process (inner monologue). Then, generate the solution.

Your response format must follow the template below:
<think>
Your thoughts or/and draft, like working through an exercise on scratch paper. Be as casual and as long as you want until you are confident to generate a correct solution.
</think>
<solution>
Final solution presented to the user.
</solution>
system
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** ValueError: Invalid palette size when saving multi-frame P-mode GIF with specific parameters

**Description:**
When attempting to save a multi-frame GIF image in mode "P" with the parameters `disposal=2` and `optimize=False`, a `ValueError` is raised indicating an invalid palette size. This issue occurs during the palette assignment for the background image, preventing the GIF from being saved successfully.

**Example Code:**
```python
from PIL import Image, ImageDraw, ImageSequence

# Create the first palette-based image
im1 = Image.new("P", (100, 100))
draw = ImageDraw.Draw(im1)
draw.ellipse([(40, 40), (60, 60)], fill=1)
palette_data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254
im1.putpalette(palette_data, "RGBA")

# Create a second palette-based image
im2 = Image.new("P", (100, 100))
im2.putpalette(palette_data, "RGBA")

# Attempt to save the GIF with specific parameters
out = "temp.gif"
im1.save(out, save_all=True, append_images=[im2], disposal=2, optimize=False)
```

**Expected Behavior:**
The GIF should be saved successfully without any errors, correctly applying the specified disposal method and optimization settings.

**Actual Behavior:**
A `ValueError` is raised with the message "invalid palette size," preventing the GIF from being saved.

**Error Message:**
```
ValueError: invalid palette size
```
[/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.
#
from __future__ import annotations

import itertools
import math
import os
import subprocess
from enum import IntEnum
from functools import cached_property
from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union

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

if TYPE_CHECKING:
    from . import _imaging
    from ._typing import Buffer


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: bytes) -> bool:
    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) -> bytes | None:
        s = self.fp.read(1)
        if s and s[0]:
            return self.fp.read(s[0])
        return None

    def _is_palette_needed(self, p: bytes) -> bool:
        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) -> None:
        # Screen
        s = self.fp.read(13)
        if not _accept(s):
            msg = "not a GIF file"
            raise SyntaxError(msg)

        self.info["version"] = s[:6]
        self._size = i16(s, 6), i16(s, 8)
        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: int | None = None
        self._seek(0)  # get ready to read first frame

    @property
    def n_frames(self) -> int:
        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

    @cached_property
    def is_animated(self) -> bool:
        if self._n_frames is not None:
            return self._n_frames != 1

        current = self.tell()
        if current:
            return True

        try:
            self._seek(1, False)
            is_animated = True
        except EOFError:
            is_animated = False

        self.seek(current)
        return is_animated

    def seek(self, frame: int) -> None:
        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)
                msg = "no more images in GIF file"
                raise EOFError(msg) from e

    def _seek(self, frame: int, update_image: bool = True) -> None:
        if frame == 0:
            # rewind
            self.__offset = 0
            self.dispose: _imaging.ImagingCore | None = 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:
            msg = f"cannot seek to frame {frame}"
            raise ValueError(msg)

        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";":
            msg = "no more images in GIF file"
            raise EOFError(msg)

        palette: ImagePalette.ImagePalette | Literal[False] | None = None

        info: dict[str, Any] = {}
        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 and block is not None:
                    #
                    # 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 and block is not None:
                    #
                    # application extension
                    #
                    info["extension"] = block, self.fp.tell()
                    if block[:11] == b"NETSCAPE2.0":
                        block = self.data()
                        if block and 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
            s = None

        if interlace is None:
            msg = "image not found in GIF frame"
            raise EOFError(msg)

        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 palette:
                self.palette = palette
            elif self.global_palette:
                from copy import copy

                self.palette = copy(self.global_palette)
            else:
                self.palette = None
        else:
            if self.mode == "P":
                if (
                    LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
                    or palette
                ):
                    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: int) -> tuple[int, int, int]:
            if self._frame_palette:
                if color * 3 + 3 > len(self._frame_palette.palette):
                    color = 0
                return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
            else:
                return (color, color, color)

        self.dispose = None
        self.dispose_extent = frame_dispose_extent
        if self.dispose_extent and self.disposal_method >= 2:
            try:
                if 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 = [
                ImageFile._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) -> None:
        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("RGB", *self._frame_palette.getdata())
            else:
                self._im = None
        if not self._prev_im and self._im is not None and self.size != self.im.size:
            expanded_im = Image.core.fill(self.im.mode, self.size)
            if self._frame_palette:
                expanded_im.putpalette("RGB", *self._frame_palette.getdata())
            expanded_im.paste(self.im, (0, 0) + self.im.size)

            self.im = expanded_im
        self._mode = temp_mode
        self._frame_palette = None

        super().load_prepare()

    def load_end(self) -> None:
        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.size != self._prev_im.size:
            if self._frame_transparency is not None:
                expanded_im = Image.core.fill("RGBA", self.size)
            else:
                expanded_im = Image.core.fill("P", self.size)
                expanded_im.putpalette("RGB", "RGB", self.im.getpalette())
                expanded_im = expanded_im.convert("RGB")
            expanded_im.paste(self._prev_im, (0, 0) + self._prev_im.size)

            self._prev_im = expanded_im
            assert self._prev_im is not None
        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")

        assert self.dispose_extent is not None
        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) -> int:
        return self.__frame


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


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


def _normalize_mode(im: Image.Image) -> Image.Image:
    """
    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)
        assert im.palette is not None
        if im.palette.mode == "RGBA":
            for rgba in im.palette.colors:
                if rgba[3] == 0:
                    im.info["transparency"] = im.palette.colors[rgba]
                    break
        return im
    return im.convert("L")


_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette]


def _normalize_palette(
    im: Image.Image, palette: _Palette | None, info: dict[str, Any]
) -> Image.Image:
    """
    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:
            im_palette = im.getpalette(None)
            assert im_palette is not None
            source_palette = bytearray(im_palette)
    else:  # L-mode
        if not source_palette:
            source_palette = bytearray(i // 3 for i in range(768))
        im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
    assert source_palette is not None

    if palette:
        used_palette_colors: list[int | None] = []
        assert im.palette is not None
        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
        dest_map: list[int] = []
        for index in used_palette_colors:
            assert index is not None
            dest_map.append(index)
        im = im.remap_palette(dest_map)
    else:
        optimized_palette_colors = _get_optimize(im, info)
        if optimized_palette_colors is not None:
            im = im.remap_palette(optimized_palette_colors, source_palette)
            if "transparency" in info:
                try:
                    info["transparency"] = optimized_palette_colors.index(
                        info["transparency"]
                    )
                except ValueError:
                    del info["transparency"]
            return im

    assert im.palette is not None
    im.palette.palette = source_palette
    return im


def _write_single_frame(
    im: Image.Image,
    fp: IO[bytes],
    palette: _Palette | None,
) -> None:
    im_out = _normalize_mode(im)
    for k, v in im_out.info.items():
        if isinstance(k, str):
            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, [ImageFile._Tile("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]
    )

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


def _getbbox(
    base_im: Image.Image, im_frame: Image.Image
) -> tuple[Image.Image, tuple[int, int, int, int] | None]:
    palette_bytes = [
        bytes(im.palette.palette) if im.palette else b"" for im in (base_im, im_frame)
    ]
    if palette_bytes[0] != palette_bytes[1]:
        im_frame = im_frame.convert("RGBA")
        base_im = base_im.convert("RGBA")
    delta = ImageChops.subtract_modulo(im_frame, base_im)
    return delta, delta.getbbox(alpha_only=False)


class _Frame(NamedTuple):
    im: Image.Image
    bbox: tuple[int, int, int, int] | None
    encoderinfo: dict[str, Any]


def _write_multiple_frames(
    im: Image.Image, fp: IO[bytes], palette: _Palette | None
) -> bool:
    duration = im.encoderinfo.get("duration")
    disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))

    im_frames: list[_Frame] = []
    previous_im: Image.Image | None = None
    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
                    if isinstance(k, str):
                        im.encoderinfo.setdefault(k, v)

            encoderinfo = im.encoderinfo.copy()
            if "transparency" in im_frame.info:
                encoderinfo.setdefault("transparency", im_frame.info["transparency"])
            im_frame = _normalize_palette(im_frame, palette, encoderinfo)
            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

            diff_frame = None
            if im_frames and previous_im:
                # delta frame
                delta, bbox = _getbbox(previous_im, im_frame)
                if not bbox:
                    # This frame is identical to the previous frame
                    if encoderinfo.get("duration"):
                        im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"]
                    continue
                if im_frames[-1].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)
                        assert im_frames[0].im.palette is not None
                        background_im.putpalette(im_frames[0].im.palette)
                    bbox = _getbbox(background_im, im_frame)[1]
                elif encoderinfo.get("optimize") and im_frame.mode != "1":
                    if "transparency" not in encoderinfo:
                        assert im_frame.palette is not None
                        try:
                            encoderinfo["transparency"] = (
                                im_frame.palette._new_color_index(im_frame)
                            )
                        except ValueError:
                            pass
                    if "transparency" in encoderinfo:
                        # When the delta is zero, fill the image with transparency
                        diff_frame = im_frame.copy()
                        fill = Image.new("P", delta.size, encoderinfo["transparency"])
                        if delta.mode == "RGBA":
                            r, g, b, a = delta.split()
                            mask = ImageMath.lambda_eval(
                                lambda args: args["convert"](
                                    args["max"](
                                        args["max"](
                                            args["max"](args["r"], args["g"]), args["b"]
                                        ),
                                        args["a"],
                                    )
                                    * 255,
                                    "1",
                                ),
                                r=r,
                                g=g,
                                b=b,
                                a=a,
                            )
                        else:
                            if delta.mode == "P":
                                # Convert to L without considering palette
                                delta_l = Image.new("L", delta.size)
                                delta_l.putdata(delta.getdata())
                                delta = delta_l
                            mask = ImageMath.lambda_eval(
                                lambda args: args["convert"](args["im"] * 255, "1"),
                                im=delta,
                            )
                        diff_frame.paste(fill, mask=ImageOps.invert(mask))
            else:
                bbox = None
            previous_im = im_frame
            im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))

    if len(im_frames) == 1:
        if "duration" in im.encoderinfo:
            # Since multiple frames will not be written, use the combined duration
            im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"]
        return False

    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


def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
    _save(im, fp, filename, save_all=True)


def _save(
    im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
) -> None:
    # 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.setdefault("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: Image.Image) -> int:
    interlace = im.encoderinfo.get("interlace", 1)

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

    return interlace


def _write_local_header(
    fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int
) -> None:
    try:
        transparency = im.encoderinfo["transparency"]
    except KeyError:
        transparency = None

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

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

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

        fp.write(
            b"!"
            + o8(249)  # extension intro
            + o8(4)  # length
            + o8(packed_flag)  # packed fields
            + o16(duration)  # duration
            + o8(transparency or 0)  # 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: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
    # 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
                assert quant_proc.stdout is not None
                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: Image.Image, info: dict[str, Any]) -> list[int] | None:
    """
    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"):
        # 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

            assert im.palette is not None
            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
    return None


def _get_color_table_size(palette_bytes: bytes) -> int:
    # 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: bytes) -> 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: Image.Image) -> bytes:
    """
    Gets the palette for inclusion in the gif header

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

    palette = bytes(im.palette.palette)
    if im.palette.mode == "RGBA":
        palette = b"".join(palette[i * 4 : i * 4 + 3] for i in range(len(palette) // 3))
    return palette


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


def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]:
    """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 info.get("loop") is not None
            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 info.get("loop") is not None:
        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: IO[bytes],
    im_frame: Image.Image,
    offset: tuple[int, int],
    params: dict[str, Any],
) -> None:
    try:
        im_frame.encoderinfo = params

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

        ImageFile._save(
            im_frame,
            fp,
            [ImageFile._Tile("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: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None
) -> tuple[list[bytes], list[int] | 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)

    """
    if info is None:
        info = {}

    used_palette_colors = _get_optimize(im, 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: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any
) -> list[bytes]:
    """
    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

    """
    from io import BytesIO

    class Collector(BytesIO):
        data = []

        def write(self, data: Buffer) -> int:
            self.data.append(data)
            return len(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