{
    "file_diffs": [
        {
            "old_file_content": "import pytest\n\nfrom PIL import Image, ImageSequence, PngImagePlugin\n\n\n# APNG browser support tests and fixtures via:\n# https://philip.html5.org/tests/apng/tests.html\n# (referenced from https://wiki.mozilla.org/APNG_Specification)\ndef test_apng_basic():\n    with Image.open(\"Tests/images/apng/single_frame.png\") as im:\n        assert not im.is_animated\n        assert im.n_frames == 1\n        assert im.get_format_mimetype() == \"image/apng\"\n        assert im.info.get(\"default_image\") is None\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/single_frame_default.png\") as im:\n        assert im.is_animated\n        assert im.n_frames == 2\n        assert im.get_format_mimetype() == \"image/apng\"\n        assert im.info.get(\"default_image\")\n        assert im.getpixel((0, 0)) == (255, 0, 0, 255)\n        assert im.getpixel((64, 32)) == (255, 0, 0, 255)\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n        # test out of bounds seek\n        with pytest.raises(EOFError):\n            im.seek(2)\n\n        # test rewind support\n        im.seek(0)\n        assert im.getpixel((0, 0)) == (255, 0, 0, 255)\n        assert im.getpixel((64, 32)) == (255, 0, 0, 255)\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\n@pytest.mark.parametrize(\n    \"filename\",\n    (\"Tests/images/apng/split_fdat.png\", \"Tests/images/apng/split_fdat_zero_chunk.png\"),\n)\ndef test_apng_fdat(filename):\n    with Image.open(filename) as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_dispose():\n    with Image.open(\"Tests/images/apng/dispose_op_none.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_background.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/dispose_op_background_final.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_previous.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_previous_final.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_previous_first.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n\ndef test_apng_dispose_region():\n    with Image.open(\"Tests/images/apng/dispose_op_none_region.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_background_before_region.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/dispose_op_background_region.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 255, 255)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/dispose_op_previous_region.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_dispose_op_previous_frame():\n    # Test that the dispose settings being used are from the previous frame\n    #\n    # Image created with:\n    # red = Image.new(\"RGBA\", (128, 64), (255, 0, 0, 255))\n    # green = red.copy()\n    # green.paste(Image.new(\"RGBA\", (64, 32), (0, 255, 0, 255)))\n    # blue = red.copy()\n    # blue.paste(Image.new(\"RGBA\", (64, 32), (0, 255, 0, 255)), (64, 32))\n    #\n    # red.save(\n    #     \"Tests/images/apng/dispose_op_previous_frame.png\",\n    #     save_all=True,\n    #     append_images=[green, blue],\n    #     disposal=[\n    #         PngImagePlugin.Disposal.OP_NONE,\n    #         PngImagePlugin.Disposal.OP_PREVIOUS,\n    #         PngImagePlugin.Disposal.OP_PREVIOUS\n    #     ],\n    # )\n    with Image.open(\"Tests/images/apng/dispose_op_previous_frame.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (255, 0, 0, 255)\n\n\ndef test_apng_dispose_op_background_p_mode():\n    with Image.open(\"Tests/images/apng/dispose_op_background_p_mode.png\") as im:\n        im.seek(1)\n        im.load()\n        assert im.size == (128, 64)\n\n\ndef test_apng_blend():\n    with Image.open(\"Tests/images/apng/blend_op_source_solid.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/blend_op_source_transparent.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/blend_op_source_near_transparent.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 2)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 2)\n\n    with Image.open(\"Tests/images/apng/blend_op_over.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/blend_op_over_near_transparent.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 97)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_blend_transparency():\n    with Image.open(\"Tests/images/blend_transparency.png\") as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (255, 0, 0)\n\n\ndef test_apng_chunk_order():\n    with Image.open(\"Tests/images/apng/fctl_actl.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_delay():\n    with Image.open(\"Tests/images/apng/delay.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n        im.seek(3)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(4)\n        assert im.info.get(\"duration\") == 1000.0\n\n    with Image.open(\"Tests/images/apng/delay_round.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n\n    with Image.open(\"Tests/images/apng/delay_short_max.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n\n    with Image.open(\"Tests/images/apng/delay_zero_denom.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n\n    with Image.open(\"Tests/images/apng/delay_zero_numer.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 0.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 0.0\n        im.seek(3)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(4)\n        assert im.info.get(\"duration\") == 1000.0\n\n\ndef test_apng_num_plays():\n    with Image.open(\"Tests/images/apng/num_plays.png\") as im:\n        assert im.info.get(\"loop\") == 0\n\n    with Image.open(\"Tests/images/apng/num_plays_1.png\") as im:\n        assert im.info.get(\"loop\") == 1\n\n\ndef test_apng_mode():\n    with Image.open(\"Tests/images/apng/mode_16bit.png\") as im:\n        assert im.mode == \"RGBA\"\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 128, 191)\n        assert im.getpixel((64, 32)) == (0, 0, 128, 191)\n\n    with Image.open(\"Tests/images/apng/mode_grayscale.png\") as im:\n        assert im.mode == \"L\"\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == 128\n        assert im.getpixel((64, 32)) == 255\n\n    with Image.open(\"Tests/images/apng/mode_grayscale_alpha.png\") as im:\n        assert im.mode == \"LA\"\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (128, 191)\n        assert im.getpixel((64, 32)) == (128, 191)\n\n    with Image.open(\"Tests/images/apng/mode_palette.png\") as im:\n        assert im.mode == \"P\"\n        im.seek(im.n_frames - 1)\n        im = im.convert(\"RGB\")\n        assert im.getpixel((0, 0)) == (0, 255, 0)\n        assert im.getpixel((64, 32)) == (0, 255, 0)\n\n    with Image.open(\"Tests/images/apng/mode_palette_alpha.png\") as im:\n        assert im.mode == \"P\"\n        im.seek(im.n_frames - 1)\n        im = im.convert(\"RGBA\")\n        assert im.getpixel((0, 0)) == (255, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (255, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/mode_palette_1bit_alpha.png\") as im:\n        assert im.mode == \"P\"\n        im.seek(im.n_frames - 1)\n        im = im.convert(\"RGBA\")\n        assert im.getpixel((0, 0)) == (0, 0, 255, 128)\n        assert im.getpixel((64, 32)) == (0, 0, 255, 128)\n\n\ndef test_apng_chunk_errors():\n    with Image.open(\"Tests/images/apng/chunk_no_actl.png\") as im:\n        assert not im.is_animated\n\n    with pytest.warns(UserWarning):\n        with Image.open(\"Tests/images/apng/chunk_multi_actl.png\") as im:\n            im.load()\n        assert not im.is_animated\n\n    with Image.open(\"Tests/images/apng/chunk_actl_after_idat.png\") as im:\n        assert not im.is_animated\n\n    with Image.open(\"Tests/images/apng/chunk_no_fctl.png\") as im:\n        with pytest.raises(SyntaxError):\n            im.seek(im.n_frames - 1)\n\n    with Image.open(\"Tests/images/apng/chunk_repeat_fctl.png\") as im:\n        with pytest.raises(SyntaxError):\n            im.seek(im.n_frames - 1)\n\n    with Image.open(\"Tests/images/apng/chunk_no_fdat.png\") as im:\n        with pytest.raises(SyntaxError):\n            im.seek(im.n_frames - 1)\n\n\ndef test_apng_syntax_errors():\n    with pytest.warns(UserWarning):\n        with Image.open(\"Tests/images/apng/syntax_num_frames_zero.png\") as im:\n            assert not im.is_animated\n            with pytest.raises(OSError):\n                im.load()\n\n    with pytest.warns(UserWarning):\n        with Image.open(\"Tests/images/apng/syntax_num_frames_zero_default.png\") as im:\n            assert not im.is_animated\n            im.load()\n\n    # we can handle this case gracefully\n    exception = None\n    with Image.open(\"Tests/images/apng/syntax_num_frames_low.png\") as im:\n        try:\n            im.seek(im.n_frames - 1)\n        except Exception as e:\n            exception = e\n        assert exception is None\n\n    with pytest.raises(OSError):\n        with Image.open(\"Tests/images/apng/syntax_num_frames_high.png\") as im:\n            im.seek(im.n_frames - 1)\n            im.load()\n\n    with pytest.warns(UserWarning):\n        with Image.open(\"Tests/images/apng/syntax_num_frames_invalid.png\") as im:\n            assert not im.is_animated\n            im.load()\n\n\n@pytest.mark.parametrize(\n    \"test_file\",\n    (\n        \"sequence_start.png\",\n        \"sequence_gap.png\",\n        \"sequence_repeat.png\",\n        \"sequence_repeat_chunk.png\",\n        \"sequence_reorder.png\",\n        \"sequence_reorder_chunk.png\",\n        \"sequence_fdat_fctl.png\",\n    ),\n)\ndef test_apng_sequence_errors(test_file):\n    with pytest.raises(SyntaxError):\n        with Image.open(f\"Tests/images/apng/{test_file}\") as im:\n            im.seek(im.n_frames - 1)\n            im.load()\n\n\ndef test_apng_save(tmp_path):\n    with Image.open(\"Tests/images/apng/single_frame.png\") as im:\n        test_file = str(tmp_path / \"temp.png\")\n        im.save(test_file, save_all=True)\n\n    with Image.open(test_file) as im:\n        im.load()\n        assert not im.is_animated\n        assert im.n_frames == 1\n        assert im.get_format_mimetype() == \"image/apng\"\n        assert im.info.get(\"default_image\") is None\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/single_frame_default.png\") as im:\n        frames = []\n        for frame_im in ImageSequence.Iterator(im):\n            frames.append(frame_im.copy())\n        frames[0].save(\n            test_file, save_all=True, default_image=True, append_images=frames[1:]\n        )\n\n    with Image.open(test_file) as im:\n        im.load()\n        assert im.is_animated\n        assert im.n_frames == 2\n        assert im.get_format_mimetype() == \"image/apng\"\n        assert im.info.get(\"default_image\")\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_save_alpha(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n\n    im = Image.new(\"RGBA\", (1, 1), (255, 0, 0, 255))\n    im2 = Image.new(\"RGBA\", (1, 1), (255, 0, 0, 127))\n    im.save(test_file, save_all=True, append_images=[im2])\n\n    with Image.open(test_file) as reloaded:\n        assert reloaded.getpixel((0, 0)) == (255, 0, 0, 255)\n\n        reloaded.seek(1)\n        assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127)\n\n\ndef test_apng_save_split_fdat(tmp_path):\n    # test to make sure we do not generate sequence errors when writing\n    # frames with image data spanning multiple fdAT chunks (in this case\n    # both the default image and first animation frame will span multiple\n    # data chunks)\n    test_file = str(tmp_path / \"temp.png\")\n    with Image.open(\"Tests/images/old-style-jpeg-compression.png\") as im:\n        frames = [im.copy(), Image.new(\"RGBA\", im.size, (255, 0, 0, 255))]\n        im.save(\n            test_file,\n            save_all=True,\n            default_image=True,\n            append_images=frames,\n        )\n    with Image.open(test_file) as im:\n        exception = None\n        try:\n            im.seek(im.n_frames - 1)\n            im.load()\n        except Exception as e:\n            exception = e\n        assert exception is None\n\n\ndef test_apng_save_duration_loop(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    with Image.open(\"Tests/images/apng/delay.png\") as im:\n        frames = []\n        durations = []\n        loop = im.info.get(\"loop\")\n        default_image = im.info.get(\"default_image\")\n        for i, frame_im in enumerate(ImageSequence.Iterator(im)):\n            frames.append(frame_im.copy())\n            if i != 0 or not default_image:\n                durations.append(frame_im.info.get(\"duration\", 0))\n        frames[0].save(\n            test_file,\n            save_all=True,\n            default_image=default_image,\n            append_images=frames[1:],\n            duration=durations,\n            loop=loop,\n        )\n\n    with Image.open(test_file) as im:\n        im.load()\n        assert im.info.get(\"loop\") == loop\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n        im.seek(3)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(4)\n        assert im.info.get(\"duration\") == 1000.0\n\n    # test removal of duplicated frames\n    frame = Image.new(\"RGBA\", (128, 64), (255, 0, 0, 255))\n    frame.save(\n        test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]\n    )\n    with Image.open(test_file) as im:\n        im.load()\n        assert im.n_frames == 1\n        assert im.info.get(\"duration\") == 750\n\n    # test info duration\n    frame.info[\"duration\"] = 750\n    frame.save(test_file, save_all=True)\n    with Image.open(test_file) as im:\n        assert im.info.get(\"duration\") == 750\n\n\ndef test_apng_save_duplicate_duration(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    frame = Image.new(\"RGB\", (1, 1))\n\n    # Test a single duration is correctly combined across duplicate frames\n    frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500)\n    with Image.open(test_file) as im:\n        assert im.n_frames == 1\n        assert im.info.get(\"duration\") == 1500\n\n\ndef test_apng_save_disposal(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    size = (128, 64)\n    red = Image.new(\"RGBA\", size, (255, 0, 0, 255))\n    green = Image.new(\"RGBA\", size, (0, 255, 0, 255))\n    transparent = Image.new(\"RGBA\", size, (0, 0, 0, 0))\n\n    # test OP_NONE\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green, transparent],\n        disposal=PngImagePlugin.Disposal.OP_NONE,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test OP_BACKGROUND\n    disposal = [\n        PngImagePlugin.Disposal.OP_NONE,\n        PngImagePlugin.Disposal.OP_BACKGROUND,\n        PngImagePlugin.Disposal.OP_NONE,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[red, transparent],\n        disposal=disposal,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    disposal = [\n        PngImagePlugin.Disposal.OP_NONE,\n        PngImagePlugin.Disposal.OP_BACKGROUND,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green],\n        disposal=disposal,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test OP_PREVIOUS\n    disposal = [\n        PngImagePlugin.Disposal.OP_NONE,\n        PngImagePlugin.Disposal.OP_PREVIOUS,\n        PngImagePlugin.Disposal.OP_NONE,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green, red, transparent],\n        default_image=True,\n        disposal=disposal,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(3)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    disposal = [\n        PngImagePlugin.Disposal.OP_NONE,\n        PngImagePlugin.Disposal.OP_PREVIOUS,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green],\n        disposal=disposal,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test info disposal\n    red.info[\"disposal\"] = PngImagePlugin.Disposal.OP_BACKGROUND\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[Image.new(\"RGBA\", (10, 10), (0, 255, 0, 255))],\n    )\n    with Image.open(test_file) as im:\n        im.seek(1)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n\ndef test_apng_save_disposal_previous(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    size = (128, 64)\n    blue = Image.new(\"RGBA\", size, (0, 0, 255, 255))\n    red = Image.new(\"RGBA\", size, (255, 0, 0, 255))\n    green = Image.new(\"RGBA\", size, (0, 255, 0, 255))\n\n    # test OP_NONE\n    blue.save(\n        test_file,\n        save_all=True,\n        append_images=[red, green],\n        disposal=PngImagePlugin.Disposal.OP_PREVIOUS,\n    )\n    with Image.open(test_file) as im:\n        assert im.getpixel((0, 0)) == (0, 0, 255, 255)\n\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_save_blend(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    size = (128, 64)\n    red = Image.new(\"RGBA\", size, (255, 0, 0, 255))\n    green = Image.new(\"RGBA\", size, (0, 255, 0, 255))\n    transparent = Image.new(\"RGBA\", size, (0, 0, 0, 0))\n\n    # test OP_SOURCE on solid color\n    blend = [\n        PngImagePlugin.Blend.OP_OVER,\n        PngImagePlugin.Blend.OP_SOURCE,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[red, green],\n        default_image=True,\n        disposal=PngImagePlugin.Disposal.OP_NONE,\n        blend=blend,\n    )\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test OP_SOURCE on transparent color\n    blend = [\n        PngImagePlugin.Blend.OP_OVER,\n        PngImagePlugin.Blend.OP_SOURCE,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[red, transparent],\n        default_image=True,\n        disposal=PngImagePlugin.Disposal.OP_NONE,\n        blend=blend,\n    )\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    # test OP_OVER\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green, transparent],\n        default_image=True,\n        disposal=PngImagePlugin.Disposal.OP_NONE,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test info blend\n    red.info[\"blend\"] = PngImagePlugin.Blend.OP_OVER\n    red.save(test_file, save_all=True, append_images=[green, transparent])\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n\n\ndef test_seek_after_close():\n    im = Image.open(\"Tests/images/apng/delay.png\")\n    im.seek(1)\n    im.close()\n\n    with pytest.raises(ValueError):\n        im.seek(0)\n\n\n@pytest.mark.parametrize(\"mode\", (\"RGBA\", \"RGB\", \"P\"))\n@pytest.mark.parametrize(\"default_image\", (True, False))\ndef test_different_modes_in_later_frames(mode, default_image, tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n\n    im = Image.new(\"L\", (1, 1))\n    im.save(\n        test_file,\n        save_all=True,\n        default_image=default_image,\n        append_images=[Image.new(mode, (1, 1))],\n    )\n    with Image.open(test_file) as reloaded:\n        assert reloaded.mode == mode\n",
            "new_file_content": "import pytest\n\nfrom PIL import Image, ImageSequence, PngImagePlugin\n\n\n# APNG browser support tests and fixtures via:\n# https://philip.html5.org/tests/apng/tests.html\n# (referenced from https://wiki.mozilla.org/APNG_Specification)\ndef test_apng_basic():\n    with Image.open(\"Tests/images/apng/single_frame.png\") as im:\n        assert not im.is_animated\n        assert im.n_frames == 1\n        assert im.get_format_mimetype() == \"image/apng\"\n        assert im.info.get(\"default_image\") is None\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/single_frame_default.png\") as im:\n        assert im.is_animated\n        assert im.n_frames == 2\n        assert im.get_format_mimetype() == \"image/apng\"\n        assert im.info.get(\"default_image\")\n        assert im.getpixel((0, 0)) == (255, 0, 0, 255)\n        assert im.getpixel((64, 32)) == (255, 0, 0, 255)\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n        # test out of bounds seek\n        with pytest.raises(EOFError):\n            im.seek(2)\n\n        # test rewind support\n        im.seek(0)\n        assert im.getpixel((0, 0)) == (255, 0, 0, 255)\n        assert im.getpixel((64, 32)) == (255, 0, 0, 255)\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\n@pytest.mark.parametrize(\n    \"filename\",\n    (\"Tests/images/apng/split_fdat.png\", \"Tests/images/apng/split_fdat_zero_chunk.png\"),\n)\ndef test_apng_fdat(filename):\n    with Image.open(filename) as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_dispose():\n    with Image.open(\"Tests/images/apng/dispose_op_none.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_background.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/dispose_op_background_final.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_previous.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_previous_final.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_previous_first.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n\ndef test_apng_dispose_region():\n    with Image.open(\"Tests/images/apng/dispose_op_none_region.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_background_before_region.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/dispose_op_background_region.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 255, 255)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/dispose_op_previous_region.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_dispose_op_previous_frame():\n    # Test that the dispose settings being used are from the previous frame\n    #\n    # Image created with:\n    # red = Image.new(\"RGBA\", (128, 64), (255, 0, 0, 255))\n    # green = red.copy()\n    # green.paste(Image.new(\"RGBA\", (64, 32), (0, 255, 0, 255)))\n    # blue = red.copy()\n    # blue.paste(Image.new(\"RGBA\", (64, 32), (0, 255, 0, 255)), (64, 32))\n    #\n    # red.save(\n    #     \"Tests/images/apng/dispose_op_previous_frame.png\",\n    #     save_all=True,\n    #     append_images=[green, blue],\n    #     disposal=[\n    #         PngImagePlugin.Disposal.OP_NONE,\n    #         PngImagePlugin.Disposal.OP_PREVIOUS,\n    #         PngImagePlugin.Disposal.OP_PREVIOUS\n    #     ],\n    # )\n    with Image.open(\"Tests/images/apng/dispose_op_previous_frame.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (255, 0, 0, 255)\n\n\ndef test_apng_dispose_op_background_p_mode():\n    with Image.open(\"Tests/images/apng/dispose_op_background_p_mode.png\") as im:\n        im.seek(1)\n        im.load()\n        assert im.size == (128, 64)\n\n\ndef test_apng_blend():\n    with Image.open(\"Tests/images/apng/blend_op_source_solid.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/blend_op_source_transparent.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/blend_op_source_near_transparent.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 2)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 2)\n\n    with Image.open(\"Tests/images/apng/blend_op_over.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/blend_op_over_near_transparent.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 97)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_blend_transparency():\n    with Image.open(\"Tests/images/blend_transparency.png\") as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (255, 0, 0)\n\n\ndef test_apng_chunk_order():\n    with Image.open(\"Tests/images/apng/fctl_actl.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_delay():\n    with Image.open(\"Tests/images/apng/delay.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n        im.seek(3)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(4)\n        assert im.info.get(\"duration\") == 1000.0\n\n    with Image.open(\"Tests/images/apng/delay_round.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n\n    with Image.open(\"Tests/images/apng/delay_short_max.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n\n    with Image.open(\"Tests/images/apng/delay_zero_denom.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n\n    with Image.open(\"Tests/images/apng/delay_zero_numer.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 0.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 0.0\n        im.seek(3)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(4)\n        assert im.info.get(\"duration\") == 1000.0\n\n\ndef test_apng_num_plays():\n    with Image.open(\"Tests/images/apng/num_plays.png\") as im:\n        assert im.info.get(\"loop\") == 0\n\n    with Image.open(\"Tests/images/apng/num_plays_1.png\") as im:\n        assert im.info.get(\"loop\") == 1\n\n\ndef test_apng_mode():\n    with Image.open(\"Tests/images/apng/mode_16bit.png\") as im:\n        assert im.mode == \"RGBA\"\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 128, 191)\n        assert im.getpixel((64, 32)) == (0, 0, 128, 191)\n\n    with Image.open(\"Tests/images/apng/mode_grayscale.png\") as im:\n        assert im.mode == \"L\"\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == 128\n        assert im.getpixel((64, 32)) == 255\n\n    with Image.open(\"Tests/images/apng/mode_grayscale_alpha.png\") as im:\n        assert im.mode == \"LA\"\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (128, 191)\n        assert im.getpixel((64, 32)) == (128, 191)\n\n    with Image.open(\"Tests/images/apng/mode_palette.png\") as im:\n        assert im.mode == \"P\"\n        im.seek(im.n_frames - 1)\n        im = im.convert(\"RGB\")\n        assert im.getpixel((0, 0)) == (0, 255, 0)\n        assert im.getpixel((64, 32)) == (0, 255, 0)\n\n    with Image.open(\"Tests/images/apng/mode_palette_alpha.png\") as im:\n        assert im.mode == \"P\"\n        im.seek(im.n_frames - 1)\n        im = im.convert(\"RGBA\")\n        assert im.getpixel((0, 0)) == (255, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (255, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/mode_palette_1bit_alpha.png\") as im:\n        assert im.mode == \"P\"\n        im.seek(im.n_frames - 1)\n        im = im.convert(\"RGBA\")\n        assert im.getpixel((0, 0)) == (0, 0, 255, 128)\n        assert im.getpixel((64, 32)) == (0, 0, 255, 128)\n\n\ndef test_apng_chunk_errors():\n    with Image.open(\"Tests/images/apng/chunk_no_actl.png\") as im:\n        assert not im.is_animated\n\n    with pytest.warns(UserWarning):\n        with Image.open(\"Tests/images/apng/chunk_multi_actl.png\") as im:\n            im.load()\n        assert not im.is_animated\n\n    with Image.open(\"Tests/images/apng/chunk_actl_after_idat.png\") as im:\n        assert not im.is_animated\n\n    with Image.open(\"Tests/images/apng/chunk_no_fctl.png\") as im:\n        with pytest.raises(SyntaxError):\n            im.seek(im.n_frames - 1)\n\n    with Image.open(\"Tests/images/apng/chunk_repeat_fctl.png\") as im:\n        with pytest.raises(SyntaxError):\n            im.seek(im.n_frames - 1)\n\n    with Image.open(\"Tests/images/apng/chunk_no_fdat.png\") as im:\n        with pytest.raises(SyntaxError):\n            im.seek(im.n_frames - 1)\n\n\ndef test_apng_syntax_errors():\n    with pytest.warns(UserWarning):\n        with Image.open(\"Tests/images/apng/syntax_num_frames_zero.png\") as im:\n            assert not im.is_animated\n            with pytest.raises(OSError):\n                im.load()\n\n    with pytest.warns(UserWarning):\n        with Image.open(\"Tests/images/apng/syntax_num_frames_zero_default.png\") as im:\n            assert not im.is_animated\n            im.load()\n\n    # we can handle this case gracefully\n    exception = None\n    with Image.open(\"Tests/images/apng/syntax_num_frames_low.png\") as im:\n        try:\n            im.seek(im.n_frames - 1)\n        except Exception as e:\n            exception = e\n        assert exception is None\n\n    with pytest.raises(OSError):\n        with Image.open(\"Tests/images/apng/syntax_num_frames_high.png\") as im:\n            im.seek(im.n_frames - 1)\n            im.load()\n\n    with pytest.warns(UserWarning):\n        with Image.open(\"Tests/images/apng/syntax_num_frames_invalid.png\") as im:\n            assert not im.is_animated\n            im.load()\n\n\n@pytest.mark.parametrize(\n    \"test_file\",\n    (\n        \"sequence_start.png\",\n        \"sequence_gap.png\",\n        \"sequence_repeat.png\",\n        \"sequence_repeat_chunk.png\",\n        \"sequence_reorder.png\",\n        \"sequence_reorder_chunk.png\",\n        \"sequence_fdat_fctl.png\",\n    ),\n)\ndef test_apng_sequence_errors(test_file):\n    with pytest.raises(SyntaxError):\n        with Image.open(f\"Tests/images/apng/{test_file}\") as im:\n            im.seek(im.n_frames - 1)\n            im.load()\n\n\ndef test_apng_save(tmp_path):\n    with Image.open(\"Tests/images/apng/single_frame.png\") as im:\n        test_file = str(tmp_path / \"temp.png\")\n        im.save(test_file, save_all=True)\n\n    with Image.open(test_file) as im:\n        im.load()\n        assert not im.is_animated\n        assert im.n_frames == 1\n        assert im.get_format_mimetype() == \"image/png\"\n        assert im.info.get(\"default_image\") is None\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/single_frame_default.png\") as im:\n        frames = []\n        for frame_im in ImageSequence.Iterator(im):\n            frames.append(frame_im.copy())\n        frames[0].save(\n            test_file, save_all=True, default_image=True, append_images=frames[1:]\n        )\n\n    with Image.open(test_file) as im:\n        im.load()\n        assert im.is_animated\n        assert im.n_frames == 2\n        assert im.get_format_mimetype() == \"image/apng\"\n        assert im.info.get(\"default_image\")\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_save_alpha(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n\n    im = Image.new(\"RGBA\", (1, 1), (255, 0, 0, 255))\n    im2 = Image.new(\"RGBA\", (1, 1), (255, 0, 0, 127))\n    im.save(test_file, save_all=True, append_images=[im2])\n\n    with Image.open(test_file) as reloaded:\n        assert reloaded.getpixel((0, 0)) == (255, 0, 0, 255)\n\n        reloaded.seek(1)\n        assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127)\n\n\ndef test_apng_save_split_fdat(tmp_path):\n    # test to make sure we do not generate sequence errors when writing\n    # frames with image data spanning multiple fdAT chunks (in this case\n    # both the default image and first animation frame will span multiple\n    # data chunks)\n    test_file = str(tmp_path / \"temp.png\")\n    with Image.open(\"Tests/images/old-style-jpeg-compression.png\") as im:\n        frames = [im.copy(), Image.new(\"RGBA\", im.size, (255, 0, 0, 255))]\n        im.save(\n            test_file,\n            save_all=True,\n            default_image=True,\n            append_images=frames,\n        )\n    with Image.open(test_file) as im:\n        exception = None\n        try:\n            im.seek(im.n_frames - 1)\n            im.load()\n        except Exception as e:\n            exception = e\n        assert exception is None\n\n\ndef test_apng_save_duration_loop(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    with Image.open(\"Tests/images/apng/delay.png\") as im:\n        frames = []\n        durations = []\n        loop = im.info.get(\"loop\")\n        default_image = im.info.get(\"default_image\")\n        for i, frame_im in enumerate(ImageSequence.Iterator(im)):\n            frames.append(frame_im.copy())\n            if i != 0 or not default_image:\n                durations.append(frame_im.info.get(\"duration\", 0))\n        frames[0].save(\n            test_file,\n            save_all=True,\n            default_image=default_image,\n            append_images=frames[1:],\n            duration=durations,\n            loop=loop,\n        )\n\n    with Image.open(test_file) as im:\n        im.load()\n        assert im.info.get(\"loop\") == loop\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n        im.seek(3)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(4)\n        assert im.info.get(\"duration\") == 1000.0\n\n    # test removal of duplicated frames\n    frame = Image.new(\"RGBA\", (128, 64), (255, 0, 0, 255))\n    frame.save(\n        test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]\n    )\n    with Image.open(test_file) as im:\n        assert im.n_frames == 1\n        assert \"duration\" not in im.info\n\n    different_frame = Image.new(\"RGBA\", (128, 64))\n    frame.save(\n        test_file,\n        save_all=True,\n        append_images=[frame, different_frame],\n        duration=[500, 100, 150],\n    )\n    with Image.open(test_file) as im:\n        assert im.n_frames == 2\n        assert im.info[\"duration\"] == 600\n\n        im.seek(1)\n        assert im.info[\"duration\"] == 150\n\n    # test info duration\n    frame.info[\"duration\"] = 300\n    frame.save(test_file, save_all=True, append_images=[frame, different_frame])\n    with Image.open(test_file) as im:\n        assert im.n_frames == 2\n        assert im.info[\"duration\"] == 600\n\n\ndef test_apng_save_disposal(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    size = (128, 64)\n    red = Image.new(\"RGBA\", size, (255, 0, 0, 255))\n    green = Image.new(\"RGBA\", size, (0, 255, 0, 255))\n    transparent = Image.new(\"RGBA\", size, (0, 0, 0, 0))\n\n    # test OP_NONE\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green, transparent],\n        disposal=PngImagePlugin.Disposal.OP_NONE,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test OP_BACKGROUND\n    disposal = [\n        PngImagePlugin.Disposal.OP_NONE,\n        PngImagePlugin.Disposal.OP_BACKGROUND,\n        PngImagePlugin.Disposal.OP_NONE,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[red, transparent],\n        disposal=disposal,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    disposal = [\n        PngImagePlugin.Disposal.OP_NONE,\n        PngImagePlugin.Disposal.OP_BACKGROUND,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green],\n        disposal=disposal,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test OP_PREVIOUS\n    disposal = [\n        PngImagePlugin.Disposal.OP_NONE,\n        PngImagePlugin.Disposal.OP_PREVIOUS,\n        PngImagePlugin.Disposal.OP_NONE,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green, red, transparent],\n        default_image=True,\n        disposal=disposal,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(3)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    disposal = [\n        PngImagePlugin.Disposal.OP_NONE,\n        PngImagePlugin.Disposal.OP_PREVIOUS,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green],\n        disposal=disposal,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test info disposal\n    red.info[\"disposal\"] = PngImagePlugin.Disposal.OP_BACKGROUND\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[Image.new(\"RGBA\", (10, 10), (0, 255, 0, 255))],\n    )\n    with Image.open(test_file) as im:\n        im.seek(1)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n\ndef test_apng_save_disposal_previous(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    size = (128, 64)\n    blue = Image.new(\"RGBA\", size, (0, 0, 255, 255))\n    red = Image.new(\"RGBA\", size, (255, 0, 0, 255))\n    green = Image.new(\"RGBA\", size, (0, 255, 0, 255))\n\n    # test OP_NONE\n    blue.save(\n        test_file,\n        save_all=True,\n        append_images=[red, green],\n        disposal=PngImagePlugin.Disposal.OP_PREVIOUS,\n    )\n    with Image.open(test_file) as im:\n        assert im.getpixel((0, 0)) == (0, 0, 255, 255)\n\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_save_blend(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    size = (128, 64)\n    red = Image.new(\"RGBA\", size, (255, 0, 0, 255))\n    green = Image.new(\"RGBA\", size, (0, 255, 0, 255))\n    transparent = Image.new(\"RGBA\", size, (0, 0, 0, 0))\n\n    # test OP_SOURCE on solid color\n    blend = [\n        PngImagePlugin.Blend.OP_OVER,\n        PngImagePlugin.Blend.OP_SOURCE,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[red, green],\n        default_image=True,\n        disposal=PngImagePlugin.Disposal.OP_NONE,\n        blend=blend,\n    )\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test OP_SOURCE on transparent color\n    blend = [\n        PngImagePlugin.Blend.OP_OVER,\n        PngImagePlugin.Blend.OP_SOURCE,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[red, transparent],\n        default_image=True,\n        disposal=PngImagePlugin.Disposal.OP_NONE,\n        blend=blend,\n    )\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    # test OP_OVER\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green, transparent],\n        default_image=True,\n        disposal=PngImagePlugin.Disposal.OP_NONE,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test info blend\n    red.info[\"blend\"] = PngImagePlugin.Blend.OP_OVER\n    red.save(test_file, save_all=True, append_images=[green, transparent])\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n\n\ndef test_seek_after_close():\n    im = Image.open(\"Tests/images/apng/delay.png\")\n    im.seek(1)\n    im.close()\n\n    with pytest.raises(ValueError):\n        im.seek(0)\n\n\n@pytest.mark.parametrize(\"mode\", (\"RGBA\", \"RGB\", \"P\"))\n@pytest.mark.parametrize(\"default_image\", (True, False))\n@pytest.mark.parametrize(\"duplicate\", (True, False))\ndef test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n\n    im = Image.new(\"L\", (1, 1))\n    im.save(\n        test_file,\n        save_all=True,\n        default_image=default_image,\n        append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)],\n    )\n    with Image.open(test_file) as reloaded:\n        assert reloaded.mode == mode\n",
            "header": {
                "file": {
                    "path": "Tests/test_file_apng.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "fffbc54ca",
                "new_commit_hash": "d0c81b5e9",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Tests/test_file_apng.py"
            },
            "plus_file": {
                "path": "b/Tests/test_file_apng.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 350,
                            "length": 7
                        },
                        "new_range": {
                            "start": 350,
                            "length": 7
                        },
                        "section": "def test_apng_save(tmp_path):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        im.load()",
                                "type": "context"
                            },
                            {
                                "content": "        assert not im.is_animated",
                                "type": "context"
                            },
                            {
                                "content": "        assert im.n_frames == 1",
                                "type": "context"
                            },
                            {
                                "content": "        assert im.get_format_mimetype() == \"image/apng\"",
                                "type": "deleted"
                            },
                            {
                                "content": "        assert im.get_format_mimetype() == \"image/png\"",
                                "type": "added"
                            },
                            {
                                "content": "        assert im.info.get(\"default_image\") is None",
                                "type": "context"
                            },
                            {
                                "content": "        assert im.getpixel((0, 0)) == (0, 255, 0, 255)",
                                "type": "context"
                            },
                            {
                                "content": "        assert im.getpixel((64, 32)) == (0, 255, 0, 255)",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Tests/test_file_apng.py",
                            "type": "function",
                            "name": "test_apng_save",
                            "content": "def test_apng_save(tmp_path):\n    with Image.open(\"Tests/images/apng/single_frame.png\") as im:\n        test_file = str(tmp_path / \"temp.png\")\n        im.save(test_file, save_all=True)\n\n    with Image.open(test_file) as im:\n        im.load()\n        assert not im.is_animated\n        assert im.n_frames == 1\n        assert im.get_format_mimetype() == \"image/apng\"\n        assert im.info.get(\"default_image\") is None\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/single_frame_default.png\") as im:\n        frames = []\n        for frame_im in ImageSequence.Iterator(im):\n            frames.append(frame_im.copy())\n        frames[0].save(\n            test_file, save_all=True, default_image=True, append_images=frames[1:]\n        )\n\n    with Image.open(test_file) as im:\n        im.load()\n        assert im.is_animated\n        assert im.n_frames == 2\n        assert im.get_format_mimetype() == \"image/apng\"\n        assert im.info.get(\"default_image\")\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 344,
                            "end_lineno": 374,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 450,
                            "length": 26
                        },
                        "new_range": {
                            "start": 450,
                            "length": 29
                        },
                        "section": "def test_apng_save_duration_loop(tmp_path):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]",
                                "type": "context"
                            },
                            {
                                "content": "    )",
                                "type": "context"
                            },
                            {
                                "content": "    with Image.open(test_file) as im:",
                                "type": "context"
                            },
                            {
                                "content": "        im.load()",
                                "type": "deleted"
                            },
                            {
                                "content": "        assert im.n_frames == 1",
                                "type": "context"
                            },
                            {
                                "content": "        assert im.info.get(\"duration\") == 750",
                                "type": "deleted"
                            },
                            {
                                "content": "        assert \"duration\" not in im.info",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    # test info duration",
                                "type": "deleted"
                            },
                            {
                                "content": "    frame.info[\"duration\"] = 750",
                                "type": "deleted"
                            },
                            {
                                "content": "    frame.save(test_file, save_all=True)",
                                "type": "deleted"
                            },
                            {
                                "content": "    different_frame = Image.new(\"RGBA\", (128, 64))",
                                "type": "added"
                            },
                            {
                                "content": "    frame.save(",
                                "type": "added"
                            },
                            {
                                "content": "        test_file,",
                                "type": "added"
                            },
                            {
                                "content": "        save_all=True,",
                                "type": "added"
                            },
                            {
                                "content": "        append_images=[frame, different_frame],",
                                "type": "added"
                            },
                            {
                                "content": "        duration=[500, 100, 150],",
                                "type": "added"
                            },
                            {
                                "content": "    )",
                                "type": "added"
                            },
                            {
                                "content": "    with Image.open(test_file) as im:",
                                "type": "context"
                            },
                            {
                                "content": "        assert im.info.get(\"duration\") == 750",
                                "type": "deleted"
                            },
                            {
                                "content": "",
                                "type": "deleted"
                            },
                            {
                                "content": "        assert im.n_frames == 2",
                                "type": "added"
                            },
                            {
                                "content": "        assert im.info[\"duration\"] == 600",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "def test_apng_save_duplicate_duration(tmp_path):",
                                "type": "deleted"
                            },
                            {
                                "content": "    test_file = str(tmp_path / \"temp.png\")",
                                "type": "deleted"
                            },
                            {
                                "content": "    frame = Image.new(\"RGB\", (1, 1))",
                                "type": "deleted"
                            },
                            {
                                "content": "        im.seek(1)",
                                "type": "added"
                            },
                            {
                                "content": "        assert im.info[\"duration\"] == 150",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    # Test a single duration is correctly combined across duplicate frames",
                                "type": "deleted"
                            },
                            {
                                "content": "    frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500)",
                                "type": "deleted"
                            },
                            {
                                "content": "    # test info duration",
                                "type": "added"
                            },
                            {
                                "content": "    frame.info[\"duration\"] = 300",
                                "type": "added"
                            },
                            {
                                "content": "    frame.save(test_file, save_all=True, append_images=[frame, different_frame])",
                                "type": "added"
                            },
                            {
                                "content": "    with Image.open(test_file) as im:",
                                "type": "context"
                            },
                            {
                                "content": "        assert im.n_frames == 1",
                                "type": "deleted"
                            },
                            {
                                "content": "        assert im.info.get(\"duration\") == 1500",
                                "type": "deleted"
                            },
                            {
                                "content": "        assert im.n_frames == 2",
                                "type": "added"
                            },
                            {
                                "content": "        assert im.info[\"duration\"] == 600",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "def test_apng_save_disposal(tmp_path):",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Tests/test_file_apng.py",
                            "type": "function",
                            "name": "test_apng_save_duration_loop",
                            "content": "def test_apng_save_duration_loop(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    with Image.open(\"Tests/images/apng/delay.png\") as im:\n        frames = []\n        durations = []\n        loop = im.info.get(\"loop\")\n        default_image = im.info.get(\"default_image\")\n        for i, frame_im in enumerate(ImageSequence.Iterator(im)):\n            frames.append(frame_im.copy())\n            if i != 0 or not default_image:\n                durations.append(frame_im.info.get(\"duration\", 0))\n        frames[0].save(\n            test_file,\n            save_all=True,\n            default_image=default_image,\n            append_images=frames[1:],\n            duration=durations,\n            loop=loop,\n        )\n\n    with Image.open(test_file) as im:\n        im.load()\n        assert im.info.get(\"loop\") == loop\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n        im.seek(3)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(4)\n        assert im.info.get(\"duration\") == 1000.0\n\n    # test removal of duplicated frames\n    frame = Image.new(\"RGBA\", (128, 64), (255, 0, 0, 255))\n    frame.save(\n        test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]\n    )\n    with Image.open(test_file) as im:\n        im.load()\n        assert im.n_frames == 1\n        assert im.info.get(\"duration\") == 750\n\n    # test info duration\n    frame.info[\"duration\"] = 750\n    frame.save(test_file, save_all=True)\n    with Image.open(test_file) as im:\n        assert im.info.get(\"duration\") == 750",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 415,
                            "end_lineno": 461,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": [
                        {
                            "file_name": "Tests/test_file_apng.py",
                            "type": "function",
                            "name": "test_apng_save_duplicate_duration",
                            "content": "def test_apng_save_duplicate_duration(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    frame = Image.new(\"RGB\", (1, 1))\n\n    # Test a single duration is correctly combined across duplicate frames\n    frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500)\n    with Image.open(test_file) as im:\n        assert im.n_frames == 1\n        assert im.info.get(\"duration\") == 1500",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 464,
                            "end_lineno": 472,
                            "parent": null
                        }
                    ]
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 674,
                            "length": 7
                        },
                        "new_range": {
                            "start": 677,
                            "length": 8
                        },
                        "section": "def test_seek_after_close():"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "@pytest.mark.parametrize(\"mode\", (\"RGBA\", \"RGB\", \"P\"))",
                                "type": "context"
                            },
                            {
                                "content": "@pytest.mark.parametrize(\"default_image\", (True, False))",
                                "type": "context"
                            },
                            {
                                "content": "def test_different_modes_in_later_frames(mode, default_image, tmp_path):",
                                "type": "deleted"
                            },
                            {
                                "content": "@pytest.mark.parametrize(\"duplicate\", (True, False))",
                                "type": "added"
                            },
                            {
                                "content": "def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path):",
                                "type": "added"
                            },
                            {
                                "content": "    test_file = str(tmp_path / \"temp.png\")",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    im = Image.new(\"L\", (1, 1))",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Tests/test_file_apng.py",
                            "type": "function",
                            "name": "test_different_modes_in_later_frames",
                            "content": "def test_different_modes_in_later_frames(mode, default_image, tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n\n    im = Image.new(\"L\", (1, 1))\n    im.save(\n        test_file,\n        save_all=True,\n        default_image=default_image,\n        append_images=[Image.new(mode, (1, 1))],\n    )\n    with Image.open(test_file) as reloaded:\n        assert reloaded.mode == mode",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 677,
                            "end_lineno": 688,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 682,
                            "length": 7
                        },
                        "new_range": {
                            "start": 686,
                            "length": 7
                        },
                        "section": "def test_different_modes_in_later_frames(mode, default_image, tmp_path):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        test_file,",
                                "type": "context"
                            },
                            {
                                "content": "        save_all=True,",
                                "type": "context"
                            },
                            {
                                "content": "        default_image=default_image,",
                                "type": "context"
                            },
                            {
                                "content": "        append_images=[Image.new(mode, (1, 1))],",
                                "type": "deleted"
                            },
                            {
                                "content": "        append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)],",
                                "type": "added"
                            },
                            {
                                "content": "    )",
                                "type": "context"
                            },
                            {
                                "content": "    with Image.open(test_file) as reloaded:",
                                "type": "context"
                            },
                            {
                                "content": "        assert reloaded.mode == mode",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Tests/test_file_apng.py",
                            "type": "function",
                            "name": "test_different_modes_in_later_frames",
                            "content": "def test_different_modes_in_later_frames(mode, default_image, tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n\n    im = Image.new(\"L\", (1, 1))\n    im.save(\n        test_file,\n        save_all=True,\n        default_image=default_image,\n        append_images=[Image.new(mode, (1, 1))],\n    )\n    with Image.open(test_file) as reloaded:\n        assert reloaded.mode == mode",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 677,
                            "end_lineno": 688,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "#\n# The Python Imaging Library.\n# $Id$\n#\n# PNG support code\n#\n# See \"PNG (Portable Network Graphics) Specification, version 1.0;\n# W3C Recommendation\", 1996-10-01, Thomas Boutell (ed.).\n#\n# history:\n# 1996-05-06 fl   Created (couldn't resist it)\n# 1996-12-14 fl   Upgraded, added read and verify support (0.2)\n# 1996-12-15 fl   Separate PNG stream parser\n# 1996-12-29 fl   Added write support, added getchunks\n# 1996-12-30 fl   Eliminated circular references in decoder (0.3)\n# 1998-07-12 fl   Read/write 16-bit images as mode I (0.4)\n# 2001-02-08 fl   Added transparency support (from Zircon) (0.5)\n# 2001-04-16 fl   Don't close data source in \"open\" method (0.6)\n# 2004-02-24 fl   Don't even pretend to support interlaced files (0.7)\n# 2004-08-31 fl   Do basic sanity check on chunk identifiers (0.8)\n# 2004-09-20 fl   Added PngInfo chunk container\n# 2004-12-18 fl   Added DPI read support (based on code by Niki Spahiev)\n# 2008-08-13 fl   Added tRNS support for RGB images\n# 2009-03-06 fl   Support for preserving ICC profiles (by Florian Hoech)\n# 2009-03-08 fl   Added zTXT support (from Lowell Alleman)\n# 2009-03-29 fl   Read interlaced PNG files (from Conrado Porto Lopes Gouvua)\n#\n# Copyright (c) 1997-2009 by Secret Labs AB\n# Copyright (c) 1996 by Fredrik Lundh\n#\n# See the README file for information on usage and redistribution.\n#\n\nimport itertools\nimport logging\nimport re\nimport struct\nimport warnings\nimport zlib\nfrom enum import IntEnum\n\nfrom . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence\nfrom ._binary import i16be as i16\nfrom ._binary import i32be as i32\nfrom ._binary import o8\nfrom ._binary import o16be as o16\nfrom ._binary import o32be as o32\n\nlogger = logging.getLogger(__name__)\n\nis_cid = re.compile(rb\"\\w\\w\\w\\w\").match\n\n\n_MAGIC = b\"\\211PNG\\r\\n\\032\\n\"\n\n\n_MODES = {\n    # supported bits/color combinations, and corresponding modes/rawmodes\n    # Grayscale\n    (1, 0): (\"1\", \"1\"),\n    (2, 0): (\"L\", \"L;2\"),\n    (4, 0): (\"L\", \"L;4\"),\n    (8, 0): (\"L\", \"L\"),\n    (16, 0): (\"I\", \"I;16B\"),\n    # Truecolour\n    (8, 2): (\"RGB\", \"RGB\"),\n    (16, 2): (\"RGB\", \"RGB;16B\"),\n    # Indexed-colour\n    (1, 3): (\"P\", \"P;1\"),\n    (2, 3): (\"P\", \"P;2\"),\n    (4, 3): (\"P\", \"P;4\"),\n    (8, 3): (\"P\", \"P\"),\n    # Grayscale with alpha\n    (8, 4): (\"LA\", \"LA\"),\n    (16, 4): (\"RGBA\", \"LA;16B\"),  # LA;16B->LA not yet available\n    # Truecolour with alpha\n    (8, 6): (\"RGBA\", \"RGBA\"),\n    (16, 6): (\"RGBA\", \"RGBA;16B\"),\n}\n\n\n_simple_palette = re.compile(b\"^\\xff*\\x00\\xff*$\")\n\nMAX_TEXT_CHUNK = ImageFile.SAFEBLOCK\n\"\"\"\nMaximum decompressed size for a iTXt or zTXt chunk.\nEliminates decompression bombs where compressed chunks can expand 1000x.\nSee :ref:`Text in PNG File Format<png-text>`.\n\"\"\"\nMAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK\n\"\"\"\nSet the maximum total text chunk size.\nSee :ref:`Text in PNG File Format<png-text>`.\n\"\"\"\n\n\n# APNG frame disposal modes\nclass Disposal(IntEnum):\n    OP_NONE = 0\n    \"\"\"\n    No disposal is done on this frame before rendering the next frame.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n    OP_BACKGROUND = 1\n    \"\"\"\n    This frame’s modified region is cleared to fully transparent black before rendering\n    the next frame.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n    OP_PREVIOUS = 2\n    \"\"\"\n    This frame’s modified region is reverted to the previous frame’s contents before\n    rendering the next frame.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n\n\n# APNG frame blend modes\nclass Blend(IntEnum):\n    OP_SOURCE = 0\n    \"\"\"\n    All color components of this frame, including alpha, overwrite the previous output\n    image contents.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n    OP_OVER = 1\n    \"\"\"\n    This frame should be alpha composited with the previous output image contents.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n\n\ndef _safe_zlib_decompress(s):\n    dobj = zlib.decompressobj()\n    plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)\n    if dobj.unconsumed_tail:\n        msg = \"Decompressed Data Too Large\"\n        raise ValueError(msg)\n    return plaintext\n\n\ndef _crc32(data, seed=0):\n    return zlib.crc32(data, seed) & 0xFFFFFFFF\n\n\n# --------------------------------------------------------------------\n# Support classes.  Suitable for PNG and related formats like MNG etc.\n\n\nclass ChunkStream:\n    def __init__(self, fp):\n        self.fp = fp\n        self.queue = []\n\n    def read(self):\n        \"\"\"Fetch a new chunk. Returns header information.\"\"\"\n        cid = None\n\n        if self.queue:\n            cid, pos, length = self.queue.pop()\n            self.fp.seek(pos)\n        else:\n            s = self.fp.read(8)\n            cid = s[4:]\n            pos = self.fp.tell()\n            length = i32(s)\n\n        if not is_cid(cid):\n            if not ImageFile.LOAD_TRUNCATED_IMAGES:\n                msg = f\"broken PNG file (chunk {repr(cid)})\"\n                raise SyntaxError(msg)\n\n        return cid, pos, length\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, *args):\n        self.close()\n\n    def close(self):\n        self.queue = self.fp = None\n\n    def push(self, cid, pos, length):\n        self.queue.append((cid, pos, length))\n\n    def call(self, cid, pos, length):\n        \"\"\"Call the appropriate chunk handler\"\"\"\n\n        logger.debug(\"STREAM %r %s %s\", cid, pos, length)\n        return getattr(self, \"chunk_\" + cid.decode(\"ascii\"))(pos, length)\n\n    def crc(self, cid, data):\n        \"\"\"Read and verify checksum\"\"\"\n\n        # Skip CRC checks for ancillary chunks if allowed to load truncated\n        # images\n        # 5th byte of first char is 1 [specs, section 5.4]\n        if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1):\n            self.crc_skip(cid, data)\n            return\n\n        try:\n            crc1 = _crc32(data, _crc32(cid))\n            crc2 = i32(self.fp.read(4))\n            if crc1 != crc2:\n                msg = f\"broken PNG file (bad header checksum in {repr(cid)})\"\n                raise SyntaxError(msg)\n        except struct.error as e:\n            msg = f\"broken PNG file (incomplete checksum in {repr(cid)})\"\n            raise SyntaxError(msg) from e\n\n    def crc_skip(self, cid, data):\n        \"\"\"Read checksum\"\"\"\n\n        self.fp.read(4)\n\n    def verify(self, endchunk=b\"IEND\"):\n        # Simple approach; just calculate checksum for all remaining\n        # blocks.  Must be called directly after open.\n\n        cids = []\n\n        while True:\n            try:\n                cid, pos, length = self.read()\n            except struct.error as e:\n                msg = \"truncated PNG file\"\n                raise OSError(msg) from e\n\n            if cid == endchunk:\n                break\n            self.crc(cid, ImageFile._safe_read(self.fp, length))\n            cids.append(cid)\n\n        return cids\n\n\nclass iTXt(str):\n    \"\"\"\n    Subclass of string to allow iTXt chunks to look like strings while\n    keeping their extra information\n\n    \"\"\"\n\n    @staticmethod\n    def __new__(cls, text, lang=None, tkey=None):\n        \"\"\"\n        :param cls: the class to use when creating the instance\n        :param text: value for this key\n        :param lang: language code\n        :param tkey: UTF-8 version of the key name\n        \"\"\"\n\n        self = str.__new__(cls, text)\n        self.lang = lang\n        self.tkey = tkey\n        return self\n\n\nclass PngInfo:\n    \"\"\"\n    PNG chunk container (for use with save(pnginfo=))\n\n    \"\"\"\n\n    def __init__(self):\n        self.chunks = []\n\n    def add(self, cid, data, after_idat=False):\n        \"\"\"Appends an arbitrary chunk. Use with caution.\n\n        :param cid: a byte string, 4 bytes long.\n        :param data: a byte string of the encoded data\n        :param after_idat: for use with private chunks. Whether the chunk\n                           should be written after IDAT\n\n        \"\"\"\n\n        chunk = [cid, data]\n        if after_idat:\n            chunk.append(True)\n        self.chunks.append(tuple(chunk))\n\n    def add_itxt(self, key, value, lang=\"\", tkey=\"\", zip=False):\n        \"\"\"Appends an iTXt chunk.\n\n        :param key: latin-1 encodable text key name\n        :param value: value for this key\n        :param lang: language code\n        :param tkey: UTF-8 version of the key name\n        :param zip: compression flag\n\n        \"\"\"\n\n        if not isinstance(key, bytes):\n            key = key.encode(\"latin-1\", \"strict\")\n        if not isinstance(value, bytes):\n            value = value.encode(\"utf-8\", \"strict\")\n        if not isinstance(lang, bytes):\n            lang = lang.encode(\"utf-8\", \"strict\")\n        if not isinstance(tkey, bytes):\n            tkey = tkey.encode(\"utf-8\", \"strict\")\n\n        if zip:\n            self.add(\n                b\"iTXt\",\n                key + b\"\\0\\x01\\0\" + lang + b\"\\0\" + tkey + b\"\\0\" + zlib.compress(value),\n            )\n        else:\n            self.add(b\"iTXt\", key + b\"\\0\\0\\0\" + lang + b\"\\0\" + tkey + b\"\\0\" + value)\n\n    def add_text(self, key, value, zip=False):\n        \"\"\"Appends a text chunk.\n\n        :param key: latin-1 encodable text key name\n        :param value: value for this key, text or an\n           :py:class:`PIL.PngImagePlugin.iTXt` instance\n        :param zip: compression flag\n\n        \"\"\"\n        if isinstance(value, iTXt):\n            return self.add_itxt(key, value, value.lang, value.tkey, zip=zip)\n\n        # The tEXt chunk stores latin-1 text\n        if not isinstance(value, bytes):\n            try:\n                value = value.encode(\"latin-1\", \"strict\")\n            except UnicodeError:\n                return self.add_itxt(key, value, zip=zip)\n\n        if not isinstance(key, bytes):\n            key = key.encode(\"latin-1\", \"strict\")\n\n        if zip:\n            self.add(b\"zTXt\", key + b\"\\0\\0\" + zlib.compress(value))\n        else:\n            self.add(b\"tEXt\", key + b\"\\0\" + value)\n\n\n# --------------------------------------------------------------------\n# PNG image stream (IHDR/IEND)\n\n\nclass PngStream(ChunkStream):\n    def __init__(self, fp):\n        super().__init__(fp)\n\n        # local copies of Image attributes\n        self.im_info = {}\n        self.im_text = {}\n        self.im_size = (0, 0)\n        self.im_mode = None\n        self.im_tile = None\n        self.im_palette = None\n        self.im_custom_mimetype = None\n        self.im_n_frames = None\n        self._seq_num = None\n        self.rewind_state = None\n\n        self.text_memory = 0\n\n    def check_text_memory(self, chunklen):\n        self.text_memory += chunklen\n        if self.text_memory > MAX_TEXT_MEMORY:\n            msg = (\n                \"Too much memory used in text chunks: \"\n                f\"{self.text_memory}>MAX_TEXT_MEMORY\"\n            )\n            raise ValueError(msg)\n\n    def save_rewind(self):\n        self.rewind_state = {\n            \"info\": self.im_info.copy(),\n            \"tile\": self.im_tile,\n            \"seq_num\": self._seq_num,\n        }\n\n    def rewind(self):\n        self.im_info = self.rewind_state[\"info\"]\n        self.im_tile = self.rewind_state[\"tile\"]\n        self._seq_num = self.rewind_state[\"seq_num\"]\n\n    def chunk_iCCP(self, pos, length):\n        # ICC profile\n        s = ImageFile._safe_read(self.fp, length)\n        # according to PNG spec, the iCCP chunk contains:\n        # Profile name  1-79 bytes (character string)\n        # Null separator        1 byte (null character)\n        # Compression method    1 byte (0)\n        # Compressed profile    n bytes (zlib with deflate compression)\n        i = s.find(b\"\\0\")\n        logger.debug(\"iCCP profile name %r\", s[:i])\n        logger.debug(\"Compression method %s\", s[i])\n        comp_method = s[i]\n        if comp_method != 0:\n            msg = f\"Unknown compression method {comp_method} in iCCP chunk\"\n            raise SyntaxError(msg)\n        try:\n            icc_profile = _safe_zlib_decompress(s[i + 2 :])\n        except ValueError:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                icc_profile = None\n            else:\n                raise\n        except zlib.error:\n            icc_profile = None  # FIXME\n        self.im_info[\"icc_profile\"] = icc_profile\n        return s\n\n    def chunk_IHDR(self, pos, length):\n        # image header\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 13:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"Truncated IHDR chunk\"\n            raise ValueError(msg)\n        self.im_size = i32(s, 0), i32(s, 4)\n        try:\n            self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])]\n        except Exception:\n            pass\n        if s[12]:\n            self.im_info[\"interlace\"] = 1\n        if s[11]:\n            msg = \"unknown filter category\"\n            raise SyntaxError(msg)\n        return s\n\n    def chunk_IDAT(self, pos, length):\n        # image data\n        if \"bbox\" in self.im_info:\n            tile = [(\"zip\", self.im_info[\"bbox\"], pos, self.im_rawmode)]\n        else:\n            if self.im_n_frames is not None:\n                self.im_info[\"default_image\"] = True\n            tile = [(\"zip\", (0, 0) + self.im_size, pos, self.im_rawmode)]\n        self.im_tile = tile\n        self.im_idat = length\n        msg = \"image data found\"\n        raise EOFError(msg)\n\n    def chunk_IEND(self, pos, length):\n        msg = \"end of PNG image\"\n        raise EOFError(msg)\n\n    def chunk_PLTE(self, pos, length):\n        # palette\n        s = ImageFile._safe_read(self.fp, length)\n        if self.im_mode == \"P\":\n            self.im_palette = \"RGB\", s\n        return s\n\n    def chunk_tRNS(self, pos, length):\n        # transparency\n        s = ImageFile._safe_read(self.fp, length)\n        if self.im_mode == \"P\":\n            if _simple_palette.match(s):\n                # tRNS contains only one full-transparent entry,\n                # other entries are full opaque\n                i = s.find(b\"\\0\")\n                if i >= 0:\n                    self.im_info[\"transparency\"] = i\n            else:\n                # otherwise, we have a byte string with one alpha value\n                # for each palette entry\n                self.im_info[\"transparency\"] = s\n        elif self.im_mode in (\"1\", \"L\", \"I\"):\n            self.im_info[\"transparency\"] = i16(s)\n        elif self.im_mode == \"RGB\":\n            self.im_info[\"transparency\"] = i16(s), i16(s, 2), i16(s, 4)\n        return s\n\n    def chunk_gAMA(self, pos, length):\n        # gamma setting\n        s = ImageFile._safe_read(self.fp, length)\n        self.im_info[\"gamma\"] = i32(s) / 100000.0\n        return s\n\n    def chunk_cHRM(self, pos, length):\n        # chromaticity, 8 unsigned ints, actual value is scaled by 100,000\n        # WP x,y, Red x,y, Green x,y Blue x,y\n\n        s = ImageFile._safe_read(self.fp, length)\n        raw_vals = struct.unpack(\">%dI\" % (len(s) // 4), s)\n        self.im_info[\"chromaticity\"] = tuple(elt / 100000.0 for elt in raw_vals)\n        return s\n\n    def chunk_sRGB(self, pos, length):\n        # srgb rendering intent, 1 byte\n        # 0 perceptual\n        # 1 relative colorimetric\n        # 2 saturation\n        # 3 absolute colorimetric\n\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 1:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"Truncated sRGB chunk\"\n            raise ValueError(msg)\n        self.im_info[\"srgb\"] = s[0]\n        return s\n\n    def chunk_pHYs(self, pos, length):\n        # pixels per unit\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 9:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"Truncated pHYs chunk\"\n            raise ValueError(msg)\n        px, py = i32(s, 0), i32(s, 4)\n        unit = s[8]\n        if unit == 1:  # meter\n            dpi = px * 0.0254, py * 0.0254\n            self.im_info[\"dpi\"] = dpi\n        elif unit == 0:\n            self.im_info[\"aspect\"] = px, py\n        return s\n\n    def chunk_tEXt(self, pos, length):\n        # text\n        s = ImageFile._safe_read(self.fp, length)\n        try:\n            k, v = s.split(b\"\\0\", 1)\n        except ValueError:\n            # fallback for broken tEXt tags\n            k = s\n            v = b\"\"\n        if k:\n            k = k.decode(\"latin-1\", \"strict\")\n            v_str = v.decode(\"latin-1\", \"replace\")\n\n            self.im_info[k] = v if k == \"exif\" else v_str\n            self.im_text[k] = v_str\n            self.check_text_memory(len(v_str))\n\n        return s\n\n    def chunk_zTXt(self, pos, length):\n        # compressed text\n        s = ImageFile._safe_read(self.fp, length)\n        try:\n            k, v = s.split(b\"\\0\", 1)\n        except ValueError:\n            k = s\n            v = b\"\"\n        if v:\n            comp_method = v[0]\n        else:\n            comp_method = 0\n        if comp_method != 0:\n            msg = f\"Unknown compression method {comp_method} in zTXt chunk\"\n            raise SyntaxError(msg)\n        try:\n            v = _safe_zlib_decompress(v[1:])\n        except ValueError:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                v = b\"\"\n            else:\n                raise\n        except zlib.error:\n            v = b\"\"\n\n        if k:\n            k = k.decode(\"latin-1\", \"strict\")\n            v = v.decode(\"latin-1\", \"replace\")\n\n            self.im_info[k] = self.im_text[k] = v\n            self.check_text_memory(len(v))\n\n        return s\n\n    def chunk_iTXt(self, pos, length):\n        # international text\n        r = s = ImageFile._safe_read(self.fp, length)\n        try:\n            k, r = r.split(b\"\\0\", 1)\n        except ValueError:\n            return s\n        if len(r) < 2:\n            return s\n        cf, cm, r = r[0], r[1], r[2:]\n        try:\n            lang, tk, v = r.split(b\"\\0\", 2)\n        except ValueError:\n            return s\n        if cf != 0:\n            if cm == 0:\n                try:\n                    v = _safe_zlib_decompress(v)\n                except ValueError:\n                    if ImageFile.LOAD_TRUNCATED_IMAGES:\n                        return s\n                    else:\n                        raise\n                except zlib.error:\n                    return s\n            else:\n                return s\n        try:\n            k = k.decode(\"latin-1\", \"strict\")\n            lang = lang.decode(\"utf-8\", \"strict\")\n            tk = tk.decode(\"utf-8\", \"strict\")\n            v = v.decode(\"utf-8\", \"strict\")\n        except UnicodeError:\n            return s\n\n        self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk)\n        self.check_text_memory(len(v))\n\n        return s\n\n    def chunk_eXIf(self, pos, length):\n        s = ImageFile._safe_read(self.fp, length)\n        self.im_info[\"exif\"] = b\"Exif\\x00\\x00\" + s\n        return s\n\n    # APNG chunks\n    def chunk_acTL(self, pos, length):\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 8:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"APNG contains truncated acTL chunk\"\n            raise ValueError(msg)\n        if self.im_n_frames is not None:\n            self.im_n_frames = None\n            warnings.warn(\"Invalid APNG, will use default PNG image if possible\")\n            return s\n        n_frames = i32(s)\n        if n_frames == 0 or n_frames > 0x80000000:\n            warnings.warn(\"Invalid APNG, will use default PNG image if possible\")\n            return s\n        self.im_n_frames = n_frames\n        self.im_info[\"loop\"] = i32(s, 4)\n        self.im_custom_mimetype = \"image/apng\"\n        return s\n\n    def chunk_fcTL(self, pos, length):\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 26:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"APNG contains truncated fcTL chunk\"\n            raise ValueError(msg)\n        seq = i32(s)\n        if (self._seq_num is None and seq != 0) or (\n            self._seq_num is not None and self._seq_num != seq - 1\n        ):\n            msg = \"APNG contains frame sequence errors\"\n            raise SyntaxError(msg)\n        self._seq_num = seq\n        width, height = i32(s, 4), i32(s, 8)\n        px, py = i32(s, 12), i32(s, 16)\n        im_w, im_h = self.im_size\n        if px + width > im_w or py + height > im_h:\n            msg = \"APNG contains invalid frames\"\n            raise SyntaxError(msg)\n        self.im_info[\"bbox\"] = (px, py, px + width, py + height)\n        delay_num, delay_den = i16(s, 20), i16(s, 22)\n        if delay_den == 0:\n            delay_den = 100\n        self.im_info[\"duration\"] = float(delay_num) / float(delay_den) * 1000\n        self.im_info[\"disposal\"] = s[24]\n        self.im_info[\"blend\"] = s[25]\n        return s\n\n    def chunk_fdAT(self, pos, length):\n        if length < 4:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                s = ImageFile._safe_read(self.fp, length)\n                return s\n            msg = \"APNG contains truncated fDAT chunk\"\n            raise ValueError(msg)\n        s = ImageFile._safe_read(self.fp, 4)\n        seq = i32(s)\n        if self._seq_num != seq - 1:\n            msg = \"APNG contains frame sequence errors\"\n            raise SyntaxError(msg)\n        self._seq_num = seq\n        return self.chunk_IDAT(pos + 4, length - 4)\n\n\n# --------------------------------------------------------------------\n# PNG reader\n\n\ndef _accept(prefix):\n    return prefix[:8] == _MAGIC\n\n\n##\n# Image plugin for PNG images.\n\n\nclass PngImageFile(ImageFile.ImageFile):\n    format = \"PNG\"\n    format_description = \"Portable network graphics\"\n\n    def _open(self):\n        if not _accept(self.fp.read(8)):\n            msg = \"not a PNG file\"\n            raise SyntaxError(msg)\n        self._fp = self.fp\n        self.__frame = 0\n\n        #\n        # Parse headers up to the first IDAT or fDAT chunk\n\n        self.private_chunks = []\n        self.png = PngStream(self.fp)\n\n        while True:\n            #\n            # get next chunk\n\n            cid, pos, length = self.png.read()\n\n            try:\n                s = self.png.call(cid, pos, length)\n            except EOFError:\n                break\n            except AttributeError:\n                logger.debug(\"%r %s %s (unknown)\", cid, pos, length)\n                s = ImageFile._safe_read(self.fp, length)\n                if cid[1:2].islower():\n                    self.private_chunks.append((cid, s))\n\n            self.png.crc(cid, s)\n\n        #\n        # Copy relevant attributes from the PngStream.  An alternative\n        # would be to let the PngStream class modify these attributes\n        # directly, but that introduces circular references which are\n        # difficult to break if things go wrong in the decoder...\n        # (believe me, I've tried ;-)\n\n        self._mode = self.png.im_mode\n        self._size = self.png.im_size\n        self.info = self.png.im_info\n        self._text = None\n        self.tile = self.png.im_tile\n        self.custom_mimetype = self.png.im_custom_mimetype\n        self.n_frames = self.png.im_n_frames or 1\n        self.default_image = self.info.get(\"default_image\", False)\n\n        if self.png.im_palette:\n            rawmode, data = self.png.im_palette\n            self.palette = ImagePalette.raw(rawmode, data)\n\n        if cid == b\"fdAT\":\n            self.__prepare_idat = length - 4\n        else:\n            self.__prepare_idat = length  # used by load_prepare()\n\n        if self.png.im_n_frames is not None:\n            self._close_exclusive_fp_after_loading = False\n            self.png.save_rewind()\n            self.__rewind_idat = self.__prepare_idat\n            self.__rewind = self._fp.tell()\n            if self.default_image:\n                # IDAT chunk contains default image and not first animation frame\n                self.n_frames += 1\n            self._seek(0)\n        self.is_animated = self.n_frames > 1\n\n    @property\n    def text(self):\n        # experimental\n        if self._text is None:\n            # iTxt, tEXt and zTXt chunks may appear at the end of the file\n            # So load the file to ensure that they are read\n            if self.is_animated:\n                frame = self.__frame\n                # for APNG, seek to the final frame before loading\n                self.seek(self.n_frames - 1)\n            self.load()\n            if self.is_animated:\n                self.seek(frame)\n        return self._text\n\n    def verify(self):\n        \"\"\"Verify PNG file\"\"\"\n\n        if self.fp is None:\n            msg = \"verify must be called directly after open\"\n            raise RuntimeError(msg)\n\n        # back up to beginning of IDAT block\n        self.fp.seek(self.tile[0][2] - 8)\n\n        self.png.verify()\n        self.png.close()\n\n        if self._exclusive_fp:\n            self.fp.close()\n        self.fp = None\n\n    def seek(self, frame):\n        if not self._seek_check(frame):\n            return\n        if frame < self.__frame:\n            self._seek(0, True)\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 APNG file\"\n                raise EOFError(msg) from e\n\n    def _seek(self, frame, rewind=False):\n        if frame == 0:\n            if rewind:\n                self._fp.seek(self.__rewind)\n                self.png.rewind()\n                self.__prepare_idat = self.__rewind_idat\n                self.im = None\n                if self.pyaccess:\n                    self.pyaccess = None\n                self.info = self.png.im_info\n                self.tile = self.png.im_tile\n                self.fp = self._fp\n            self._prev_im = None\n            self.dispose = None\n            self.default_image = self.info.get(\"default_image\", False)\n            self.dispose_op = self.info.get(\"disposal\")\n            self.blend_op = self.info.get(\"blend\")\n            self.dispose_extent = self.info.get(\"bbox\")\n            self.__frame = 0\n        else:\n            if frame != self.__frame + 1:\n                msg = f\"cannot seek to frame {frame}\"\n                raise ValueError(msg)\n\n            # ensure previous frame was loaded\n            self.load()\n\n            if self.dispose:\n                self.im.paste(self.dispose, self.dispose_extent)\n            self._prev_im = self.im.copy()\n\n            self.fp = self._fp\n\n            # advance to the next frame\n            if self.__prepare_idat:\n                ImageFile._safe_read(self.fp, self.__prepare_idat)\n                self.__prepare_idat = 0\n            frame_start = False\n            while True:\n                self.fp.read(4)  # CRC\n\n                try:\n                    cid, pos, length = self.png.read()\n                except (struct.error, SyntaxError):\n                    break\n\n                if cid == b\"IEND\":\n                    msg = \"No more images in APNG file\"\n                    raise EOFError(msg)\n                if cid == b\"fcTL\":\n                    if frame_start:\n                        # there must be at least one fdAT chunk between fcTL chunks\n                        msg = \"APNG missing frame data\"\n                        raise SyntaxError(msg)\n                    frame_start = True\n\n                try:\n                    self.png.call(cid, pos, length)\n                except UnicodeDecodeError:\n                    break\n                except EOFError:\n                    if cid == b\"fdAT\":\n                        length -= 4\n                        if frame_start:\n                            self.__prepare_idat = length\n                            break\n                    ImageFile._safe_read(self.fp, length)\n                except AttributeError:\n                    logger.debug(\"%r %s %s (unknown)\", cid, pos, length)\n                    ImageFile._safe_read(self.fp, length)\n\n            self.__frame = frame\n            self.tile = self.png.im_tile\n            self.dispose_op = self.info.get(\"disposal\")\n            self.blend_op = self.info.get(\"blend\")\n            self.dispose_extent = self.info.get(\"bbox\")\n\n            if not self.tile:\n                msg = \"image not found in APNG frame\"\n                raise EOFError(msg)\n\n        # setup frame disposal (actual disposal done when needed in the next _seek())\n        if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:\n            self.dispose_op = Disposal.OP_BACKGROUND\n\n        if self.dispose_op == Disposal.OP_PREVIOUS:\n            self.dispose = self._prev_im.copy()\n            self.dispose = self._crop(self.dispose, self.dispose_extent)\n        elif self.dispose_op == Disposal.OP_BACKGROUND:\n            self.dispose = Image.core.fill(self.mode, self.size)\n            self.dispose = self._crop(self.dispose, self.dispose_extent)\n        else:\n            self.dispose = None\n\n    def tell(self):\n        return self.__frame\n\n    def load_prepare(self):\n        \"\"\"internal: prepare to read PNG file\"\"\"\n\n        if self.info.get(\"interlace\"):\n            self.decoderconfig = self.decoderconfig + (1,)\n\n        self.__idat = self.__prepare_idat  # used by load_read()\n        ImageFile.ImageFile.load_prepare(self)\n\n    def load_read(self, read_bytes):\n        \"\"\"internal: read more image data\"\"\"\n\n        while self.__idat == 0:\n            # end of chunk, skip forward to next one\n\n            self.fp.read(4)  # CRC\n\n            cid, pos, length = self.png.read()\n\n            if cid not in [b\"IDAT\", b\"DDAT\", b\"fdAT\"]:\n                self.png.push(cid, pos, length)\n                return b\"\"\n\n            if cid == b\"fdAT\":\n                try:\n                    self.png.call(cid, pos, length)\n                except EOFError:\n                    pass\n                self.__idat = length - 4  # sequence_num has already been read\n            else:\n                self.__idat = length  # empty chunks are allowed\n\n        # read more data from this chunk\n        if read_bytes <= 0:\n            read_bytes = self.__idat\n        else:\n            read_bytes = min(read_bytes, self.__idat)\n\n        self.__idat = self.__idat - read_bytes\n\n        return self.fp.read(read_bytes)\n\n    def load_end(self):\n        \"\"\"internal: finished reading image data\"\"\"\n        if self.__idat != 0:\n            self.fp.read(self.__idat)\n        while True:\n            self.fp.read(4)  # CRC\n\n            try:\n                cid, pos, length = self.png.read()\n            except (struct.error, SyntaxError):\n                break\n\n            if cid == b\"IEND\":\n                break\n            elif cid == b\"fcTL\" and self.is_animated:\n                # start of the next frame, stop reading\n                self.__prepare_idat = 0\n                self.png.push(cid, pos, length)\n                break\n\n            try:\n                self.png.call(cid, pos, length)\n            except UnicodeDecodeError:\n                break\n            except EOFError:\n                if cid == b\"fdAT\":\n                    length -= 4\n                ImageFile._safe_read(self.fp, length)\n            except AttributeError:\n                logger.debug(\"%r %s %s (unknown)\", cid, pos, length)\n                s = ImageFile._safe_read(self.fp, length)\n                if cid[1:2].islower():\n                    self.private_chunks.append((cid, s, True))\n        self._text = self.png.im_text\n        if not self.is_animated:\n            self.png.close()\n            self.png = None\n        else:\n            if self._prev_im and self.blend_op == Blend.OP_OVER:\n                updated = self._crop(self.im, self.dispose_extent)\n                if self.im.mode == \"RGB\" and \"transparency\" in self.info:\n                    mask = updated.convert_transparent(\n                        \"RGBA\", self.info[\"transparency\"]\n                    )\n                else:\n                    mask = updated.convert(\"RGBA\")\n                self._prev_im.paste(updated, self.dispose_extent, mask)\n                self.im = self._prev_im\n                if self.pyaccess:\n                    self.pyaccess = None\n\n    def _getexif(self):\n        if \"exif\" not in self.info:\n            self.load()\n        if \"exif\" not in self.info and \"Raw profile type exif\" not in self.info:\n            return None\n        return self.getexif()._get_merged_dict()\n\n    def getexif(self):\n        if \"exif\" not in self.info:\n            self.load()\n\n        return super().getexif()\n\n    def getxmp(self):\n        \"\"\"\n        Returns a dictionary containing the XMP tags.\n        Requires defusedxml to be installed.\n\n        :returns: XMP tags in a dictionary.\n        \"\"\"\n        return (\n            self._getxmp(self.info[\"XML:com.adobe.xmp\"])\n            if \"XML:com.adobe.xmp\" in self.info\n            else {}\n        )\n\n\n# --------------------------------------------------------------------\n# PNG writer\n\n_OUTMODES = {\n    # supported PIL modes, and corresponding rawmodes/bits/color combinations\n    \"1\": (\"1\", b\"\\x01\\x00\"),\n    \"L;1\": (\"L;1\", b\"\\x01\\x00\"),\n    \"L;2\": (\"L;2\", b\"\\x02\\x00\"),\n    \"L;4\": (\"L;4\", b\"\\x04\\x00\"),\n    \"L\": (\"L\", b\"\\x08\\x00\"),\n    \"LA\": (\"LA\", b\"\\x08\\x04\"),\n    \"I\": (\"I;16B\", b\"\\x10\\x00\"),\n    \"I;16\": (\"I;16B\", b\"\\x10\\x00\"),\n    \"I;16B\": (\"I;16B\", b\"\\x10\\x00\"),\n    \"P;1\": (\"P;1\", b\"\\x01\\x03\"),\n    \"P;2\": (\"P;2\", b\"\\x02\\x03\"),\n    \"P;4\": (\"P;4\", b\"\\x04\\x03\"),\n    \"P\": (\"P\", b\"\\x08\\x03\"),\n    \"RGB\": (\"RGB\", b\"\\x08\\x02\"),\n    \"RGBA\": (\"RGBA\", b\"\\x08\\x06\"),\n}\n\n\ndef putchunk(fp, cid, *data):\n    \"\"\"Write a PNG chunk (including CRC field)\"\"\"\n\n    data = b\"\".join(data)\n\n    fp.write(o32(len(data)) + cid)\n    fp.write(data)\n    crc = _crc32(data, _crc32(cid))\n    fp.write(o32(crc))\n\n\nclass _idat:\n    # wrap output from the encoder in IDAT chunks\n\n    def __init__(self, fp, chunk):\n        self.fp = fp\n        self.chunk = chunk\n\n    def write(self, data):\n        self.chunk(self.fp, b\"IDAT\", data)\n\n\nclass _fdat:\n    # wrap encoder output in fdAT chunks\n\n    def __init__(self, fp, chunk, seq_num):\n        self.fp = fp\n        self.chunk = chunk\n        self.seq_num = seq_num\n\n    def write(self, data):\n        self.chunk(self.fp, b\"fdAT\", o32(self.seq_num), data)\n        self.seq_num += 1\n\n\ndef _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images):\n    duration = im.encoderinfo.get(\"duration\", im.info.get(\"duration\", 0))\n    loop = im.encoderinfo.get(\"loop\", im.info.get(\"loop\", 0))\n    disposal = im.encoderinfo.get(\"disposal\", im.info.get(\"disposal\", Disposal.OP_NONE))\n    blend = im.encoderinfo.get(\"blend\", im.info.get(\"blend\", Blend.OP_SOURCE))\n\n    if default_image:\n        chain = itertools.chain(append_images)\n    else:\n        chain = itertools.chain([im], append_images)\n\n    im_frames = []\n    frame_count = 0\n    for im_seq in chain:\n        for im_frame in ImageSequence.Iterator(im_seq):\n            if im_frame.mode == rawmode:\n                im_frame = im_frame.copy()\n            else:\n                im_frame = im_frame.convert(rawmode)\n            encoderinfo = im.encoderinfo.copy()\n            if isinstance(duration, (list, tuple)):\n                encoderinfo[\"duration\"] = duration[frame_count]\n            if isinstance(disposal, (list, tuple)):\n                encoderinfo[\"disposal\"] = disposal[frame_count]\n            if isinstance(blend, (list, tuple)):\n                encoderinfo[\"blend\"] = blend[frame_count]\n            frame_count += 1\n\n            if im_frames:\n                previous = im_frames[-1]\n                prev_disposal = previous[\"encoderinfo\"].get(\"disposal\")\n                prev_blend = previous[\"encoderinfo\"].get(\"blend\")\n                if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:\n                    prev_disposal = Disposal.OP_BACKGROUND\n\n                if prev_disposal == Disposal.OP_BACKGROUND:\n                    base_im = previous[\"im\"].copy()\n                    dispose = Image.core.fill(\"RGBA\", im.size, (0, 0, 0, 0))\n                    bbox = previous[\"bbox\"]\n                    if bbox:\n                        dispose = dispose.crop(bbox)\n                    else:\n                        bbox = (0, 0) + im.size\n                    base_im.paste(dispose, bbox)\n                elif prev_disposal == Disposal.OP_PREVIOUS:\n                    base_im = im_frames[-2][\"im\"]\n                else:\n                    base_im = previous[\"im\"]\n                delta = ImageChops.subtract_modulo(\n                    im_frame.convert(\"RGBA\"), base_im.convert(\"RGBA\")\n                )\n                bbox = delta.getbbox(alpha_only=False)\n                if (\n                    not bbox\n                    and prev_disposal == encoderinfo.get(\"disposal\")\n                    and prev_blend == encoderinfo.get(\"blend\")\n                ):\n                    previous[\"encoderinfo\"][\"duration\"] += encoderinfo.get(\n                        \"duration\", duration\n                    )\n                    continue\n            else:\n                bbox = None\n            if \"duration\" not in encoderinfo:\n                encoderinfo[\"duration\"] = duration\n            im_frames.append({\"im\": im_frame, \"bbox\": bbox, \"encoderinfo\": encoderinfo})\n\n    # animation control\n    chunk(\n        fp,\n        b\"acTL\",\n        o32(len(im_frames)),  # 0: num_frames\n        o32(loop),  # 4: num_plays\n    )\n\n    # default image IDAT (if it exists)\n    if default_image:\n        if im.mode != rawmode:\n            im = im.convert(rawmode)\n        ImageFile._save(im, _idat(fp, chunk), [(\"zip\", (0, 0) + im.size, 0, rawmode)])\n\n    seq_num = 0\n    for frame, frame_data in enumerate(im_frames):\n        im_frame = frame_data[\"im\"]\n        if not frame_data[\"bbox\"]:\n            bbox = (0, 0) + im_frame.size\n        else:\n            bbox = frame_data[\"bbox\"]\n            im_frame = im_frame.crop(bbox)\n        size = im_frame.size\n        encoderinfo = frame_data[\"encoderinfo\"]\n        frame_duration = int(round(encoderinfo[\"duration\"]))\n        frame_disposal = encoderinfo.get(\"disposal\", disposal)\n        frame_blend = encoderinfo.get(\"blend\", blend)\n        # frame control\n        chunk(\n            fp,\n            b\"fcTL\",\n            o32(seq_num),  # sequence_number\n            o32(size[0]),  # width\n            o32(size[1]),  # height\n            o32(bbox[0]),  # x_offset\n            o32(bbox[1]),  # y_offset\n            o16(frame_duration),  # delay_numerator\n            o16(1000),  # delay_denominator\n            o8(frame_disposal),  # dispose_op\n            o8(frame_blend),  # blend_op\n        )\n        seq_num += 1\n        # frame data\n        if frame == 0 and not default_image:\n            # first frame must be in IDAT chunks for backwards compatibility\n            ImageFile._save(\n                im_frame,\n                _idat(fp, chunk),\n                [(\"zip\", (0, 0) + im_frame.size, 0, rawmode)],\n            )\n        else:\n            fdat_chunks = _fdat(fp, chunk, seq_num)\n            ImageFile._save(\n                im_frame,\n                fdat_chunks,\n                [(\"zip\", (0, 0) + im_frame.size, 0, rawmode)],\n            )\n            seq_num = fdat_chunks.seq_num\n\n\ndef _save_all(im, fp, filename):\n    _save(im, fp, filename, save_all=True)\n\n\ndef _save(im, fp, filename, chunk=putchunk, save_all=False):\n    # save an image to disk (called by the save method)\n\n    if save_all:\n        default_image = im.encoderinfo.get(\n            \"default_image\", im.info.get(\"default_image\")\n        )\n        modes = set()\n        append_images = im.encoderinfo.get(\"append_images\", [])\n        for im_seq in itertools.chain([im], append_images):\n            for im_frame in ImageSequence.Iterator(im_seq):\n                modes.add(im_frame.mode)\n        for mode in (\"RGBA\", \"RGB\", \"P\"):\n            if mode in modes:\n                break\n        else:\n            mode = modes.pop()\n    else:\n        mode = im.mode\n\n    if mode == \"P\":\n        #\n        # attempt to minimize storage requirements for palette images\n        if \"bits\" in im.encoderinfo:\n            # number of bits specified by user\n            colors = min(1 << im.encoderinfo[\"bits\"], 256)\n        else:\n            # check palette contents\n            if im.palette:\n                colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1)\n            else:\n                colors = 256\n\n        if colors <= 16:\n            if colors <= 2:\n                bits = 1\n            elif colors <= 4:\n                bits = 2\n            else:\n                bits = 4\n            mode = f\"{mode};{bits}\"\n\n    # encoder options\n    im.encoderconfig = (\n        im.encoderinfo.get(\"optimize\", False),\n        im.encoderinfo.get(\"compress_level\", -1),\n        im.encoderinfo.get(\"compress_type\", -1),\n        im.encoderinfo.get(\"dictionary\", b\"\"),\n    )\n\n    # get the corresponding PNG mode\n    try:\n        rawmode, mode = _OUTMODES[mode]\n    except KeyError as e:\n        msg = f\"cannot write mode {mode} as PNG\"\n        raise OSError(msg) from e\n\n    #\n    # write minimal PNG file\n\n    fp.write(_MAGIC)\n\n    chunk(\n        fp,\n        b\"IHDR\",\n        o32(im.size[0]),  # 0: size\n        o32(im.size[1]),\n        mode,  # 8: depth/type\n        b\"\\0\",  # 10: compression\n        b\"\\0\",  # 11: filter category\n        b\"\\0\",  # 12: interlace flag\n    )\n\n    chunks = [b\"cHRM\", b\"gAMA\", b\"sBIT\", b\"sRGB\", b\"tIME\"]\n\n    icc = im.encoderinfo.get(\"icc_profile\", im.info.get(\"icc_profile\"))\n    if icc:\n        # ICC profile\n        # according to PNG spec, the iCCP chunk contains:\n        # Profile name  1-79 bytes (character string)\n        # Null separator        1 byte (null character)\n        # Compression method    1 byte (0)\n        # Compressed profile    n bytes (zlib with deflate compression)\n        name = b\"ICC Profile\"\n        data = name + b\"\\0\\0\" + zlib.compress(icc)\n        chunk(fp, b\"iCCP\", data)\n\n        # You must either have sRGB or iCCP.\n        # Disallow sRGB chunks when an iCCP-chunk has been emitted.\n        chunks.remove(b\"sRGB\")\n\n    info = im.encoderinfo.get(\"pnginfo\")\n    if info:\n        chunks_multiple_allowed = [b\"sPLT\", b\"iTXt\", b\"tEXt\", b\"zTXt\"]\n        for info_chunk in info.chunks:\n            cid, data = info_chunk[:2]\n            if cid in chunks:\n                chunks.remove(cid)\n                chunk(fp, cid, data)\n            elif cid in chunks_multiple_allowed:\n                chunk(fp, cid, data)\n            elif cid[1:2].islower():\n                # Private chunk\n                after_idat = info_chunk[2:3]\n                if not after_idat:\n                    chunk(fp, cid, data)\n\n    if im.mode == \"P\":\n        palette_byte_number = colors * 3\n        palette_bytes = im.im.getpalette(\"RGB\")[:palette_byte_number]\n        while len(palette_bytes) < palette_byte_number:\n            palette_bytes += b\"\\0\"\n        chunk(fp, b\"PLTE\", palette_bytes)\n\n    transparency = im.encoderinfo.get(\"transparency\", im.info.get(\"transparency\", None))\n\n    if transparency or transparency == 0:\n        if im.mode == \"P\":\n            # limit to actual palette size\n            alpha_bytes = colors\n            if isinstance(transparency, bytes):\n                chunk(fp, b\"tRNS\", transparency[:alpha_bytes])\n            else:\n                transparency = max(0, min(255, transparency))\n                alpha = b\"\\xFF\" * transparency + b\"\\0\"\n                chunk(fp, b\"tRNS\", alpha[:alpha_bytes])\n        elif im.mode in (\"1\", \"L\", \"I\"):\n            transparency = max(0, min(65535, transparency))\n            chunk(fp, b\"tRNS\", o16(transparency))\n        elif im.mode == \"RGB\":\n            red, green, blue = transparency\n            chunk(fp, b\"tRNS\", o16(red) + o16(green) + o16(blue))\n        else:\n            if \"transparency\" in im.encoderinfo:\n                # don't bother with transparency if it's an RGBA\n                # and it's in the info dict. It's probably just stale.\n                msg = \"cannot use transparency for this mode\"\n                raise OSError(msg)\n    else:\n        if im.mode == \"P\" and im.im.getpalettemode() == \"RGBA\":\n            alpha = im.im.getpalette(\"RGBA\", \"A\")\n            alpha_bytes = colors\n            chunk(fp, b\"tRNS\", alpha[:alpha_bytes])\n\n    dpi = im.encoderinfo.get(\"dpi\")\n    if dpi:\n        chunk(\n            fp,\n            b\"pHYs\",\n            o32(int(dpi[0] / 0.0254 + 0.5)),\n            o32(int(dpi[1] / 0.0254 + 0.5)),\n            b\"\\x01\",\n        )\n\n    if info:\n        chunks = [b\"bKGD\", b\"hIST\"]\n        for info_chunk in info.chunks:\n            cid, data = info_chunk[:2]\n            if cid in chunks:\n                chunks.remove(cid)\n                chunk(fp, cid, data)\n\n    exif = im.encoderinfo.get(\"exif\")\n    if exif:\n        if isinstance(exif, Image.Exif):\n            exif = exif.tobytes(8)\n        if exif.startswith(b\"Exif\\x00\\x00\"):\n            exif = exif[6:]\n        chunk(fp, b\"eXIf\", exif)\n\n    if save_all:\n        _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)\n    else:\n        ImageFile._save(im, _idat(fp, chunk), [(\"zip\", (0, 0) + im.size, 0, rawmode)])\n\n    if info:\n        for info_chunk in info.chunks:\n            cid, data = info_chunk[:2]\n            if cid[1:2].islower():\n                # Private chunk\n                after_idat = info_chunk[2:3]\n                if after_idat:\n                    chunk(fp, cid, data)\n\n    chunk(fp, b\"IEND\", b\"\")\n\n    if hasattr(fp, \"flush\"):\n        fp.flush()\n\n\n# --------------------------------------------------------------------\n# PNG chunk converter\n\n\ndef getchunks(im, **params):\n    \"\"\"Return a list of PNG chunks representing this image.\"\"\"\n\n    class collector:\n        data = []\n\n        def write(self, data):\n            pass\n\n        def append(self, chunk):\n            self.data.append(chunk)\n\n    def append(fp, cid, *data):\n        data = b\"\".join(data)\n        crc = o32(_crc32(data, _crc32(cid)))\n        fp.append((cid, data, crc))\n\n    fp = collector()\n\n    try:\n        im.encoderinfo = params\n        _save(im, fp, None, append)\n    finally:\n        del im.encoderinfo\n\n    return fp.data\n\n\n# --------------------------------------------------------------------\n# Registry\n\nImage.register_open(PngImageFile.format, PngImageFile, _accept)\nImage.register_save(PngImageFile.format, _save)\nImage.register_save_all(PngImageFile.format, _save_all)\n\nImage.register_extensions(PngImageFile.format, [\".png\", \".apng\"])\n\nImage.register_mime(PngImageFile.format, \"image/png\")\n",
            "new_file_content": "#\n# The Python Imaging Library.\n# $Id$\n#\n# PNG support code\n#\n# See \"PNG (Portable Network Graphics) Specification, version 1.0;\n# W3C Recommendation\", 1996-10-01, Thomas Boutell (ed.).\n#\n# history:\n# 1996-05-06 fl   Created (couldn't resist it)\n# 1996-12-14 fl   Upgraded, added read and verify support (0.2)\n# 1996-12-15 fl   Separate PNG stream parser\n# 1996-12-29 fl   Added write support, added getchunks\n# 1996-12-30 fl   Eliminated circular references in decoder (0.3)\n# 1998-07-12 fl   Read/write 16-bit images as mode I (0.4)\n# 2001-02-08 fl   Added transparency support (from Zircon) (0.5)\n# 2001-04-16 fl   Don't close data source in \"open\" method (0.6)\n# 2004-02-24 fl   Don't even pretend to support interlaced files (0.7)\n# 2004-08-31 fl   Do basic sanity check on chunk identifiers (0.8)\n# 2004-09-20 fl   Added PngInfo chunk container\n# 2004-12-18 fl   Added DPI read support (based on code by Niki Spahiev)\n# 2008-08-13 fl   Added tRNS support for RGB images\n# 2009-03-06 fl   Support for preserving ICC profiles (by Florian Hoech)\n# 2009-03-08 fl   Added zTXT support (from Lowell Alleman)\n# 2009-03-29 fl   Read interlaced PNG files (from Conrado Porto Lopes Gouvua)\n#\n# Copyright (c) 1997-2009 by Secret Labs AB\n# Copyright (c) 1996 by Fredrik Lundh\n#\n# See the README file for information on usage and redistribution.\n#\n\nimport itertools\nimport logging\nimport re\nimport struct\nimport warnings\nimport zlib\nfrom enum import IntEnum\n\nfrom . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence\nfrom ._binary import i16be as i16\nfrom ._binary import i32be as i32\nfrom ._binary import o8\nfrom ._binary import o16be as o16\nfrom ._binary import o32be as o32\n\nlogger = logging.getLogger(__name__)\n\nis_cid = re.compile(rb\"\\w\\w\\w\\w\").match\n\n\n_MAGIC = b\"\\211PNG\\r\\n\\032\\n\"\n\n\n_MODES = {\n    # supported bits/color combinations, and corresponding modes/rawmodes\n    # Grayscale\n    (1, 0): (\"1\", \"1\"),\n    (2, 0): (\"L\", \"L;2\"),\n    (4, 0): (\"L\", \"L;4\"),\n    (8, 0): (\"L\", \"L\"),\n    (16, 0): (\"I\", \"I;16B\"),\n    # Truecolour\n    (8, 2): (\"RGB\", \"RGB\"),\n    (16, 2): (\"RGB\", \"RGB;16B\"),\n    # Indexed-colour\n    (1, 3): (\"P\", \"P;1\"),\n    (2, 3): (\"P\", \"P;2\"),\n    (4, 3): (\"P\", \"P;4\"),\n    (8, 3): (\"P\", \"P\"),\n    # Grayscale with alpha\n    (8, 4): (\"LA\", \"LA\"),\n    (16, 4): (\"RGBA\", \"LA;16B\"),  # LA;16B->LA not yet available\n    # Truecolour with alpha\n    (8, 6): (\"RGBA\", \"RGBA\"),\n    (16, 6): (\"RGBA\", \"RGBA;16B\"),\n}\n\n\n_simple_palette = re.compile(b\"^\\xff*\\x00\\xff*$\")\n\nMAX_TEXT_CHUNK = ImageFile.SAFEBLOCK\n\"\"\"\nMaximum decompressed size for a iTXt or zTXt chunk.\nEliminates decompression bombs where compressed chunks can expand 1000x.\nSee :ref:`Text in PNG File Format<png-text>`.\n\"\"\"\nMAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK\n\"\"\"\nSet the maximum total text chunk size.\nSee :ref:`Text in PNG File Format<png-text>`.\n\"\"\"\n\n\n# APNG frame disposal modes\nclass Disposal(IntEnum):\n    OP_NONE = 0\n    \"\"\"\n    No disposal is done on this frame before rendering the next frame.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n    OP_BACKGROUND = 1\n    \"\"\"\n    This frame’s modified region is cleared to fully transparent black before rendering\n    the next frame.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n    OP_PREVIOUS = 2\n    \"\"\"\n    This frame’s modified region is reverted to the previous frame’s contents before\n    rendering the next frame.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n\n\n# APNG frame blend modes\nclass Blend(IntEnum):\n    OP_SOURCE = 0\n    \"\"\"\n    All color components of this frame, including alpha, overwrite the previous output\n    image contents.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n    OP_OVER = 1\n    \"\"\"\n    This frame should be alpha composited with the previous output image contents.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n\n\ndef _safe_zlib_decompress(s):\n    dobj = zlib.decompressobj()\n    plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)\n    if dobj.unconsumed_tail:\n        msg = \"Decompressed Data Too Large\"\n        raise ValueError(msg)\n    return plaintext\n\n\ndef _crc32(data, seed=0):\n    return zlib.crc32(data, seed) & 0xFFFFFFFF\n\n\n# --------------------------------------------------------------------\n# Support classes.  Suitable for PNG and related formats like MNG etc.\n\n\nclass ChunkStream:\n    def __init__(self, fp):\n        self.fp = fp\n        self.queue = []\n\n    def read(self):\n        \"\"\"Fetch a new chunk. Returns header information.\"\"\"\n        cid = None\n\n        if self.queue:\n            cid, pos, length = self.queue.pop()\n            self.fp.seek(pos)\n        else:\n            s = self.fp.read(8)\n            cid = s[4:]\n            pos = self.fp.tell()\n            length = i32(s)\n\n        if not is_cid(cid):\n            if not ImageFile.LOAD_TRUNCATED_IMAGES:\n                msg = f\"broken PNG file (chunk {repr(cid)})\"\n                raise SyntaxError(msg)\n\n        return cid, pos, length\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, *args):\n        self.close()\n\n    def close(self):\n        self.queue = self.fp = None\n\n    def push(self, cid, pos, length):\n        self.queue.append((cid, pos, length))\n\n    def call(self, cid, pos, length):\n        \"\"\"Call the appropriate chunk handler\"\"\"\n\n        logger.debug(\"STREAM %r %s %s\", cid, pos, length)\n        return getattr(self, \"chunk_\" + cid.decode(\"ascii\"))(pos, length)\n\n    def crc(self, cid, data):\n        \"\"\"Read and verify checksum\"\"\"\n\n        # Skip CRC checks for ancillary chunks if allowed to load truncated\n        # images\n        # 5th byte of first char is 1 [specs, section 5.4]\n        if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1):\n            self.crc_skip(cid, data)\n            return\n\n        try:\n            crc1 = _crc32(data, _crc32(cid))\n            crc2 = i32(self.fp.read(4))\n            if crc1 != crc2:\n                msg = f\"broken PNG file (bad header checksum in {repr(cid)})\"\n                raise SyntaxError(msg)\n        except struct.error as e:\n            msg = f\"broken PNG file (incomplete checksum in {repr(cid)})\"\n            raise SyntaxError(msg) from e\n\n    def crc_skip(self, cid, data):\n        \"\"\"Read checksum\"\"\"\n\n        self.fp.read(4)\n\n    def verify(self, endchunk=b\"IEND\"):\n        # Simple approach; just calculate checksum for all remaining\n        # blocks.  Must be called directly after open.\n\n        cids = []\n\n        while True:\n            try:\n                cid, pos, length = self.read()\n            except struct.error as e:\n                msg = \"truncated PNG file\"\n                raise OSError(msg) from e\n\n            if cid == endchunk:\n                break\n            self.crc(cid, ImageFile._safe_read(self.fp, length))\n            cids.append(cid)\n\n        return cids\n\n\nclass iTXt(str):\n    \"\"\"\n    Subclass of string to allow iTXt chunks to look like strings while\n    keeping their extra information\n\n    \"\"\"\n\n    @staticmethod\n    def __new__(cls, text, lang=None, tkey=None):\n        \"\"\"\n        :param cls: the class to use when creating the instance\n        :param text: value for this key\n        :param lang: language code\n        :param tkey: UTF-8 version of the key name\n        \"\"\"\n\n        self = str.__new__(cls, text)\n        self.lang = lang\n        self.tkey = tkey\n        return self\n\n\nclass PngInfo:\n    \"\"\"\n    PNG chunk container (for use with save(pnginfo=))\n\n    \"\"\"\n\n    def __init__(self):\n        self.chunks = []\n\n    def add(self, cid, data, after_idat=False):\n        \"\"\"Appends an arbitrary chunk. Use with caution.\n\n        :param cid: a byte string, 4 bytes long.\n        :param data: a byte string of the encoded data\n        :param after_idat: for use with private chunks. Whether the chunk\n                           should be written after IDAT\n\n        \"\"\"\n\n        chunk = [cid, data]\n        if after_idat:\n            chunk.append(True)\n        self.chunks.append(tuple(chunk))\n\n    def add_itxt(self, key, value, lang=\"\", tkey=\"\", zip=False):\n        \"\"\"Appends an iTXt chunk.\n\n        :param key: latin-1 encodable text key name\n        :param value: value for this key\n        :param lang: language code\n        :param tkey: UTF-8 version of the key name\n        :param zip: compression flag\n\n        \"\"\"\n\n        if not isinstance(key, bytes):\n            key = key.encode(\"latin-1\", \"strict\")\n        if not isinstance(value, bytes):\n            value = value.encode(\"utf-8\", \"strict\")\n        if not isinstance(lang, bytes):\n            lang = lang.encode(\"utf-8\", \"strict\")\n        if not isinstance(tkey, bytes):\n            tkey = tkey.encode(\"utf-8\", \"strict\")\n\n        if zip:\n            self.add(\n                b\"iTXt\",\n                key + b\"\\0\\x01\\0\" + lang + b\"\\0\" + tkey + b\"\\0\" + zlib.compress(value),\n            )\n        else:\n            self.add(b\"iTXt\", key + b\"\\0\\0\\0\" + lang + b\"\\0\" + tkey + b\"\\0\" + value)\n\n    def add_text(self, key, value, zip=False):\n        \"\"\"Appends a text chunk.\n\n        :param key: latin-1 encodable text key name\n        :param value: value for this key, text or an\n           :py:class:`PIL.PngImagePlugin.iTXt` instance\n        :param zip: compression flag\n\n        \"\"\"\n        if isinstance(value, iTXt):\n            return self.add_itxt(key, value, value.lang, value.tkey, zip=zip)\n\n        # The tEXt chunk stores latin-1 text\n        if not isinstance(value, bytes):\n            try:\n                value = value.encode(\"latin-1\", \"strict\")\n            except UnicodeError:\n                return self.add_itxt(key, value, zip=zip)\n\n        if not isinstance(key, bytes):\n            key = key.encode(\"latin-1\", \"strict\")\n\n        if zip:\n            self.add(b\"zTXt\", key + b\"\\0\\0\" + zlib.compress(value))\n        else:\n            self.add(b\"tEXt\", key + b\"\\0\" + value)\n\n\n# --------------------------------------------------------------------\n# PNG image stream (IHDR/IEND)\n\n\nclass PngStream(ChunkStream):\n    def __init__(self, fp):\n        super().__init__(fp)\n\n        # local copies of Image attributes\n        self.im_info = {}\n        self.im_text = {}\n        self.im_size = (0, 0)\n        self.im_mode = None\n        self.im_tile = None\n        self.im_palette = None\n        self.im_custom_mimetype = None\n        self.im_n_frames = None\n        self._seq_num = None\n        self.rewind_state = None\n\n        self.text_memory = 0\n\n    def check_text_memory(self, chunklen):\n        self.text_memory += chunklen\n        if self.text_memory > MAX_TEXT_MEMORY:\n            msg = (\n                \"Too much memory used in text chunks: \"\n                f\"{self.text_memory}>MAX_TEXT_MEMORY\"\n            )\n            raise ValueError(msg)\n\n    def save_rewind(self):\n        self.rewind_state = {\n            \"info\": self.im_info.copy(),\n            \"tile\": self.im_tile,\n            \"seq_num\": self._seq_num,\n        }\n\n    def rewind(self):\n        self.im_info = self.rewind_state[\"info\"]\n        self.im_tile = self.rewind_state[\"tile\"]\n        self._seq_num = self.rewind_state[\"seq_num\"]\n\n    def chunk_iCCP(self, pos, length):\n        # ICC profile\n        s = ImageFile._safe_read(self.fp, length)\n        # according to PNG spec, the iCCP chunk contains:\n        # Profile name  1-79 bytes (character string)\n        # Null separator        1 byte (null character)\n        # Compression method    1 byte (0)\n        # Compressed profile    n bytes (zlib with deflate compression)\n        i = s.find(b\"\\0\")\n        logger.debug(\"iCCP profile name %r\", s[:i])\n        logger.debug(\"Compression method %s\", s[i])\n        comp_method = s[i]\n        if comp_method != 0:\n            msg = f\"Unknown compression method {comp_method} in iCCP chunk\"\n            raise SyntaxError(msg)\n        try:\n            icc_profile = _safe_zlib_decompress(s[i + 2 :])\n        except ValueError:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                icc_profile = None\n            else:\n                raise\n        except zlib.error:\n            icc_profile = None  # FIXME\n        self.im_info[\"icc_profile\"] = icc_profile\n        return s\n\n    def chunk_IHDR(self, pos, length):\n        # image header\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 13:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"Truncated IHDR chunk\"\n            raise ValueError(msg)\n        self.im_size = i32(s, 0), i32(s, 4)\n        try:\n            self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])]\n        except Exception:\n            pass\n        if s[12]:\n            self.im_info[\"interlace\"] = 1\n        if s[11]:\n            msg = \"unknown filter category\"\n            raise SyntaxError(msg)\n        return s\n\n    def chunk_IDAT(self, pos, length):\n        # image data\n        if \"bbox\" in self.im_info:\n            tile = [(\"zip\", self.im_info[\"bbox\"], pos, self.im_rawmode)]\n        else:\n            if self.im_n_frames is not None:\n                self.im_info[\"default_image\"] = True\n            tile = [(\"zip\", (0, 0) + self.im_size, pos, self.im_rawmode)]\n        self.im_tile = tile\n        self.im_idat = length\n        msg = \"image data found\"\n        raise EOFError(msg)\n\n    def chunk_IEND(self, pos, length):\n        msg = \"end of PNG image\"\n        raise EOFError(msg)\n\n    def chunk_PLTE(self, pos, length):\n        # palette\n        s = ImageFile._safe_read(self.fp, length)\n        if self.im_mode == \"P\":\n            self.im_palette = \"RGB\", s\n        return s\n\n    def chunk_tRNS(self, pos, length):\n        # transparency\n        s = ImageFile._safe_read(self.fp, length)\n        if self.im_mode == \"P\":\n            if _simple_palette.match(s):\n                # tRNS contains only one full-transparent entry,\n                # other entries are full opaque\n                i = s.find(b\"\\0\")\n                if i >= 0:\n                    self.im_info[\"transparency\"] = i\n            else:\n                # otherwise, we have a byte string with one alpha value\n                # for each palette entry\n                self.im_info[\"transparency\"] = s\n        elif self.im_mode in (\"1\", \"L\", \"I\"):\n            self.im_info[\"transparency\"] = i16(s)\n        elif self.im_mode == \"RGB\":\n            self.im_info[\"transparency\"] = i16(s), i16(s, 2), i16(s, 4)\n        return s\n\n    def chunk_gAMA(self, pos, length):\n        # gamma setting\n        s = ImageFile._safe_read(self.fp, length)\n        self.im_info[\"gamma\"] = i32(s) / 100000.0\n        return s\n\n    def chunk_cHRM(self, pos, length):\n        # chromaticity, 8 unsigned ints, actual value is scaled by 100,000\n        # WP x,y, Red x,y, Green x,y Blue x,y\n\n        s = ImageFile._safe_read(self.fp, length)\n        raw_vals = struct.unpack(\">%dI\" % (len(s) // 4), s)\n        self.im_info[\"chromaticity\"] = tuple(elt / 100000.0 for elt in raw_vals)\n        return s\n\n    def chunk_sRGB(self, pos, length):\n        # srgb rendering intent, 1 byte\n        # 0 perceptual\n        # 1 relative colorimetric\n        # 2 saturation\n        # 3 absolute colorimetric\n\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 1:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"Truncated sRGB chunk\"\n            raise ValueError(msg)\n        self.im_info[\"srgb\"] = s[0]\n        return s\n\n    def chunk_pHYs(self, pos, length):\n        # pixels per unit\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 9:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"Truncated pHYs chunk\"\n            raise ValueError(msg)\n        px, py = i32(s, 0), i32(s, 4)\n        unit = s[8]\n        if unit == 1:  # meter\n            dpi = px * 0.0254, py * 0.0254\n            self.im_info[\"dpi\"] = dpi\n        elif unit == 0:\n            self.im_info[\"aspect\"] = px, py\n        return s\n\n    def chunk_tEXt(self, pos, length):\n        # text\n        s = ImageFile._safe_read(self.fp, length)\n        try:\n            k, v = s.split(b\"\\0\", 1)\n        except ValueError:\n            # fallback for broken tEXt tags\n            k = s\n            v = b\"\"\n        if k:\n            k = k.decode(\"latin-1\", \"strict\")\n            v_str = v.decode(\"latin-1\", \"replace\")\n\n            self.im_info[k] = v if k == \"exif\" else v_str\n            self.im_text[k] = v_str\n            self.check_text_memory(len(v_str))\n\n        return s\n\n    def chunk_zTXt(self, pos, length):\n        # compressed text\n        s = ImageFile._safe_read(self.fp, length)\n        try:\n            k, v = s.split(b\"\\0\", 1)\n        except ValueError:\n            k = s\n            v = b\"\"\n        if v:\n            comp_method = v[0]\n        else:\n            comp_method = 0\n        if comp_method != 0:\n            msg = f\"Unknown compression method {comp_method} in zTXt chunk\"\n            raise SyntaxError(msg)\n        try:\n            v = _safe_zlib_decompress(v[1:])\n        except ValueError:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                v = b\"\"\n            else:\n                raise\n        except zlib.error:\n            v = b\"\"\n\n        if k:\n            k = k.decode(\"latin-1\", \"strict\")\n            v = v.decode(\"latin-1\", \"replace\")\n\n            self.im_info[k] = self.im_text[k] = v\n            self.check_text_memory(len(v))\n\n        return s\n\n    def chunk_iTXt(self, pos, length):\n        # international text\n        r = s = ImageFile._safe_read(self.fp, length)\n        try:\n            k, r = r.split(b\"\\0\", 1)\n        except ValueError:\n            return s\n        if len(r) < 2:\n            return s\n        cf, cm, r = r[0], r[1], r[2:]\n        try:\n            lang, tk, v = r.split(b\"\\0\", 2)\n        except ValueError:\n            return s\n        if cf != 0:\n            if cm == 0:\n                try:\n                    v = _safe_zlib_decompress(v)\n                except ValueError:\n                    if ImageFile.LOAD_TRUNCATED_IMAGES:\n                        return s\n                    else:\n                        raise\n                except zlib.error:\n                    return s\n            else:\n                return s\n        try:\n            k = k.decode(\"latin-1\", \"strict\")\n            lang = lang.decode(\"utf-8\", \"strict\")\n            tk = tk.decode(\"utf-8\", \"strict\")\n            v = v.decode(\"utf-8\", \"strict\")\n        except UnicodeError:\n            return s\n\n        self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk)\n        self.check_text_memory(len(v))\n\n        return s\n\n    def chunk_eXIf(self, pos, length):\n        s = ImageFile._safe_read(self.fp, length)\n        self.im_info[\"exif\"] = b\"Exif\\x00\\x00\" + s\n        return s\n\n    # APNG chunks\n    def chunk_acTL(self, pos, length):\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 8:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"APNG contains truncated acTL chunk\"\n            raise ValueError(msg)\n        if self.im_n_frames is not None:\n            self.im_n_frames = None\n            warnings.warn(\"Invalid APNG, will use default PNG image if possible\")\n            return s\n        n_frames = i32(s)\n        if n_frames == 0 or n_frames > 0x80000000:\n            warnings.warn(\"Invalid APNG, will use default PNG image if possible\")\n            return s\n        self.im_n_frames = n_frames\n        self.im_info[\"loop\"] = i32(s, 4)\n        self.im_custom_mimetype = \"image/apng\"\n        return s\n\n    def chunk_fcTL(self, pos, length):\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 26:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"APNG contains truncated fcTL chunk\"\n            raise ValueError(msg)\n        seq = i32(s)\n        if (self._seq_num is None and seq != 0) or (\n            self._seq_num is not None and self._seq_num != seq - 1\n        ):\n            msg = \"APNG contains frame sequence errors\"\n            raise SyntaxError(msg)\n        self._seq_num = seq\n        width, height = i32(s, 4), i32(s, 8)\n        px, py = i32(s, 12), i32(s, 16)\n        im_w, im_h = self.im_size\n        if px + width > im_w or py + height > im_h:\n            msg = \"APNG contains invalid frames\"\n            raise SyntaxError(msg)\n        self.im_info[\"bbox\"] = (px, py, px + width, py + height)\n        delay_num, delay_den = i16(s, 20), i16(s, 22)\n        if delay_den == 0:\n            delay_den = 100\n        self.im_info[\"duration\"] = float(delay_num) / float(delay_den) * 1000\n        self.im_info[\"disposal\"] = s[24]\n        self.im_info[\"blend\"] = s[25]\n        return s\n\n    def chunk_fdAT(self, pos, length):\n        if length < 4:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                s = ImageFile._safe_read(self.fp, length)\n                return s\n            msg = \"APNG contains truncated fDAT chunk\"\n            raise ValueError(msg)\n        s = ImageFile._safe_read(self.fp, 4)\n        seq = i32(s)\n        if self._seq_num != seq - 1:\n            msg = \"APNG contains frame sequence errors\"\n            raise SyntaxError(msg)\n        self._seq_num = seq\n        return self.chunk_IDAT(pos + 4, length - 4)\n\n\n# --------------------------------------------------------------------\n# PNG reader\n\n\ndef _accept(prefix):\n    return prefix[:8] == _MAGIC\n\n\n##\n# Image plugin for PNG images.\n\n\nclass PngImageFile(ImageFile.ImageFile):\n    format = \"PNG\"\n    format_description = \"Portable network graphics\"\n\n    def _open(self):\n        if not _accept(self.fp.read(8)):\n            msg = \"not a PNG file\"\n            raise SyntaxError(msg)\n        self._fp = self.fp\n        self.__frame = 0\n\n        #\n        # Parse headers up to the first IDAT or fDAT chunk\n\n        self.private_chunks = []\n        self.png = PngStream(self.fp)\n\n        while True:\n            #\n            # get next chunk\n\n            cid, pos, length = self.png.read()\n\n            try:\n                s = self.png.call(cid, pos, length)\n            except EOFError:\n                break\n            except AttributeError:\n                logger.debug(\"%r %s %s (unknown)\", cid, pos, length)\n                s = ImageFile._safe_read(self.fp, length)\n                if cid[1:2].islower():\n                    self.private_chunks.append((cid, s))\n\n            self.png.crc(cid, s)\n\n        #\n        # Copy relevant attributes from the PngStream.  An alternative\n        # would be to let the PngStream class modify these attributes\n        # directly, but that introduces circular references which are\n        # difficult to break if things go wrong in the decoder...\n        # (believe me, I've tried ;-)\n\n        self._mode = self.png.im_mode\n        self._size = self.png.im_size\n        self.info = self.png.im_info\n        self._text = None\n        self.tile = self.png.im_tile\n        self.custom_mimetype = self.png.im_custom_mimetype\n        self.n_frames = self.png.im_n_frames or 1\n        self.default_image = self.info.get(\"default_image\", False)\n\n        if self.png.im_palette:\n            rawmode, data = self.png.im_palette\n            self.palette = ImagePalette.raw(rawmode, data)\n\n        if cid == b\"fdAT\":\n            self.__prepare_idat = length - 4\n        else:\n            self.__prepare_idat = length  # used by load_prepare()\n\n        if self.png.im_n_frames is not None:\n            self._close_exclusive_fp_after_loading = False\n            self.png.save_rewind()\n            self.__rewind_idat = self.__prepare_idat\n            self.__rewind = self._fp.tell()\n            if self.default_image:\n                # IDAT chunk contains default image and not first animation frame\n                self.n_frames += 1\n            self._seek(0)\n        self.is_animated = self.n_frames > 1\n\n    @property\n    def text(self):\n        # experimental\n        if self._text is None:\n            # iTxt, tEXt and zTXt chunks may appear at the end of the file\n            # So load the file to ensure that they are read\n            if self.is_animated:\n                frame = self.__frame\n                # for APNG, seek to the final frame before loading\n                self.seek(self.n_frames - 1)\n            self.load()\n            if self.is_animated:\n                self.seek(frame)\n        return self._text\n\n    def verify(self):\n        \"\"\"Verify PNG file\"\"\"\n\n        if self.fp is None:\n            msg = \"verify must be called directly after open\"\n            raise RuntimeError(msg)\n\n        # back up to beginning of IDAT block\n        self.fp.seek(self.tile[0][2] - 8)\n\n        self.png.verify()\n        self.png.close()\n\n        if self._exclusive_fp:\n            self.fp.close()\n        self.fp = None\n\n    def seek(self, frame):\n        if not self._seek_check(frame):\n            return\n        if frame < self.__frame:\n            self._seek(0, True)\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 APNG file\"\n                raise EOFError(msg) from e\n\n    def _seek(self, frame, rewind=False):\n        if frame == 0:\n            if rewind:\n                self._fp.seek(self.__rewind)\n                self.png.rewind()\n                self.__prepare_idat = self.__rewind_idat\n                self.im = None\n                if self.pyaccess:\n                    self.pyaccess = None\n                self.info = self.png.im_info\n                self.tile = self.png.im_tile\n                self.fp = self._fp\n            self._prev_im = None\n            self.dispose = None\n            self.default_image = self.info.get(\"default_image\", False)\n            self.dispose_op = self.info.get(\"disposal\")\n            self.blend_op = self.info.get(\"blend\")\n            self.dispose_extent = self.info.get(\"bbox\")\n            self.__frame = 0\n        else:\n            if frame != self.__frame + 1:\n                msg = f\"cannot seek to frame {frame}\"\n                raise ValueError(msg)\n\n            # ensure previous frame was loaded\n            self.load()\n\n            if self.dispose:\n                self.im.paste(self.dispose, self.dispose_extent)\n            self._prev_im = self.im.copy()\n\n            self.fp = self._fp\n\n            # advance to the next frame\n            if self.__prepare_idat:\n                ImageFile._safe_read(self.fp, self.__prepare_idat)\n                self.__prepare_idat = 0\n            frame_start = False\n            while True:\n                self.fp.read(4)  # CRC\n\n                try:\n                    cid, pos, length = self.png.read()\n                except (struct.error, SyntaxError):\n                    break\n\n                if cid == b\"IEND\":\n                    msg = \"No more images in APNG file\"\n                    raise EOFError(msg)\n                if cid == b\"fcTL\":\n                    if frame_start:\n                        # there must be at least one fdAT chunk between fcTL chunks\n                        msg = \"APNG missing frame data\"\n                        raise SyntaxError(msg)\n                    frame_start = True\n\n                try:\n                    self.png.call(cid, pos, length)\n                except UnicodeDecodeError:\n                    break\n                except EOFError:\n                    if cid == b\"fdAT\":\n                        length -= 4\n                        if frame_start:\n                            self.__prepare_idat = length\n                            break\n                    ImageFile._safe_read(self.fp, length)\n                except AttributeError:\n                    logger.debug(\"%r %s %s (unknown)\", cid, pos, length)\n                    ImageFile._safe_read(self.fp, length)\n\n            self.__frame = frame\n            self.tile = self.png.im_tile\n            self.dispose_op = self.info.get(\"disposal\")\n            self.blend_op = self.info.get(\"blend\")\n            self.dispose_extent = self.info.get(\"bbox\")\n\n            if not self.tile:\n                msg = \"image not found in APNG frame\"\n                raise EOFError(msg)\n\n        # setup frame disposal (actual disposal done when needed in the next _seek())\n        if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:\n            self.dispose_op = Disposal.OP_BACKGROUND\n\n        if self.dispose_op == Disposal.OP_PREVIOUS:\n            self.dispose = self._prev_im.copy()\n            self.dispose = self._crop(self.dispose, self.dispose_extent)\n        elif self.dispose_op == Disposal.OP_BACKGROUND:\n            self.dispose = Image.core.fill(self.mode, self.size)\n            self.dispose = self._crop(self.dispose, self.dispose_extent)\n        else:\n            self.dispose = None\n\n    def tell(self):\n        return self.__frame\n\n    def load_prepare(self):\n        \"\"\"internal: prepare to read PNG file\"\"\"\n\n        if self.info.get(\"interlace\"):\n            self.decoderconfig = self.decoderconfig + (1,)\n\n        self.__idat = self.__prepare_idat  # used by load_read()\n        ImageFile.ImageFile.load_prepare(self)\n\n    def load_read(self, read_bytes):\n        \"\"\"internal: read more image data\"\"\"\n\n        while self.__idat == 0:\n            # end of chunk, skip forward to next one\n\n            self.fp.read(4)  # CRC\n\n            cid, pos, length = self.png.read()\n\n            if cid not in [b\"IDAT\", b\"DDAT\", b\"fdAT\"]:\n                self.png.push(cid, pos, length)\n                return b\"\"\n\n            if cid == b\"fdAT\":\n                try:\n                    self.png.call(cid, pos, length)\n                except EOFError:\n                    pass\n                self.__idat = length - 4  # sequence_num has already been read\n            else:\n                self.__idat = length  # empty chunks are allowed\n\n        # read more data from this chunk\n        if read_bytes <= 0:\n            read_bytes = self.__idat\n        else:\n            read_bytes = min(read_bytes, self.__idat)\n\n        self.__idat = self.__idat - read_bytes\n\n        return self.fp.read(read_bytes)\n\n    def load_end(self):\n        \"\"\"internal: finished reading image data\"\"\"\n        if self.__idat != 0:\n            self.fp.read(self.__idat)\n        while True:\n            self.fp.read(4)  # CRC\n\n            try:\n                cid, pos, length = self.png.read()\n            except (struct.error, SyntaxError):\n                break\n\n            if cid == b\"IEND\":\n                break\n            elif cid == b\"fcTL\" and self.is_animated:\n                # start of the next frame, stop reading\n                self.__prepare_idat = 0\n                self.png.push(cid, pos, length)\n                break\n\n            try:\n                self.png.call(cid, pos, length)\n            except UnicodeDecodeError:\n                break\n            except EOFError:\n                if cid == b\"fdAT\":\n                    length -= 4\n                ImageFile._safe_read(self.fp, length)\n            except AttributeError:\n                logger.debug(\"%r %s %s (unknown)\", cid, pos, length)\n                s = ImageFile._safe_read(self.fp, length)\n                if cid[1:2].islower():\n                    self.private_chunks.append((cid, s, True))\n        self._text = self.png.im_text\n        if not self.is_animated:\n            self.png.close()\n            self.png = None\n        else:\n            if self._prev_im and self.blend_op == Blend.OP_OVER:\n                updated = self._crop(self.im, self.dispose_extent)\n                if self.im.mode == \"RGB\" and \"transparency\" in self.info:\n                    mask = updated.convert_transparent(\n                        \"RGBA\", self.info[\"transparency\"]\n                    )\n                else:\n                    mask = updated.convert(\"RGBA\")\n                self._prev_im.paste(updated, self.dispose_extent, mask)\n                self.im = self._prev_im\n                if self.pyaccess:\n                    self.pyaccess = None\n\n    def _getexif(self):\n        if \"exif\" not in self.info:\n            self.load()\n        if \"exif\" not in self.info and \"Raw profile type exif\" not in self.info:\n            return None\n        return self.getexif()._get_merged_dict()\n\n    def getexif(self):\n        if \"exif\" not in self.info:\n            self.load()\n\n        return super().getexif()\n\n    def getxmp(self):\n        \"\"\"\n        Returns a dictionary containing the XMP tags.\n        Requires defusedxml to be installed.\n\n        :returns: XMP tags in a dictionary.\n        \"\"\"\n        return (\n            self._getxmp(self.info[\"XML:com.adobe.xmp\"])\n            if \"XML:com.adobe.xmp\" in self.info\n            else {}\n        )\n\n\n# --------------------------------------------------------------------\n# PNG writer\n\n_OUTMODES = {\n    # supported PIL modes, and corresponding rawmodes/bits/color combinations\n    \"1\": (\"1\", b\"\\x01\\x00\"),\n    \"L;1\": (\"L;1\", b\"\\x01\\x00\"),\n    \"L;2\": (\"L;2\", b\"\\x02\\x00\"),\n    \"L;4\": (\"L;4\", b\"\\x04\\x00\"),\n    \"L\": (\"L\", b\"\\x08\\x00\"),\n    \"LA\": (\"LA\", b\"\\x08\\x04\"),\n    \"I\": (\"I;16B\", b\"\\x10\\x00\"),\n    \"I;16\": (\"I;16B\", b\"\\x10\\x00\"),\n    \"I;16B\": (\"I;16B\", b\"\\x10\\x00\"),\n    \"P;1\": (\"P;1\", b\"\\x01\\x03\"),\n    \"P;2\": (\"P;2\", b\"\\x02\\x03\"),\n    \"P;4\": (\"P;4\", b\"\\x04\\x03\"),\n    \"P\": (\"P\", b\"\\x08\\x03\"),\n    \"RGB\": (\"RGB\", b\"\\x08\\x02\"),\n    \"RGBA\": (\"RGBA\", b\"\\x08\\x06\"),\n}\n\n\ndef putchunk(fp, cid, *data):\n    \"\"\"Write a PNG chunk (including CRC field)\"\"\"\n\n    data = b\"\".join(data)\n\n    fp.write(o32(len(data)) + cid)\n    fp.write(data)\n    crc = _crc32(data, _crc32(cid))\n    fp.write(o32(crc))\n\n\nclass _idat:\n    # wrap output from the encoder in IDAT chunks\n\n    def __init__(self, fp, chunk):\n        self.fp = fp\n        self.chunk = chunk\n\n    def write(self, data):\n        self.chunk(self.fp, b\"IDAT\", data)\n\n\nclass _fdat:\n    # wrap encoder output in fdAT chunks\n\n    def __init__(self, fp, chunk, seq_num):\n        self.fp = fp\n        self.chunk = chunk\n        self.seq_num = seq_num\n\n    def write(self, data):\n        self.chunk(self.fp, b\"fdAT\", o32(self.seq_num), data)\n        self.seq_num += 1\n\n\ndef _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images):\n    duration = im.encoderinfo.get(\"duration\", im.info.get(\"duration\", 0))\n    loop = im.encoderinfo.get(\"loop\", im.info.get(\"loop\", 0))\n    disposal = im.encoderinfo.get(\"disposal\", im.info.get(\"disposal\", Disposal.OP_NONE))\n    blend = im.encoderinfo.get(\"blend\", im.info.get(\"blend\", Blend.OP_SOURCE))\n\n    if default_image:\n        chain = itertools.chain(append_images)\n    else:\n        chain = itertools.chain([im], append_images)\n\n    im_frames = []\n    frame_count = 0\n    for im_seq in chain:\n        for im_frame in ImageSequence.Iterator(im_seq):\n            if im_frame.mode == rawmode:\n                im_frame = im_frame.copy()\n            else:\n                im_frame = im_frame.convert(rawmode)\n            encoderinfo = im.encoderinfo.copy()\n            if isinstance(duration, (list, tuple)):\n                encoderinfo[\"duration\"] = duration[frame_count]\n            if isinstance(disposal, (list, tuple)):\n                encoderinfo[\"disposal\"] = disposal[frame_count]\n            if isinstance(blend, (list, tuple)):\n                encoderinfo[\"blend\"] = blend[frame_count]\n            frame_count += 1\n\n            if im_frames:\n                previous = im_frames[-1]\n                prev_disposal = previous[\"encoderinfo\"].get(\"disposal\")\n                prev_blend = previous[\"encoderinfo\"].get(\"blend\")\n                if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:\n                    prev_disposal = Disposal.OP_BACKGROUND\n\n                if prev_disposal == Disposal.OP_BACKGROUND:\n                    base_im = previous[\"im\"].copy()\n                    dispose = Image.core.fill(\"RGBA\", im.size, (0, 0, 0, 0))\n                    bbox = previous[\"bbox\"]\n                    if bbox:\n                        dispose = dispose.crop(bbox)\n                    else:\n                        bbox = (0, 0) + im.size\n                    base_im.paste(dispose, bbox)\n                elif prev_disposal == Disposal.OP_PREVIOUS:\n                    base_im = im_frames[-2][\"im\"]\n                else:\n                    base_im = previous[\"im\"]\n                delta = ImageChops.subtract_modulo(\n                    im_frame.convert(\"RGBA\"), base_im.convert(\"RGBA\")\n                )\n                bbox = delta.getbbox(alpha_only=False)\n                if (\n                    not bbox\n                    and prev_disposal == encoderinfo.get(\"disposal\")\n                    and prev_blend == encoderinfo.get(\"blend\")\n                ):\n                    previous[\"encoderinfo\"][\"duration\"] += encoderinfo.get(\n                        \"duration\", duration\n                    )\n                    continue\n            else:\n                bbox = None\n            if \"duration\" not in encoderinfo:\n                encoderinfo[\"duration\"] = duration\n            im_frames.append({\"im\": im_frame, \"bbox\": bbox, \"encoderinfo\": encoderinfo})\n\n    if len(im_frames) == 1 and not default_image:\n        return im_frames[0][\"im\"]\n\n    # animation control\n    chunk(\n        fp,\n        b\"acTL\",\n        o32(len(im_frames)),  # 0: num_frames\n        o32(loop),  # 4: num_plays\n    )\n\n    # default image IDAT (if it exists)\n    if default_image:\n        if im.mode != rawmode:\n            im = im.convert(rawmode)\n        ImageFile._save(im, _idat(fp, chunk), [(\"zip\", (0, 0) + im.size, 0, rawmode)])\n\n    seq_num = 0\n    for frame, frame_data in enumerate(im_frames):\n        im_frame = frame_data[\"im\"]\n        if not frame_data[\"bbox\"]:\n            bbox = (0, 0) + im_frame.size\n        else:\n            bbox = frame_data[\"bbox\"]\n            im_frame = im_frame.crop(bbox)\n        size = im_frame.size\n        encoderinfo = frame_data[\"encoderinfo\"]\n        frame_duration = int(round(encoderinfo[\"duration\"]))\n        frame_disposal = encoderinfo.get(\"disposal\", disposal)\n        frame_blend = encoderinfo.get(\"blend\", blend)\n        # frame control\n        chunk(\n            fp,\n            b\"fcTL\",\n            o32(seq_num),  # sequence_number\n            o32(size[0]),  # width\n            o32(size[1]),  # height\n            o32(bbox[0]),  # x_offset\n            o32(bbox[1]),  # y_offset\n            o16(frame_duration),  # delay_numerator\n            o16(1000),  # delay_denominator\n            o8(frame_disposal),  # dispose_op\n            o8(frame_blend),  # blend_op\n        )\n        seq_num += 1\n        # frame data\n        if frame == 0 and not default_image:\n            # first frame must be in IDAT chunks for backwards compatibility\n            ImageFile._save(\n                im_frame,\n                _idat(fp, chunk),\n                [(\"zip\", (0, 0) + im_frame.size, 0, rawmode)],\n            )\n        else:\n            fdat_chunks = _fdat(fp, chunk, seq_num)\n            ImageFile._save(\n                im_frame,\n                fdat_chunks,\n                [(\"zip\", (0, 0) + im_frame.size, 0, rawmode)],\n            )\n            seq_num = fdat_chunks.seq_num\n\n\ndef _save_all(im, fp, filename):\n    _save(im, fp, filename, save_all=True)\n\n\ndef _save(im, fp, filename, chunk=putchunk, save_all=False):\n    # save an image to disk (called by the save method)\n\n    if save_all:\n        default_image = im.encoderinfo.get(\n            \"default_image\", im.info.get(\"default_image\")\n        )\n        modes = set()\n        append_images = im.encoderinfo.get(\"append_images\", [])\n        for im_seq in itertools.chain([im], append_images):\n            for im_frame in ImageSequence.Iterator(im_seq):\n                modes.add(im_frame.mode)\n        for mode in (\"RGBA\", \"RGB\", \"P\"):\n            if mode in modes:\n                break\n        else:\n            mode = modes.pop()\n    else:\n        mode = im.mode\n\n    if mode == \"P\":\n        #\n        # attempt to minimize storage requirements for palette images\n        if \"bits\" in im.encoderinfo:\n            # number of bits specified by user\n            colors = min(1 << im.encoderinfo[\"bits\"], 256)\n        else:\n            # check palette contents\n            if im.palette:\n                colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1)\n            else:\n                colors = 256\n\n        if colors <= 16:\n            if colors <= 2:\n                bits = 1\n            elif colors <= 4:\n                bits = 2\n            else:\n                bits = 4\n            mode = f\"{mode};{bits}\"\n\n    # encoder options\n    im.encoderconfig = (\n        im.encoderinfo.get(\"optimize\", False),\n        im.encoderinfo.get(\"compress_level\", -1),\n        im.encoderinfo.get(\"compress_type\", -1),\n        im.encoderinfo.get(\"dictionary\", b\"\"),\n    )\n\n    # get the corresponding PNG mode\n    try:\n        rawmode, mode = _OUTMODES[mode]\n    except KeyError as e:\n        msg = f\"cannot write mode {mode} as PNG\"\n        raise OSError(msg) from e\n\n    #\n    # write minimal PNG file\n\n    fp.write(_MAGIC)\n\n    chunk(\n        fp,\n        b\"IHDR\",\n        o32(im.size[0]),  # 0: size\n        o32(im.size[1]),\n        mode,  # 8: depth/type\n        b\"\\0\",  # 10: compression\n        b\"\\0\",  # 11: filter category\n        b\"\\0\",  # 12: interlace flag\n    )\n\n    chunks = [b\"cHRM\", b\"gAMA\", b\"sBIT\", b\"sRGB\", b\"tIME\"]\n\n    icc = im.encoderinfo.get(\"icc_profile\", im.info.get(\"icc_profile\"))\n    if icc:\n        # ICC profile\n        # according to PNG spec, the iCCP chunk contains:\n        # Profile name  1-79 bytes (character string)\n        # Null separator        1 byte (null character)\n        # Compression method    1 byte (0)\n        # Compressed profile    n bytes (zlib with deflate compression)\n        name = b\"ICC Profile\"\n        data = name + b\"\\0\\0\" + zlib.compress(icc)\n        chunk(fp, b\"iCCP\", data)\n\n        # You must either have sRGB or iCCP.\n        # Disallow sRGB chunks when an iCCP-chunk has been emitted.\n        chunks.remove(b\"sRGB\")\n\n    info = im.encoderinfo.get(\"pnginfo\")\n    if info:\n        chunks_multiple_allowed = [b\"sPLT\", b\"iTXt\", b\"tEXt\", b\"zTXt\"]\n        for info_chunk in info.chunks:\n            cid, data = info_chunk[:2]\n            if cid in chunks:\n                chunks.remove(cid)\n                chunk(fp, cid, data)\n            elif cid in chunks_multiple_allowed:\n                chunk(fp, cid, data)\n            elif cid[1:2].islower():\n                # Private chunk\n                after_idat = info_chunk[2:3]\n                if not after_idat:\n                    chunk(fp, cid, data)\n\n    if im.mode == \"P\":\n        palette_byte_number = colors * 3\n        palette_bytes = im.im.getpalette(\"RGB\")[:palette_byte_number]\n        while len(palette_bytes) < palette_byte_number:\n            palette_bytes += b\"\\0\"\n        chunk(fp, b\"PLTE\", palette_bytes)\n\n    transparency = im.encoderinfo.get(\"transparency\", im.info.get(\"transparency\", None))\n\n    if transparency or transparency == 0:\n        if im.mode == \"P\":\n            # limit to actual palette size\n            alpha_bytes = colors\n            if isinstance(transparency, bytes):\n                chunk(fp, b\"tRNS\", transparency[:alpha_bytes])\n            else:\n                transparency = max(0, min(255, transparency))\n                alpha = b\"\\xFF\" * transparency + b\"\\0\"\n                chunk(fp, b\"tRNS\", alpha[:alpha_bytes])\n        elif im.mode in (\"1\", \"L\", \"I\"):\n            transparency = max(0, min(65535, transparency))\n            chunk(fp, b\"tRNS\", o16(transparency))\n        elif im.mode == \"RGB\":\n            red, green, blue = transparency\n            chunk(fp, b\"tRNS\", o16(red) + o16(green) + o16(blue))\n        else:\n            if \"transparency\" in im.encoderinfo:\n                # don't bother with transparency if it's an RGBA\n                # and it's in the info dict. It's probably just stale.\n                msg = \"cannot use transparency for this mode\"\n                raise OSError(msg)\n    else:\n        if im.mode == \"P\" and im.im.getpalettemode() == \"RGBA\":\n            alpha = im.im.getpalette(\"RGBA\", \"A\")\n            alpha_bytes = colors\n            chunk(fp, b\"tRNS\", alpha[:alpha_bytes])\n\n    dpi = im.encoderinfo.get(\"dpi\")\n    if dpi:\n        chunk(\n            fp,\n            b\"pHYs\",\n            o32(int(dpi[0] / 0.0254 + 0.5)),\n            o32(int(dpi[1] / 0.0254 + 0.5)),\n            b\"\\x01\",\n        )\n\n    if info:\n        chunks = [b\"bKGD\", b\"hIST\"]\n        for info_chunk in info.chunks:\n            cid, data = info_chunk[:2]\n            if cid in chunks:\n                chunks.remove(cid)\n                chunk(fp, cid, data)\n\n    exif = im.encoderinfo.get(\"exif\")\n    if exif:\n        if isinstance(exif, Image.Exif):\n            exif = exif.tobytes(8)\n        if exif.startswith(b\"Exif\\x00\\x00\"):\n            exif = exif[6:]\n        chunk(fp, b\"eXIf\", exif)\n\n    if save_all:\n        im = _write_multiple_frames(\n            im, fp, chunk, rawmode, default_image, append_images\n        )\n    if im:\n        ImageFile._save(im, _idat(fp, chunk), [(\"zip\", (0, 0) + im.size, 0, rawmode)])\n\n    if info:\n        for info_chunk in info.chunks:\n            cid, data = info_chunk[:2]\n            if cid[1:2].islower():\n                # Private chunk\n                after_idat = info_chunk[2:3]\n                if after_idat:\n                    chunk(fp, cid, data)\n\n    chunk(fp, b\"IEND\", b\"\")\n\n    if hasattr(fp, \"flush\"):\n        fp.flush()\n\n\n# --------------------------------------------------------------------\n# PNG chunk converter\n\n\ndef getchunks(im, **params):\n    \"\"\"Return a list of PNG chunks representing this image.\"\"\"\n\n    class collector:\n        data = []\n\n        def write(self, data):\n            pass\n\n        def append(self, chunk):\n            self.data.append(chunk)\n\n    def append(fp, cid, *data):\n        data = b\"\".join(data)\n        crc = o32(_crc32(data, _crc32(cid)))\n        fp.append((cid, data, crc))\n\n    fp = collector()\n\n    try:\n        im.encoderinfo = params\n        _save(im, fp, None, append)\n    finally:\n        del im.encoderinfo\n\n    return fp.data\n\n\n# --------------------------------------------------------------------\n# Registry\n\nImage.register_open(PngImageFile.format, PngImageFile, _accept)\nImage.register_save(PngImageFile.format, _save)\nImage.register_save_all(PngImageFile.format, _save_all)\n\nImage.register_extensions(PngImageFile.format, [\".png\", \".apng\"])\n\nImage.register_mime(PngImageFile.format, \"image/png\")\n",
            "header": {
                "file": {
                    "path": "src/PIL/PngImagePlugin.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "1bd0f442f",
                "new_commit_hash": "dbcdee1c2",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/src/PIL/PngImagePlugin.py"
            },
            "plus_file": {
                "path": "b/src/PIL/PngImagePlugin.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 1156,
                            "length": 6
                        },
                        "new_range": {
                            "start": 1156,
                            "length": 9
                        },
                        "section": "def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "                encoderinfo[\"duration\"] = duration",
                                "type": "context"
                            },
                            {
                                "content": "            im_frames.append({\"im\": im_frame, \"bbox\": bbox, \"encoderinfo\": encoderinfo})",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    if len(im_frames) == 1 and not default_image:",
                                "type": "added"
                            },
                            {
                                "content": "        return im_frames[0][\"im\"]",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    # animation control",
                                "type": "context"
                            },
                            {
                                "content": "    chunk(",
                                "type": "context"
                            },
                            {
                                "content": "        fp,",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "src/PIL/PngImagePlugin.py",
                            "type": "function",
                            "name": "_write_multiple_frames",
                            "content": "def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images):\n    duration = im.encoderinfo.get(\"duration\", im.info.get(\"duration\", 0))\n    loop = im.encoderinfo.get(\"loop\", im.info.get(\"loop\", 0))\n    disposal = im.encoderinfo.get(\"disposal\", im.info.get(\"disposal\", Disposal.OP_NONE))\n    blend = im.encoderinfo.get(\"blend\", im.info.get(\"blend\", Blend.OP_SOURCE))\n\n    if default_image:\n        chain = itertools.chain(append_images)\n    else:\n        chain = itertools.chain([im], append_images)\n\n    im_frames = []\n    frame_count = 0\n    for im_seq in chain:\n        for im_frame in ImageSequence.Iterator(im_seq):\n            if im_frame.mode == rawmode:\n                im_frame = im_frame.copy()\n            else:\n                im_frame = im_frame.convert(rawmode)\n            encoderinfo = im.encoderinfo.copy()\n            if isinstance(duration, (list, tuple)):\n                encoderinfo[\"duration\"] = duration[frame_count]\n            if isinstance(disposal, (list, tuple)):\n                encoderinfo[\"disposal\"] = disposal[frame_count]\n            if isinstance(blend, (list, tuple)):\n                encoderinfo[\"blend\"] = blend[frame_count]\n            frame_count += 1\n\n            if im_frames:\n                previous = im_frames[-1]\n                prev_disposal = previous[\"encoderinfo\"].get(\"disposal\")\n                prev_blend = previous[\"encoderinfo\"].get(\"blend\")\n                if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:\n                    prev_disposal = Disposal.OP_BACKGROUND\n\n                if prev_disposal == Disposal.OP_BACKGROUND:\n                    base_im = previous[\"im\"].copy()\n                    dispose = Image.core.fill(\"RGBA\", im.size, (0, 0, 0, 0))\n                    bbox = previous[\"bbox\"]\n                    if bbox:\n                        dispose = dispose.crop(bbox)\n                    else:\n                        bbox = (0, 0) + im.size\n                    base_im.paste(dispose, bbox)\n                elif prev_disposal == Disposal.OP_PREVIOUS:\n                    base_im = im_frames[-2][\"im\"]\n                else:\n                    base_im = previous[\"im\"]\n                delta = ImageChops.subtract_modulo(\n                    im_frame.convert(\"RGBA\"), base_im.convert(\"RGBA\")\n                )\n                bbox = delta.getbbox(alpha_only=False)\n                if (\n                    not bbox\n                    and prev_disposal == encoderinfo.get(\"disposal\")\n                    and prev_blend == encoderinfo.get(\"blend\")\n                ):\n                    previous[\"encoderinfo\"][\"duration\"] += encoderinfo.get(\n                        \"duration\", duration\n                    )\n                    continue\n            else:\n                bbox = None\n            if \"duration\" not in encoderinfo:\n                encoderinfo[\"duration\"] = duration\n            im_frames.append({\"im\": im_frame, \"bbox\": bbox, \"encoderinfo\": encoderinfo})\n\n    if len(im_frames) == 1 and not default_image:\n        return im_frames[0][\"im\"]\n\n    # animation control\n    chunk(\n        fp,\n        b\"acTL\",\n        o32(len(im_frames)),  # 0: num_frames\n        o32(loop),  # 4: num_plays\n    )\n\n    # default image IDAT (if it exists)\n    if default_image:\n        if im.mode != rawmode:\n            im = im.convert(rawmode)\n        ImageFile._save(im, _idat(fp, chunk), [(\"zip\", (0, 0) + im.size, 0, rawmode)])\n\n    seq_num = 0\n    for frame, frame_data in enumerate(im_frames):\n        im_frame = frame_data[\"im\"]\n        if not frame_data[\"bbox\"]:\n            bbox = (0, 0) + im_frame.size\n        else:\n            bbox = frame_data[\"bbox\"]\n            im_frame = im_frame.crop(bbox)\n        size = im_frame.size\n        encoderinfo = frame_data[\"encoderinfo\"]\n        frame_duration = int(round(encoderinfo[\"duration\"]))\n        frame_disposal = encoderinfo.get(\"disposal\", disposal)\n        frame_blend = encoderinfo.get(\"blend\", blend)\n        # frame control\n        chunk(\n            fp,\n            b\"fcTL\",\n            o32(seq_num),  # sequence_number\n            o32(size[0]),  # width\n            o32(size[1]),  # height\n            o32(bbox[0]),  # x_offset\n            o32(bbox[1]),  # y_offset\n            o16(frame_duration),  # delay_numerator\n            o16(1000),  # delay_denominator\n            o8(frame_disposal),  # dispose_op\n            o8(frame_blend),  # blend_op\n        )\n        seq_num += 1\n        # frame data\n        if frame == 0 and not default_image:\n            # first frame must be in IDAT chunks for backwards compatibility\n            ImageFile._save(\n                im_frame,\n                _idat(fp, chunk),\n                [(\"zip\", (0, 0) + im_frame.size, 0, rawmode)],\n            )\n        else:\n            fdat_chunks = _fdat(fp, chunk, seq_num)\n            ImageFile._save(\n                im_frame,\n                fdat_chunks,\n                [(\"zip\", (0, 0) + im_frame.size, 0, rawmode)],\n            )\n            seq_num = fdat_chunks.seq_num",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 1092,
                            "end_lineno": 1219,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 1391,
                            "length": 8
                        },
                        "new_range": {
                            "start": 1394,
                            "length": 10
                        },
                        "section": "def _save(im, fp, filename, chunk=putchunk, save_all=False):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        chunk(fp, b\"eXIf\", exif)",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    if save_all:",
                                "type": "context"
                            },
                            {
                                "content": "        _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)",
                                "type": "deleted"
                            },
                            {
                                "content": "    else:",
                                "type": "deleted"
                            },
                            {
                                "content": "        im = _write_multiple_frames(",
                                "type": "added"
                            },
                            {
                                "content": "            im, fp, chunk, rawmode, default_image, append_images",
                                "type": "added"
                            },
                            {
                                "content": "        )",
                                "type": "added"
                            },
                            {
                                "content": "    if im:",
                                "type": "added"
                            },
                            {
                                "content": "        ImageFile._save(im, _idat(fp, chunk), [(\"zip\", (0, 0) + im.size, 0, rawmode)])",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    if info:",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "src/PIL/PngImagePlugin.py",
                            "type": "function",
                            "name": "_save",
                            "content": "def _save(im, fp, filename, chunk=putchunk, save_all=False):\n    # save an image to disk (called by the save method)\n\n    if save_all:\n        default_image = im.encoderinfo.get(\n            \"default_image\", im.info.get(\"default_image\")\n        )\n        modes = set()\n        append_images = im.encoderinfo.get(\"append_images\", [])\n        for im_seq in itertools.chain([im], append_images):\n            for im_frame in ImageSequence.Iterator(im_seq):\n                modes.add(im_frame.mode)\n        for mode in (\"RGBA\", \"RGB\", \"P\"):\n            if mode in modes:\n                break\n        else:\n            mode = modes.pop()\n    else:\n        mode = im.mode\n\n    if mode == \"P\":\n        #\n        # attempt to minimize storage requirements for palette images\n        if \"bits\" in im.encoderinfo:\n            # number of bits specified by user\n            colors = min(1 << im.encoderinfo[\"bits\"], 256)\n        else:\n            # check palette contents\n            if im.palette:\n                colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1)\n            else:\n                colors = 256\n\n        if colors <= 16:\n            if colors <= 2:\n                bits = 1\n            elif colors <= 4:\n                bits = 2\n            else:\n                bits = 4\n            mode = f\"{mode};{bits}\"\n\n    # encoder options\n    im.encoderconfig = (\n        im.encoderinfo.get(\"optimize\", False),\n        im.encoderinfo.get(\"compress_level\", -1),\n        im.encoderinfo.get(\"compress_type\", -1),\n        im.encoderinfo.get(\"dictionary\", b\"\"),\n    )\n\n    # get the corresponding PNG mode\n    try:\n        rawmode, mode = _OUTMODES[mode]\n    except KeyError as e:\n        msg = f\"cannot write mode {mode} as PNG\"\n        raise OSError(msg) from e\n\n    #\n    # write minimal PNG file\n\n    fp.write(_MAGIC)\n\n    chunk(\n        fp,\n        b\"IHDR\",\n        o32(im.size[0]),  # 0: size\n        o32(im.size[1]),\n        mode,  # 8: depth/type\n        b\"\\0\",  # 10: compression\n        b\"\\0\",  # 11: filter category\n        b\"\\0\",  # 12: interlace flag\n    )\n\n    chunks = [b\"cHRM\", b\"gAMA\", b\"sBIT\", b\"sRGB\", b\"tIME\"]\n\n    icc = im.encoderinfo.get(\"icc_profile\", im.info.get(\"icc_profile\"))\n    if icc:\n        # ICC profile\n        # according to PNG spec, the iCCP chunk contains:\n        # Profile name  1-79 bytes (character string)\n        # Null separator        1 byte (null character)\n        # Compression method    1 byte (0)\n        # Compressed profile    n bytes (zlib with deflate compression)\n        name = b\"ICC Profile\"\n        data = name + b\"\\0\\0\" + zlib.compress(icc)\n        chunk(fp, b\"iCCP\", data)\n\n        # You must either have sRGB or iCCP.\n        # Disallow sRGB chunks when an iCCP-chunk has been emitted.\n        chunks.remove(b\"sRGB\")\n\n    info = im.encoderinfo.get(\"pnginfo\")\n    if info:\n        chunks_multiple_allowed = [b\"sPLT\", b\"iTXt\", b\"tEXt\", b\"zTXt\"]\n        for info_chunk in info.chunks:\n            cid, data = info_chunk[:2]\n            if cid in chunks:\n                chunks.remove(cid)\n                chunk(fp, cid, data)\n            elif cid in chunks_multiple_allowed:\n                chunk(fp, cid, data)\n            elif cid[1:2].islower():\n                # Private chunk\n                after_idat = info_chunk[2:3]\n                if not after_idat:\n                    chunk(fp, cid, data)\n\n    if im.mode == \"P\":\n        palette_byte_number = colors * 3\n        palette_bytes = im.im.getpalette(\"RGB\")[:palette_byte_number]\n        while len(palette_bytes) < palette_byte_number:\n            palette_bytes += b\"\\0\"\n        chunk(fp, b\"PLTE\", palette_bytes)\n\n    transparency = im.encoderinfo.get(\"transparency\", im.info.get(\"transparency\", None))\n\n    if transparency or transparency == 0:\n        if im.mode == \"P\":\n            # limit to actual palette size\n            alpha_bytes = colors\n            if isinstance(transparency, bytes):\n                chunk(fp, b\"tRNS\", transparency[:alpha_bytes])\n            else:\n                transparency = max(0, min(255, transparency))\n                alpha = b\"\\xFF\" * transparency + b\"\\0\"\n                chunk(fp, b\"tRNS\", alpha[:alpha_bytes])\n        elif im.mode in (\"1\", \"L\", \"I\"):\n            transparency = max(0, min(65535, transparency))\n            chunk(fp, b\"tRNS\", o16(transparency))\n        elif im.mode == \"RGB\":\n            red, green, blue = transparency\n            chunk(fp, b\"tRNS\", o16(red) + o16(green) + o16(blue))\n        else:\n            if \"transparency\" in im.encoderinfo:\n                # don't bother with transparency if it's an RGBA\n                # and it's in the info dict. It's probably just stale.\n                msg = \"cannot use transparency for this mode\"\n                raise OSError(msg)\n    else:\n        if im.mode == \"P\" and im.im.getpalettemode() == \"RGBA\":\n            alpha = im.im.getpalette(\"RGBA\", \"A\")\n            alpha_bytes = colors\n            chunk(fp, b\"tRNS\", alpha[:alpha_bytes])\n\n    dpi = im.encoderinfo.get(\"dpi\")\n    if dpi:\n        chunk(\n            fp,\n            b\"pHYs\",\n            o32(int(dpi[0] / 0.0254 + 0.5)),\n            o32(int(dpi[1] / 0.0254 + 0.5)),\n            b\"\\x01\",\n        )\n\n    if info:\n        chunks = [b\"bKGD\", b\"hIST\"]\n        for info_chunk in info.chunks:\n            cid, data = info_chunk[:2]\n            if cid in chunks:\n                chunks.remove(cid)\n                chunk(fp, cid, data)\n\n    exif = im.encoderinfo.get(\"exif\")\n    if exif:\n        if isinstance(exif, Image.Exif):\n            exif = exif.tobytes(8)\n        if exif.startswith(b\"Exif\\x00\\x00\"):\n            exif = exif[6:]\n        chunk(fp, b\"eXIf\", exif)\n\n    if save_all:\n        _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)\n    else:\n        ImageFile._save(im, _idat(fp, chunk), [(\"zip\", (0, 0) + im.size, 0, rawmode)])\n\n    if info:\n        for info_chunk in info.chunks:\n            cid, data = info_chunk[:2]\n            if cid[1:2].islower():\n                # Private chunk\n                after_idat = info_chunk[2:3]\n                if after_idat:\n                    chunk(fp, cid, data)\n\n    chunk(fp, b\"IEND\", b\"\")\n\n    if hasattr(fp, \"flush\"):\n        fp.flush()",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 1223,
                            "end_lineno": 1410,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "a7fe25df53b9c37af24a7a362f7ebd13ba07be6d^",
    "new_commit_hash": "a7fe25df53b9c37af24a7a362f7ebd13ba07be6d",
    "commit_message": "Merge pull request #7522 from radarhere/png_save_all",
    "commit_date": "2023-11-07T11:35:24+02:00",
    "metadata": {}
}
{
    "repo_name": "pillow",
    "new_commit_hash": "a7fe25df53b9c37af24a7a362f7ebd13ba07be6d",
    "test_file_codes": [
        "import pytest\n\nfrom PIL import Image, ImageSequence, PngImagePlugin\n\n\n# APNG browser support tests and fixtures via:\n# https://philip.html5.org/tests/apng/tests.html\n# (referenced from https://wiki.mozilla.org/APNG_Specification)\ndef test_apng_basic():\n    with Image.open(\"Tests/images/apng/single_frame.png\") as im:\n        assert not im.is_animated\n        assert im.n_frames == 1\n        assert im.get_format_mimetype() == \"image/apng\"\n        assert im.info.get(\"default_image\") is None\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/single_frame_default.png\") as im:\n        assert im.is_animated\n        assert im.n_frames == 2\n        assert im.get_format_mimetype() == \"image/apng\"\n        assert im.info.get(\"default_image\")\n        assert im.getpixel((0, 0)) == (255, 0, 0, 255)\n        assert im.getpixel((64, 32)) == (255, 0, 0, 255)\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n        # test out of bounds seek\n        with pytest.raises(EOFError):\n            im.seek(2)\n\n        # test rewind support\n        im.seek(0)\n        assert im.getpixel((0, 0)) == (255, 0, 0, 255)\n        assert im.getpixel((64, 32)) == (255, 0, 0, 255)\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\n@pytest.mark.parametrize(\n    \"filename\",\n    (\"Tests/images/apng/split_fdat.png\", \"Tests/images/apng/split_fdat_zero_chunk.png\"),\n)\ndef test_apng_fdat(filename):\n    with Image.open(filename) as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_dispose():\n    with Image.open(\"Tests/images/apng/dispose_op_none.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_background.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/dispose_op_background_final.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_previous.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_previous_final.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_previous_first.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n\ndef test_apng_dispose_region():\n    with Image.open(\"Tests/images/apng/dispose_op_none_region.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/dispose_op_background_before_region.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/dispose_op_background_region.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 255, 255)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/dispose_op_previous_region.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_dispose_op_previous_frame():\n    # Test that the dispose settings being used are from the previous frame\n    #\n    # Image created with:\n    # red = Image.new(\"RGBA\", (128, 64), (255, 0, 0, 255))\n    # green = red.copy()\n    # green.paste(Image.new(\"RGBA\", (64, 32), (0, 255, 0, 255)))\n    # blue = red.copy()\n    # blue.paste(Image.new(\"RGBA\", (64, 32), (0, 255, 0, 255)), (64, 32))\n    #\n    # red.save(\n    #     \"Tests/images/apng/dispose_op_previous_frame.png\",\n    #     save_all=True,\n    #     append_images=[green, blue],\n    #     disposal=[\n    #         PngImagePlugin.Disposal.OP_NONE,\n    #         PngImagePlugin.Disposal.OP_PREVIOUS,\n    #         PngImagePlugin.Disposal.OP_PREVIOUS\n    #     ],\n    # )\n    with Image.open(\"Tests/images/apng/dispose_op_previous_frame.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (255, 0, 0, 255)\n\n\ndef test_apng_dispose_op_background_p_mode():\n    with Image.open(\"Tests/images/apng/dispose_op_background_p_mode.png\") as im:\n        im.seek(1)\n        im.load()\n        assert im.size == (128, 64)\n\n\ndef test_apng_blend():\n    with Image.open(\"Tests/images/apng/blend_op_source_solid.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/blend_op_source_transparent.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/blend_op_source_near_transparent.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 2)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 2)\n\n    with Image.open(\"Tests/images/apng/blend_op_over.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/blend_op_over_near_transparent.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 97)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_blend_transparency():\n    with Image.open(\"Tests/images/blend_transparency.png\") as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (255, 0, 0)\n\n\ndef test_apng_chunk_order():\n    with Image.open(\"Tests/images/apng/fctl_actl.png\") as im:\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_delay():\n    with Image.open(\"Tests/images/apng/delay.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n        im.seek(3)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(4)\n        assert im.info.get(\"duration\") == 1000.0\n\n    with Image.open(\"Tests/images/apng/delay_round.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n\n    with Image.open(\"Tests/images/apng/delay_short_max.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n\n    with Image.open(\"Tests/images/apng/delay_zero_denom.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n\n    with Image.open(\"Tests/images/apng/delay_zero_numer.png\") as im:\n        im.seek(1)\n        assert im.info.get(\"duration\") == 0.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 0.0\n        im.seek(3)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(4)\n        assert im.info.get(\"duration\") == 1000.0\n\n\ndef test_apng_num_plays():\n    with Image.open(\"Tests/images/apng/num_plays.png\") as im:\n        assert im.info.get(\"loop\") == 0\n\n    with Image.open(\"Tests/images/apng/num_plays_1.png\") as im:\n        assert im.info.get(\"loop\") == 1\n\n\ndef test_apng_mode():\n    with Image.open(\"Tests/images/apng/mode_16bit.png\") as im:\n        assert im.mode == \"RGBA\"\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (0, 0, 128, 191)\n        assert im.getpixel((64, 32)) == (0, 0, 128, 191)\n\n    with Image.open(\"Tests/images/apng/mode_grayscale.png\") as im:\n        assert im.mode == \"L\"\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == 128\n        assert im.getpixel((64, 32)) == 255\n\n    with Image.open(\"Tests/images/apng/mode_grayscale_alpha.png\") as im:\n        assert im.mode == \"LA\"\n        im.seek(im.n_frames - 1)\n        assert im.getpixel((0, 0)) == (128, 191)\n        assert im.getpixel((64, 32)) == (128, 191)\n\n    with Image.open(\"Tests/images/apng/mode_palette.png\") as im:\n        assert im.mode == \"P\"\n        im.seek(im.n_frames - 1)\n        im = im.convert(\"RGB\")\n        assert im.getpixel((0, 0)) == (0, 255, 0)\n        assert im.getpixel((64, 32)) == (0, 255, 0)\n\n    with Image.open(\"Tests/images/apng/mode_palette_alpha.png\") as im:\n        assert im.mode == \"P\"\n        im.seek(im.n_frames - 1)\n        im = im.convert(\"RGBA\")\n        assert im.getpixel((0, 0)) == (255, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (255, 0, 0, 0)\n\n    with Image.open(\"Tests/images/apng/mode_palette_1bit_alpha.png\") as im:\n        assert im.mode == \"P\"\n        im.seek(im.n_frames - 1)\n        im = im.convert(\"RGBA\")\n        assert im.getpixel((0, 0)) == (0, 0, 255, 128)\n        assert im.getpixel((64, 32)) == (0, 0, 255, 128)\n\n\ndef test_apng_chunk_errors():\n    with Image.open(\"Tests/images/apng/chunk_no_actl.png\") as im:\n        assert not im.is_animated\n\n    with pytest.warns(UserWarning):\n        with Image.open(\"Tests/images/apng/chunk_multi_actl.png\") as im:\n            im.load()\n        assert not im.is_animated\n\n    with Image.open(\"Tests/images/apng/chunk_actl_after_idat.png\") as im:\n        assert not im.is_animated\n\n    with Image.open(\"Tests/images/apng/chunk_no_fctl.png\") as im:\n        with pytest.raises(SyntaxError):\n            im.seek(im.n_frames - 1)\n\n    with Image.open(\"Tests/images/apng/chunk_repeat_fctl.png\") as im:\n        with pytest.raises(SyntaxError):\n            im.seek(im.n_frames - 1)\n\n    with Image.open(\"Tests/images/apng/chunk_no_fdat.png\") as im:\n        with pytest.raises(SyntaxError):\n            im.seek(im.n_frames - 1)\n\n\ndef test_apng_syntax_errors():\n    with pytest.warns(UserWarning):\n        with Image.open(\"Tests/images/apng/syntax_num_frames_zero.png\") as im:\n            assert not im.is_animated\n            with pytest.raises(OSError):\n                im.load()\n\n    with pytest.warns(UserWarning):\n        with Image.open(\"Tests/images/apng/syntax_num_frames_zero_default.png\") as im:\n            assert not im.is_animated\n            im.load()\n\n    # we can handle this case gracefully\n    exception = None\n    with Image.open(\"Tests/images/apng/syntax_num_frames_low.png\") as im:\n        try:\n            im.seek(im.n_frames - 1)\n        except Exception as e:\n            exception = e\n        assert exception is None\n\n    with pytest.raises(OSError):\n        with Image.open(\"Tests/images/apng/syntax_num_frames_high.png\") as im:\n            im.seek(im.n_frames - 1)\n            im.load()\n\n    with pytest.warns(UserWarning):\n        with Image.open(\"Tests/images/apng/syntax_num_frames_invalid.png\") as im:\n            assert not im.is_animated\n            im.load()\n\n\n@pytest.mark.parametrize(\n    \"test_file\",\n    (\n        \"sequence_start.png\",\n        \"sequence_gap.png\",\n        \"sequence_repeat.png\",\n        \"sequence_repeat_chunk.png\",\n        \"sequence_reorder.png\",\n        \"sequence_reorder_chunk.png\",\n        \"sequence_fdat_fctl.png\",\n    ),\n)\ndef test_apng_sequence_errors(test_file):\n    with pytest.raises(SyntaxError):\n        with Image.open(f\"Tests/images/apng/{test_file}\") as im:\n            im.seek(im.n_frames - 1)\n            im.load()\n\n\ndef test_apng_save(tmp_path):\n    with Image.open(\"Tests/images/apng/single_frame.png\") as im:\n        test_file = str(tmp_path / \"temp.png\")\n        im.save(test_file, save_all=True)\n\n    with Image.open(test_file) as im:\n        im.load()\n        assert not im.is_animated\n        assert im.n_frames == 1\n        assert im.get_format_mimetype() == \"image/png\"\n        assert im.info.get(\"default_image\") is None\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    with Image.open(\"Tests/images/apng/single_frame_default.png\") as im:\n        frames = []\n        for frame_im in ImageSequence.Iterator(im):\n            frames.append(frame_im.copy())\n        frames[0].save(\n            test_file, save_all=True, default_image=True, append_images=frames[1:]\n        )\n\n    with Image.open(test_file) as im:\n        im.load()\n        assert im.is_animated\n        assert im.n_frames == 2\n        assert im.get_format_mimetype() == \"image/apng\"\n        assert im.info.get(\"default_image\")\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_save_alpha(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n\n    im = Image.new(\"RGBA\", (1, 1), (255, 0, 0, 255))\n    im2 = Image.new(\"RGBA\", (1, 1), (255, 0, 0, 127))\n    im.save(test_file, save_all=True, append_images=[im2])\n\n    with Image.open(test_file) as reloaded:\n        assert reloaded.getpixel((0, 0)) == (255, 0, 0, 255)\n\n        reloaded.seek(1)\n        assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127)\n\n\ndef test_apng_save_split_fdat(tmp_path):\n    # test to make sure we do not generate sequence errors when writing\n    # frames with image data spanning multiple fdAT chunks (in this case\n    # both the default image and first animation frame will span multiple\n    # data chunks)\n    test_file = str(tmp_path / \"temp.png\")\n    with Image.open(\"Tests/images/old-style-jpeg-compression.png\") as im:\n        frames = [im.copy(), Image.new(\"RGBA\", im.size, (255, 0, 0, 255))]\n        im.save(\n            test_file,\n            save_all=True,\n            default_image=True,\n            append_images=frames,\n        )\n    with Image.open(test_file) as im:\n        exception = None\n        try:\n            im.seek(im.n_frames - 1)\n            im.load()\n        except Exception as e:\n            exception = e\n        assert exception is None\n\n\ndef test_apng_save_duration_loop(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    with Image.open(\"Tests/images/apng/delay.png\") as im:\n        frames = []\n        durations = []\n        loop = im.info.get(\"loop\")\n        default_image = im.info.get(\"default_image\")\n        for i, frame_im in enumerate(ImageSequence.Iterator(im)):\n            frames.append(frame_im.copy())\n            if i != 0 or not default_image:\n                durations.append(frame_im.info.get(\"duration\", 0))\n        frames[0].save(\n            test_file,\n            save_all=True,\n            default_image=default_image,\n            append_images=frames[1:],\n            duration=durations,\n            loop=loop,\n        )\n\n    with Image.open(test_file) as im:\n        im.load()\n        assert im.info.get(\"loop\") == loop\n        im.seek(1)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(2)\n        assert im.info.get(\"duration\") == 1000.0\n        im.seek(3)\n        assert im.info.get(\"duration\") == 500.0\n        im.seek(4)\n        assert im.info.get(\"duration\") == 1000.0\n\n    # test removal of duplicated frames\n    frame = Image.new(\"RGBA\", (128, 64), (255, 0, 0, 255))\n    frame.save(\n        test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]\n    )\n    with Image.open(test_file) as im:\n        assert im.n_frames == 1\n        assert \"duration\" not in im.info\n\n    different_frame = Image.new(\"RGBA\", (128, 64))\n    frame.save(\n        test_file,\n        save_all=True,\n        append_images=[frame, different_frame],\n        duration=[500, 100, 150],\n    )\n    with Image.open(test_file) as im:\n        assert im.n_frames == 2\n        assert im.info[\"duration\"] == 600\n\n        im.seek(1)\n        assert im.info[\"duration\"] == 150\n\n    # test info duration\n    frame.info[\"duration\"] = 300\n    frame.save(test_file, save_all=True, append_images=[frame, different_frame])\n    with Image.open(test_file) as im:\n        assert im.n_frames == 2\n        assert im.info[\"duration\"] == 600\n\n\ndef test_apng_save_disposal(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    size = (128, 64)\n    red = Image.new(\"RGBA\", size, (255, 0, 0, 255))\n    green = Image.new(\"RGBA\", size, (0, 255, 0, 255))\n    transparent = Image.new(\"RGBA\", size, (0, 0, 0, 0))\n\n    # test OP_NONE\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green, transparent],\n        disposal=PngImagePlugin.Disposal.OP_NONE,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test OP_BACKGROUND\n    disposal = [\n        PngImagePlugin.Disposal.OP_NONE,\n        PngImagePlugin.Disposal.OP_BACKGROUND,\n        PngImagePlugin.Disposal.OP_NONE,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[red, transparent],\n        disposal=disposal,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    disposal = [\n        PngImagePlugin.Disposal.OP_NONE,\n        PngImagePlugin.Disposal.OP_BACKGROUND,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green],\n        disposal=disposal,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test OP_PREVIOUS\n    disposal = [\n        PngImagePlugin.Disposal.OP_NONE,\n        PngImagePlugin.Disposal.OP_PREVIOUS,\n        PngImagePlugin.Disposal.OP_NONE,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green, red, transparent],\n        default_image=True,\n        disposal=disposal,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(3)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    disposal = [\n        PngImagePlugin.Disposal.OP_NONE,\n        PngImagePlugin.Disposal.OP_PREVIOUS,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green],\n        disposal=disposal,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test info disposal\n    red.info[\"disposal\"] = PngImagePlugin.Disposal.OP_BACKGROUND\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[Image.new(\"RGBA\", (10, 10), (0, 255, 0, 255))],\n    )\n    with Image.open(test_file) as im:\n        im.seek(1)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n\ndef test_apng_save_disposal_previous(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    size = (128, 64)\n    blue = Image.new(\"RGBA\", size, (0, 0, 255, 255))\n    red = Image.new(\"RGBA\", size, (255, 0, 0, 255))\n    green = Image.new(\"RGBA\", size, (0, 255, 0, 255))\n\n    # test OP_NONE\n    blue.save(\n        test_file,\n        save_all=True,\n        append_images=[red, green],\n        disposal=PngImagePlugin.Disposal.OP_PREVIOUS,\n    )\n    with Image.open(test_file) as im:\n        assert im.getpixel((0, 0)) == (0, 0, 255, 255)\n\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n\ndef test_apng_save_blend(tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n    size = (128, 64)\n    red = Image.new(\"RGBA\", size, (255, 0, 0, 255))\n    green = Image.new(\"RGBA\", size, (0, 255, 0, 255))\n    transparent = Image.new(\"RGBA\", size, (0, 0, 0, 0))\n\n    # test OP_SOURCE on solid color\n    blend = [\n        PngImagePlugin.Blend.OP_OVER,\n        PngImagePlugin.Blend.OP_SOURCE,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[red, green],\n        default_image=True,\n        disposal=PngImagePlugin.Disposal.OP_NONE,\n        blend=blend,\n    )\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test OP_SOURCE on transparent color\n    blend = [\n        PngImagePlugin.Blend.OP_OVER,\n        PngImagePlugin.Blend.OP_SOURCE,\n    ]\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[red, transparent],\n        default_image=True,\n        disposal=PngImagePlugin.Disposal.OP_NONE,\n        blend=blend,\n    )\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 0, 0, 0)\n        assert im.getpixel((64, 32)) == (0, 0, 0, 0)\n\n    # test OP_OVER\n    red.save(\n        test_file,\n        save_all=True,\n        append_images=[green, transparent],\n        default_image=True,\n        disposal=PngImagePlugin.Disposal.OP_NONE,\n        blend=PngImagePlugin.Blend.OP_OVER,\n    )\n    with Image.open(test_file) as im:\n        im.seek(1)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n        assert im.getpixel((64, 32)) == (0, 255, 0, 255)\n\n    # test info blend\n    red.info[\"blend\"] = PngImagePlugin.Blend.OP_OVER\n    red.save(test_file, save_all=True, append_images=[green, transparent])\n    with Image.open(test_file) as im:\n        im.seek(2)\n        assert im.getpixel((0, 0)) == (0, 255, 0, 255)\n\n\ndef test_seek_after_close():\n    im = Image.open(\"Tests/images/apng/delay.png\")\n    im.seek(1)\n    im.close()\n\n    with pytest.raises(ValueError):\n        im.seek(0)\n\n\n@pytest.mark.parametrize(\"mode\", (\"RGBA\", \"RGB\", \"P\"))\n@pytest.mark.parametrize(\"default_image\", (True, False))\n@pytest.mark.parametrize(\"duplicate\", (True, False))\ndef test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path):\n    test_file = str(tmp_path / \"temp.png\")\n\n    im = Image.new(\"L\", (1, 1))\n    im.save(\n        test_file,\n        save_all=True,\n        default_image=default_image,\n        append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)],\n    )\n    with Image.open(test_file) as reloaded:\n        assert reloaded.mode == mode\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 15ms\nInstalled 12 packages in 214ms\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.16s\nPrepared 1 package in 34.59s\nInstalled 1 package in 1ms\n + pillow==10.2.0.dev0 (from file:///home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_a7fe25df53b9c37af24a7a362f7ebd13ba07be6d)\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_a7fe25df53b9c37af24a7a362f7ebd13ba07be6d\nconfigfile: pyproject.toml\nplugins: cov-6.0.0\ncollected 42 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[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\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_apng_basic\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_fdat[Tests/images/apng/split_fdat.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_fdat[Tests/images/apng/split_fdat_zero_chunk.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_dispose\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_dispose_region\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_dispose_op_previous_frame\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_dispose_op_background_p_mode\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_blend\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_blend_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_chunk_order\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_delay\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_num_plays\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_mode\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_chunk_errors\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_syntax_errors\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_start.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_gap.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_repeat.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_repeat_chunk.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_reorder.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_reorder_chunk.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_fdat_fctl.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save_alpha\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save_split_fdat\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save_duration_loop\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save_disposal\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save_disposal_previous\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save_blend\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_different_modes_in_later_frames[True-True-RGBA]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[True-True-RGB]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[True-True-P]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[True-False-RGBA]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[True-False-RGB]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[True-False-P]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[False-True-RGBA]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[False-True-RGB]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[False-True-P]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[False-False-RGBA]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[False-False-RGB]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[False-False-P]\u001b[0m\n\u001b[32m============================== \u001b[32m\u001b[1m42 passed\u001b[0m\u001b[32m in 2.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_a7fe25df53b9c37af24a7a362f7ebd13ba07be6d\nconfigfile: pyproject.toml\nplugins: cov-6.0.0\ncollected 42 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[31mF\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[31mF\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[31m           [100%]\u001b[0m\n\n=================================== FAILURES ===================================\n\u001b[31m\u001b[1m________________________________ test_apng_save ________________________________\u001b[0m\n\ntmp_path = PosixPath('/tmp/pytest-of-gcpuser/pytest-513/test_apng_save0')\n\n    def test_apng_save(tmp_path):\n        with Image.open(\"Tests/images/apng/single_frame.png\") as im:\n            test_file = str(tmp_path / \"temp.png\")\n            im.save(test_file, save_all=True)\n    \n        with Image.open(test_file) as im:\n            im.load()\n            assert not im.is_animated\n            assert im.n_frames == 1\n>           assert im.get_format_mimetype() == \"image/png\"\n\u001b[1m\u001b[31mE           AssertionError: assert 'image/apng' == 'image/png'\u001b[0m\n\u001b[1m\u001b[31mE             \u001b[0m\n\u001b[1m\u001b[31mE             - image/png\u001b[0m\n\u001b[1m\u001b[31mE             + image/apng\u001b[0m\n\u001b[1m\u001b[31mE             ?       +\u001b[0m\n\n\u001b[1m\u001b[31mr2e_tests/test_1.py\u001b[0m:353: AssertionError\n\u001b[31m\u001b[1m_________________________ test_apng_save_duration_loop _________________________\u001b[0m\n\ntmp_path = PosixPath('/tmp/pytest-of-gcpuser/pytest-513/test_apng_save_duration_loop0')\n\n    def test_apng_save_duration_loop(tmp_path):\n        test_file = str(tmp_path / \"temp.png\")\n        with Image.open(\"Tests/images/apng/delay.png\") as im:\n            frames = []\n            durations = []\n            loop = im.info.get(\"loop\")\n            default_image = im.info.get(\"default_image\")\n            for i, frame_im in enumerate(ImageSequence.Iterator(im)):\n                frames.append(frame_im.copy())\n                if i != 0 or not default_image:\n                    durations.append(frame_im.info.get(\"duration\", 0))\n            frames[0].save(\n                test_file,\n                save_all=True,\n                default_image=default_image,\n                append_images=frames[1:],\n                duration=durations,\n                loop=loop,\n            )\n    \n        with Image.open(test_file) as im:\n            im.load()\n            assert im.info.get(\"loop\") == loop\n            im.seek(1)\n            assert im.info.get(\"duration\") == 500.0\n            im.seek(2)\n            assert im.info.get(\"duration\") == 1000.0\n            im.seek(3)\n            assert im.info.get(\"duration\") == 500.0\n            im.seek(4)\n            assert im.info.get(\"duration\") == 1000.0\n    \n        # test removal of duplicated frames\n        frame = Image.new(\"RGBA\", (128, 64), (255, 0, 0, 255))\n        frame.save(\n            test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]\n        )\n        with Image.open(test_file) as im:\n            assert im.n_frames == 1\n>           assert \"duration\" not in im.info\n\u001b[1m\u001b[31mE           AssertionError: assert 'duration' not in {'bbox': (0, 0, 128, 64), 'blend': 0, 'disposal': 0, 'duration': 750.0, ...}\u001b[0m\n\u001b[1m\u001b[31mE            +  where {'bbox': (0, 0, 128, 64), 'blend': 0, 'disposal': 0, 'duration': 750.0, ...} = <PIL.PngImagePlugin.PngImageFile image mode=RGBA size=128x64 at 0x7D1BD52942E0>.info\u001b[0m\n\n\u001b[1m\u001b[31mr2e_tests/test_1.py\u001b[0m:454: AssertionError\n==================================== PASSES ====================================\n\u001b[36m\u001b[1m=========================== short test summary info ============================\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_basic\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_fdat[Tests/images/apng/split_fdat.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_fdat[Tests/images/apng/split_fdat_zero_chunk.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_dispose\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_dispose_region\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_dispose_op_previous_frame\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_dispose_op_background_p_mode\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_blend\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_blend_transparency\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_chunk_order\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_delay\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_num_plays\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_mode\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_chunk_errors\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_syntax_errors\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_start.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_gap.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_repeat.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_repeat_chunk.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_reorder.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_reorder_chunk.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_sequence_errors[sequence_fdat_fctl.png]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save_alpha\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save_split_fdat\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save_disposal\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save_disposal_previous\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save_blend\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_different_modes_in_later_frames[True-True-RGBA]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[True-True-RGB]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[True-True-P]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[True-False-RGBA]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[True-False-RGB]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[True-False-P]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[False-True-RGBA]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[False-True-RGB]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[False-True-P]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[False-False-RGBA]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[False-False-RGB]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_different_modes_in_later_frames[False-False-P]\u001b[0m\n\u001b[31mFAILED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save\u001b[0m - AssertionError: assert 'image/apng' == 'image/png'\n\u001b[31mFAILED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_apng_save_duration_loop\u001b[0m - AssertionError: assert 'duration' not in {'bbox': (0, 0, 128, 64), 'blend':...\n\u001b[31m========================= \u001b[31m\u001b[1m2 failed\u001b[0m, \u001b[32m40 passed\u001b[0m\u001b[31m in 2.04s\u001b[0m\u001b[31m =========================\u001b[0m\n",
    "old_commit_res_stderr": ""
}
Tests/test_file_apng.py src/PIL/PngImagePlugin.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Incorrect MIME type assigned when saving APNG files

**Description:**
When saving an animated PNG (APNG) with a single frame and without a default image, the resulting file's MIME type is incorrectly set to `"image/apng"` instead of the standard `"image/png"`.

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

# Create an APNG with a single frame and no default image
im = Image.new("RGBA", (100, 100), (255, 0, 0, 255))
test_file = "temp.png"
im.save(test_file, save_all=True)

with Image.open(test_file) as im_saved:
    mime_type = im_saved.get_format_mimetype()
    print(mime_type)  # Outputs "image/apng" instead of "image/png"
```

**Expected Behavior:**
Saving an APNG with a single frame and no default image should set the MIME type to `"image/png"`.

**Actual Behavior:**
The MIME type is incorrectly set to `"image/apng"`.

[/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/PngImagePlugin.py
#
# The Python Imaging Library.
# $Id$
#
# PNG support code
#
# See "PNG (Portable Network Graphics) Specification, version 1.0;
# W3C Recommendation", 1996-10-01, Thomas Boutell (ed.).
#
# history:
# 1996-05-06 fl   Created (couldn't resist it)
# 1996-12-14 fl   Upgraded, added read and verify support (0.2)
# 1996-12-15 fl   Separate PNG stream parser
# 1996-12-29 fl   Added write support, added getchunks
# 1996-12-30 fl   Eliminated circular references in decoder (0.3)
# 1998-07-12 fl   Read/write 16-bit images as mode I (0.4)
# 2001-02-08 fl   Added transparency support (from Zircon) (0.5)
# 2001-04-16 fl   Don't close data source in "open" method (0.6)
# 2004-02-24 fl   Don't even pretend to support interlaced files (0.7)
# 2004-08-31 fl   Do basic sanity check on chunk identifiers (0.8)
# 2004-09-20 fl   Added PngInfo chunk container
# 2004-12-18 fl   Added DPI read support (based on code by Niki Spahiev)
# 2008-08-13 fl   Added tRNS support for RGB images
# 2009-03-06 fl   Support for preserving ICC profiles (by Florian Hoech)
# 2009-03-08 fl   Added zTXT support (from Lowell Alleman)
# 2009-03-29 fl   Read interlaced PNG files (from Conrado Porto Lopes Gouvua)
#
# Copyright (c) 1997-2009 by Secret Labs AB
# Copyright (c) 1996 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#

import itertools
import logging
import re
import struct
import warnings
import zlib
from enum import IntEnum

from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import o8
from ._binary import o16be as o16
from ._binary import o32be as o32

logger = logging.getLogger(__name__)

is_cid = re.compile(rb"\w\w\w\w").match


_MAGIC = b"\211PNG\r\n\032\n"


_MODES = {
    # supported bits/color combinations, and corresponding modes/rawmodes
    # Grayscale
    (1, 0): ("1", "1"),
    (2, 0): ("L", "L;2"),
    (4, 0): ("L", "L;4"),
    (8, 0): ("L", "L"),
    (16, 0): ("I", "I;16B"),
    # Truecolour
    (8, 2): ("RGB", "RGB"),
    (16, 2): ("RGB", "RGB;16B"),
    # Indexed-colour
    (1, 3): ("P", "P;1"),
    (2, 3): ("P", "P;2"),
    (4, 3): ("P", "P;4"),
    (8, 3): ("P", "P"),
    # Grayscale with alpha
    (8, 4): ("LA", "LA"),
    (16, 4): ("RGBA", "LA;16B"),  # LA;16B->LA not yet available
    # Truecolour with alpha
    (8, 6): ("RGBA", "RGBA"),
    (16, 6): ("RGBA", "RGBA;16B"),
}


_simple_palette = re.compile(b"^\xff*\x00\xff*$")

MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK
"""
Maximum decompressed size for a iTXt or zTXt chunk.
Eliminates decompression bombs where compressed chunks can expand 1000x.
See :ref:`Text in PNG File Format<png-text>`.
"""
MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK
"""
Set the maximum total text chunk size.
See :ref:`Text in PNG File Format<png-text>`.
"""


# APNG frame disposal modes
class Disposal(IntEnum):
    OP_NONE = 0
    """
    No disposal is done on this frame before rendering the next frame.
    See :ref:`Saving APNG sequences<apng-saving>`.
    """
    OP_BACKGROUND = 1
    """
    This frame’s modified region is cleared to fully transparent black before rendering
    the next frame.
    See :ref:`Saving APNG sequences<apng-saving>`.
    """
    OP_PREVIOUS = 2
    """
    This frame’s modified region is reverted to the previous frame’s contents before
    rendering the next frame.
    See :ref:`Saving APNG sequences<apng-saving>`.
    """


# APNG frame blend modes
class Blend(IntEnum):
    OP_SOURCE = 0
    """
    All color components of this frame, including alpha, overwrite the previous output
    image contents.
    See :ref:`Saving APNG sequences<apng-saving>`.
    """
    OP_OVER = 1
    """
    This frame should be alpha composited with the previous output image contents.
    See :ref:`Saving APNG sequences<apng-saving>`.
    """


def _safe_zlib_decompress(s):
    dobj = zlib.decompressobj()
    plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)
    if dobj.unconsumed_tail:
        msg = "Decompressed Data Too Large"
        raise ValueError(msg)
    return plaintext


def _crc32(data, seed=0):
    return zlib.crc32(data, seed) & 0xFFFFFFFF


# --------------------------------------------------------------------
# Support classes.  Suitable for PNG and related formats like MNG etc.


class ChunkStream:
    def __init__(self, fp):
        self.fp = fp
        self.queue = []

    def read(self):
        """Fetch a new chunk. Returns header information."""
        cid = None

        if self.queue:
            cid, pos, length = self.queue.pop()
            self.fp.seek(pos)
        else:
            s = self.fp.read(8)
            cid = s[4:]
            pos = self.fp.tell()
            length = i32(s)

        if not is_cid(cid):
            if not ImageFile.LOAD_TRUNCATED_IMAGES:
                msg = f"broken PNG file (chunk {repr(cid)})"
                raise SyntaxError(msg)

        return cid, pos, length

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

    def close(self):
        self.queue = self.fp = None

    def push(self, cid, pos, length):
        self.queue.append((cid, pos, length))

    def call(self, cid, pos, length):
        """Call the appropriate chunk handler"""

        logger.debug("STREAM %r %s %s", cid, pos, length)
        return getattr(self, "chunk_" + cid.decode("ascii"))(pos, length)

    def crc(self, cid, data):
        """Read and verify checksum"""

        # Skip CRC checks for ancillary chunks if allowed to load truncated
        # images
        # 5th byte of first char is 1 [specs, section 5.4]
        if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1):
            self.crc_skip(cid, data)
            return

        try:
            crc1 = _crc32(data, _crc32(cid))
            crc2 = i32(self.fp.read(4))
            if crc1 != crc2:
                msg = f"broken PNG file (bad header checksum in {repr(cid)})"
                raise SyntaxError(msg)
        except struct.error as e:
            msg = f"broken PNG file (incomplete checksum in {repr(cid)})"
            raise SyntaxError(msg) from e

    def crc_skip(self, cid, data):
        """Read checksum"""

        self.fp.read(4)

    def verify(self, endchunk=b"IEND"):
        # Simple approach; just calculate checksum for all remaining
        # blocks.  Must be called directly after open.

        cids = []

        while True:
            try:
                cid, pos, length = self.read()
            except struct.error as e:
                msg = "truncated PNG file"
                raise OSError(msg) from e

            if cid == endchunk:
                break
            self.crc(cid, ImageFile._safe_read(self.fp, length))
            cids.append(cid)

        return cids


class iTXt(str):
    """
    Subclass of string to allow iTXt chunks to look like strings while
    keeping their extra information

    """

    @staticmethod
    def __new__(cls, text, lang=None, tkey=None):
        """
        :param cls: the class to use when creating the instance
        :param text: value for this key
        :param lang: language code
        :param tkey: UTF-8 version of the key name
        """

        self = str.__new__(cls, text)
        self.lang = lang
        self.tkey = tkey
        return self


class PngInfo:
    """
    PNG chunk container (for use with save(pnginfo=))

    """

    def __init__(self):
        self.chunks = []

    def add(self, cid, data, after_idat=False):
        """Appends an arbitrary chunk. Use with caution.

        :param cid: a byte string, 4 bytes long.
        :param data: a byte string of the encoded data
        :param after_idat: for use with private chunks. Whether the chunk
                           should be written after IDAT

        """

        chunk = [cid, data]
        if after_idat:
            chunk.append(True)
        self.chunks.append(tuple(chunk))

    def add_itxt(self, key, value, lang="", tkey="", zip=False):
        """Appends an iTXt chunk.

        :param key: latin-1 encodable text key name
        :param value: value for this key
        :param lang: language code
        :param tkey: UTF-8 version of the key name
        :param zip: compression flag

        """

        if not isinstance(key, bytes):
            key = key.encode("latin-1", "strict")
        if not isinstance(value, bytes):
            value = value.encode("utf-8", "strict")
        if not isinstance(lang, bytes):
            lang = lang.encode("utf-8", "strict")
        if not isinstance(tkey, bytes):
            tkey = tkey.encode("utf-8", "strict")

        if zip:
            self.add(
                b"iTXt",
                key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value),
            )
        else:
            self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value)

    def add_text(self, key, value, zip=False):
        """Appends a text chunk.

        :param key: latin-1 encodable text key name
        :param value: value for this key, text or an
           :py:class:`PIL.PngImagePlugin.iTXt` instance
        :param zip: compression flag

        """
        if isinstance(value, iTXt):
            return self.add_itxt(key, value, value.lang, value.tkey, zip=zip)

        # The tEXt chunk stores latin-1 text
        if not isinstance(value, bytes):
            try:
                value = value.encode("latin-1", "strict")
            except UnicodeError:
                return self.add_itxt(key, value, zip=zip)

        if not isinstance(key, bytes):
            key = key.encode("latin-1", "strict")

        if zip:
            self.add(b"zTXt", key + b"\0\0" + zlib.compress(value))
        else:
            self.add(b"tEXt", key + b"\0" + value)


# --------------------------------------------------------------------
# PNG image stream (IHDR/IEND)


class PngStream(ChunkStream):
    def __init__(self, fp):
        super().__init__(fp)

        # local copies of Image attributes
        self.im_info = {}
        self.im_text = {}
        self.im_size = (0, 0)
        self.im_mode = None
        self.im_tile = None
        self.im_palette = None
        self.im_custom_mimetype = None
        self.im_n_frames = None
        self._seq_num = None
        self.rewind_state = None

        self.text_memory = 0

    def check_text_memory(self, chunklen):
        self.text_memory += chunklen
        if self.text_memory > MAX_TEXT_MEMORY:
            msg = (
                "Too much memory used in text chunks: "
                f"{self.text_memory}>MAX_TEXT_MEMORY"
            )
            raise ValueError(msg)

    def save_rewind(self):
        self.rewind_state = {
            "info": self.im_info.copy(),
            "tile": self.im_tile,
            "seq_num": self._seq_num,
        }

    def rewind(self):
        self.im_info = self.rewind_state["info"]
        self.im_tile = self.rewind_state["tile"]
        self._seq_num = self.rewind_state["seq_num"]

    def chunk_iCCP(self, pos, length):
        # ICC profile
        s = ImageFile._safe_read(self.fp, length)
        # according to PNG spec, the iCCP chunk contains:
        # Profile name  1-79 bytes (character string)
        # Null separator        1 byte (null character)
        # Compression method    1 byte (0)
        # Compressed profile    n bytes (zlib with deflate compression)
        i = s.find(b"\0")
        logger.debug("iCCP profile name %r", s[:i])
        logger.debug("Compression method %s", s[i])
        comp_method = s[i]
        if comp_method != 0:
            msg = f"Unknown compression method {comp_method} in iCCP chunk"
            raise SyntaxError(msg)
        try:
            icc_profile = _safe_zlib_decompress(s[i + 2 :])
        except ValueError:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                icc_profile = None
            else:
                raise
        except zlib.error:
            icc_profile = None  # FIXME
        self.im_info["icc_profile"] = icc_profile
        return s

    def chunk_IHDR(self, pos, length):
        # image header
        s = ImageFile._safe_read(self.fp, length)
        if length < 13:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                return s
            msg = "Truncated IHDR chunk"
            raise ValueError(msg)
        self.im_size = i32(s, 0), i32(s, 4)
        try:
            self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])]
        except Exception:
            pass
        if s[12]:
            self.im_info["interlace"] = 1
        if s[11]:
            msg = "unknown filter category"
            raise SyntaxError(msg)
        return s

    def chunk_IDAT(self, pos, length):
        # image data
        if "bbox" in self.im_info:
            tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)]
        else:
            if self.im_n_frames is not None:
                self.im_info["default_image"] = True
            tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)]
        self.im_tile = tile
        self.im_idat = length
        msg = "image data found"
        raise EOFError(msg)

    def chunk_IEND(self, pos, length):
        msg = "end of PNG image"
        raise EOFError(msg)

    def chunk_PLTE(self, pos, length):
        # palette
        s = ImageFile._safe_read(self.fp, length)
        if self.im_mode == "P":
            self.im_palette = "RGB", s
        return s

    def chunk_tRNS(self, pos, length):
        # transparency
        s = ImageFile._safe_read(self.fp, length)
        if self.im_mode == "P":
            if _simple_palette.match(s):
                # tRNS contains only one full-transparent entry,
                # other entries are full opaque
                i = s.find(b"\0")
                if i >= 0:
                    self.im_info["transparency"] = i
            else:
                # otherwise, we have a byte string with one alpha value
                # for each palette entry
                self.im_info["transparency"] = s
        elif self.im_mode in ("1", "L", "I"):
            self.im_info["transparency"] = i16(s)
        elif self.im_mode == "RGB":
            self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4)
        return s

    def chunk_gAMA(self, pos, length):
        # gamma setting
        s = ImageFile._safe_read(self.fp, length)
        self.im_info["gamma"] = i32(s) / 100000.0
        return s

    def chunk_cHRM(self, pos, length):
        # chromaticity, 8 unsigned ints, actual value is scaled by 100,000
        # WP x,y, Red x,y, Green x,y Blue x,y

        s = ImageFile._safe_read(self.fp, length)
        raw_vals = struct.unpack(">%dI" % (len(s) // 4), s)
        self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals)
        return s

    def chunk_sRGB(self, pos, length):
        # srgb rendering intent, 1 byte
        # 0 perceptual
        # 1 relative colorimetric
        # 2 saturation
        # 3 absolute colorimetric

        s = ImageFile._safe_read(self.fp, length)
        if length < 1:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                return s
            msg = "Truncated sRGB chunk"
            raise ValueError(msg)
        self.im_info["srgb"] = s[0]
        return s

    def chunk_pHYs(self, pos, length):
        # pixels per unit
        s = ImageFile._safe_read(self.fp, length)
        if length < 9:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                return s
            msg = "Truncated pHYs chunk"
            raise ValueError(msg)
        px, py = i32(s, 0), i32(s, 4)
        unit = s[8]
        if unit == 1:  # meter
            dpi = px * 0.0254, py * 0.0254
            self.im_info["dpi"] = dpi
        elif unit == 0:
            self.im_info["aspect"] = px, py
        return s

    def chunk_tEXt(self, pos, length):
        # text
        s = ImageFile._safe_read(self.fp, length)
        try:
            k, v = s.split(b"\0", 1)
        except ValueError:
            # fallback for broken tEXt tags
            k = s
            v = b""
        if k:
            k = k.decode("latin-1", "strict")
            v_str = v.decode("latin-1", "replace")

            self.im_info[k] = v if k == "exif" else v_str
            self.im_text[k] = v_str
            self.check_text_memory(len(v_str))

        return s

    def chunk_zTXt(self, pos, length):
        # compressed text
        s = ImageFile._safe_read(self.fp, length)
        try:
            k, v = s.split(b"\0", 1)
        except ValueError:
            k = s
            v = b""
        if v:
            comp_method = v[0]
        else:
            comp_method = 0
        if comp_method != 0:
            msg = f"Unknown compression method {comp_method} in zTXt chunk"
            raise SyntaxError(msg)
        try:
            v = _safe_zlib_decompress(v[1:])
        except ValueError:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                v = b""
            else:
                raise
        except zlib.error:
            v = b""

        if k:
            k = k.decode("latin-1", "strict")
            v = v.decode("latin-1", "replace")

            self.im_info[k] = self.im_text[k] = v
            self.check_text_memory(len(v))

        return s

    def chunk_iTXt(self, pos, length):
        # international text
        r = s = ImageFile._safe_read(self.fp, length)
        try:
            k, r = r.split(b"\0", 1)
        except ValueError:
            return s
        if len(r) < 2:
            return s
        cf, cm, r = r[0], r[1], r[2:]
        try:
            lang, tk, v = r.split(b"\0", 2)
        except ValueError:
            return s
        if cf != 0:
            if cm == 0:
                try:
                    v = _safe_zlib_decompress(v)
                except ValueError:
                    if ImageFile.LOAD_TRUNCATED_IMAGES:
                        return s
                    else:
                        raise
                except zlib.error:
                    return s
            else:
                return s
        try:
            k = k.decode("latin-1", "strict")
            lang = lang.decode("utf-8", "strict")
            tk = tk.decode("utf-8", "strict")
            v = v.decode("utf-8", "strict")
        except UnicodeError:
            return s

        self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk)
        self.check_text_memory(len(v))

        return s

    def chunk_eXIf(self, pos, length):
        s = ImageFile._safe_read(self.fp, length)
        self.im_info["exif"] = b"Exif\x00\x00" + s
        return s

    # APNG chunks
    def chunk_acTL(self, pos, length):
        s = ImageFile._safe_read(self.fp, length)
        if length < 8:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                return s
            msg = "APNG contains truncated acTL chunk"
            raise ValueError(msg)
        if self.im_n_frames is not None:
            self.im_n_frames = None
            warnings.warn("Invalid APNG, will use default PNG image if possible")
            return s
        n_frames = i32(s)
        if n_frames == 0 or n_frames > 0x80000000:
            warnings.warn("Invalid APNG, will use default PNG image if possible")
            return s
        self.im_n_frames = n_frames
        self.im_info["loop"] = i32(s, 4)
        self.im_custom_mimetype = "image/apng"
        return s

    def chunk_fcTL(self, pos, length):
        s = ImageFile._safe_read(self.fp, length)
        if length < 26:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                return s
            msg = "APNG contains truncated fcTL chunk"
            raise ValueError(msg)
        seq = i32(s)
        if (self._seq_num is None and seq != 0) or (
            self._seq_num is not None and self._seq_num != seq - 1
        ):
            msg = "APNG contains frame sequence errors"
            raise SyntaxError(msg)
        self._seq_num = seq
        width, height = i32(s, 4), i32(s, 8)
        px, py = i32(s, 12), i32(s, 16)
        im_w, im_h = self.im_size
        if px + width > im_w or py + height > im_h:
            msg = "APNG contains invalid frames"
            raise SyntaxError(msg)
        self.im_info["bbox"] = (px, py, px + width, py + height)
        delay_num, delay_den = i16(s, 20), i16(s, 22)
        if delay_den == 0:
            delay_den = 100
        self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000
        self.im_info["disposal"] = s[24]
        self.im_info["blend"] = s[25]
        return s

    def chunk_fdAT(self, pos, length):
        if length < 4:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                s = ImageFile._safe_read(self.fp, length)
                return s
            msg = "APNG contains truncated fDAT chunk"
            raise ValueError(msg)
        s = ImageFile._safe_read(self.fp, 4)
        seq = i32(s)
        if self._seq_num != seq - 1:
            msg = "APNG contains frame sequence errors"
            raise SyntaxError(msg)
        self._seq_num = seq
        return self.chunk_IDAT(pos + 4, length - 4)


# --------------------------------------------------------------------
# PNG reader


def _accept(prefix):
    return prefix[:8] == _MAGIC


##
# Image plugin for PNG images.


class PngImageFile(ImageFile.ImageFile):
    format = "PNG"
    format_description = "Portable network graphics"

    def _open(self):
        if not _accept(self.fp.read(8)):
            msg = "not a PNG file"
            raise SyntaxError(msg)
        self._fp = self.fp
        self.__frame = 0

        #
        # Parse headers up to the first IDAT or fDAT chunk

        self.private_chunks = []
        self.png = PngStream(self.fp)

        while True:
            #
            # get next chunk

            cid, pos, length = self.png.read()

            try:
                s = self.png.call(cid, pos, length)
            except EOFError:
                break
            except AttributeError:
                logger.debug("%r %s %s (unknown)", cid, pos, length)
                s = ImageFile._safe_read(self.fp, length)
                if cid[1:2].islower():
                    self.private_chunks.append((cid, s))

            self.png.crc(cid, s)

        #
        # Copy relevant attributes from the PngStream.  An alternative
        # would be to let the PngStream class modify these attributes
        # directly, but that introduces circular references which are
        # difficult to break if things go wrong in the decoder...
        # (believe me, I've tried ;-)

        self._mode = self.png.im_mode
        self._size = self.png.im_size
        self.info = self.png.im_info
        self._text = None
        self.tile = self.png.im_tile
        self.custom_mimetype = self.png.im_custom_mimetype
        self.n_frames = self.png.im_n_frames or 1
        self.default_image = self.info.get("default_image", False)

        if self.png.im_palette:
            rawmode, data = self.png.im_palette
            self.palette = ImagePalette.raw(rawmode, data)

        if cid == b"fdAT":
            self.__prepare_idat = length - 4
        else:
            self.__prepare_idat = length  # used by load_prepare()

        if self.png.im_n_frames is not None:
            self._close_exclusive_fp_after_loading = False
            self.png.save_rewind()
            self.__rewind_idat = self.__prepare_idat
            self.__rewind = self._fp.tell()
            if self.default_image:
                # IDAT chunk contains default image and not first animation frame
                self.n_frames += 1
            self._seek(0)
        self.is_animated = self.n_frames > 1

    @property
    def text(self):
        # experimental
        if self._text is None:
            # iTxt, tEXt and zTXt chunks may appear at the end of the file
            # So load the file to ensure that they are read
            if self.is_animated:
                frame = self.__frame
                # for APNG, seek to the final frame before loading
                self.seek(self.n_frames - 1)
            self.load()
            if self.is_animated:
                self.seek(frame)
        return self._text

    def verify(self):
        """Verify PNG file"""

        if self.fp is None:
            msg = "verify must be called directly after open"
            raise RuntimeError(msg)

        # back up to beginning of IDAT block
        self.fp.seek(self.tile[0][2] - 8)

        self.png.verify()
        self.png.close()

        if self._exclusive_fp:
            self.fp.close()
        self.fp = None

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

        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 APNG file"
                raise EOFError(msg) from e

    def _seek(self, frame, rewind=False):
        if frame == 0:
            if rewind:
                self._fp.seek(self.__rewind)
                self.png.rewind()
                self.__prepare_idat = self.__rewind_idat
                self.im = None
                if self.pyaccess:
                    self.pyaccess = None
                self.info = self.png.im_info
                self.tile = self.png.im_tile
                self.fp = self._fp
            self._prev_im = None
            self.dispose = None
            self.default_image = self.info.get("default_image", False)
            self.dispose_op = self.info.get("disposal")
            self.blend_op = self.info.get("blend")
            self.dispose_extent = self.info.get("bbox")
            self.__frame = 0
        else:
            if frame != self.__frame + 1:
                msg = f"cannot seek to frame {frame}"
                raise ValueError(msg)

            # ensure previous frame was loaded
            self.load()

            if self.dispose:
                self.im.paste(self.dispose, self.dispose_extent)
            self._prev_im = self.im.copy()

            self.fp = self._fp

            # advance to the next frame
            if self.__prepare_idat:
                ImageFile._safe_read(self.fp, self.__prepare_idat)
                self.__prepare_idat = 0
            frame_start = False
            while True:
                self.fp.read(4)  # CRC

                try:
                    cid, pos, length = self.png.read()
                except (struct.error, SyntaxError):
                    break

                if cid == b"IEND":
                    msg = "No more images in APNG file"
                    raise EOFError(msg)
                if cid == b"fcTL":
                    if frame_start:
                        # there must be at least one fdAT chunk between fcTL chunks
                        msg = "APNG missing frame data"
                        raise SyntaxError(msg)
                    frame_start = True

                try:
                    self.png.call(cid, pos, length)
                except UnicodeDecodeError:
                    break
                except EOFError:
                    if cid == b"fdAT":
                        length -= 4
                        if frame_start:
                            self.__prepare_idat = length
                            break
                    ImageFile._safe_read(self.fp, length)
                except AttributeError:
                    logger.debug("%r %s %s (unknown)", cid, pos, length)
                    ImageFile._safe_read(self.fp, length)

            self.__frame = frame
            self.tile = self.png.im_tile
            self.dispose_op = self.info.get("disposal")
            self.blend_op = self.info.get("blend")
            self.dispose_extent = self.info.get("bbox")

            if not self.tile:
                msg = "image not found in APNG frame"
                raise EOFError(msg)

        # setup frame disposal (actual disposal done when needed in the next _seek())
        if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:
            self.dispose_op = Disposal.OP_BACKGROUND

        if self.dispose_op == Disposal.OP_PREVIOUS:
            self.dispose = self._prev_im.copy()
            self.dispose = self._crop(self.dispose, self.dispose_extent)
        elif self.dispose_op == Disposal.OP_BACKGROUND:
            self.dispose = Image.core.fill(self.mode, self.size)
            self.dispose = self._crop(self.dispose, self.dispose_extent)
        else:
            self.dispose = None

    def tell(self):
        return self.__frame

    def load_prepare(self):
        """internal: prepare to read PNG file"""

        if self.info.get("interlace"):
            self.decoderconfig = self.decoderconfig + (1,)

        self.__idat = self.__prepare_idat  # used by load_read()
        ImageFile.ImageFile.load_prepare(self)

    def load_read(self, read_bytes):
        """internal: read more image data"""

        while self.__idat == 0:
            # end of chunk, skip forward to next one

            self.fp.read(4)  # CRC

            cid, pos, length = self.png.read()

            if cid not in [b"IDAT", b"DDAT", b"fdAT"]:
                self.png.push(cid, pos, length)
                return b""

            if cid == b"fdAT":
                try:
                    self.png.call(cid, pos, length)
                except EOFError:
                    pass
                self.__idat = length - 4  # sequence_num has already been read
            else:
                self.__idat = length  # empty chunks are allowed

        # read more data from this chunk
        if read_bytes <= 0:
            read_bytes = self.__idat
        else:
            read_bytes = min(read_bytes, self.__idat)

        self.__idat = self.__idat - read_bytes

        return self.fp.read(read_bytes)

    def load_end(self):
        """internal: finished reading image data"""
        if self.__idat != 0:
            self.fp.read(self.__idat)
        while True:
            self.fp.read(4)  # CRC

            try:
                cid, pos, length = self.png.read()
            except (struct.error, SyntaxError):
                break

            if cid == b"IEND":
                break
            elif cid == b"fcTL" and self.is_animated:
                # start of the next frame, stop reading
                self.__prepare_idat = 0
                self.png.push(cid, pos, length)
                break

            try:
                self.png.call(cid, pos, length)
            except UnicodeDecodeError:
                break
            except EOFError:
                if cid == b"fdAT":
                    length -= 4
                ImageFile._safe_read(self.fp, length)
            except AttributeError:
                logger.debug("%r %s %s (unknown)", cid, pos, length)
                s = ImageFile._safe_read(self.fp, length)
                if cid[1:2].islower():
                    self.private_chunks.append((cid, s, True))
        self._text = self.png.im_text
        if not self.is_animated:
            self.png.close()
            self.png = None
        else:
            if self._prev_im and self.blend_op == Blend.OP_OVER:
                updated = self._crop(self.im, self.dispose_extent)
                if self.im.mode == "RGB" and "transparency" in self.info:
                    mask = updated.convert_transparent(
                        "RGBA", self.info["transparency"]
                    )
                else:
                    mask = updated.convert("RGBA")
                self._prev_im.paste(updated, self.dispose_extent, mask)
                self.im = self._prev_im
                if self.pyaccess:
                    self.pyaccess = None

    def _getexif(self):
        if "exif" not in self.info:
            self.load()
        if "exif" not in self.info and "Raw profile type exif" not in self.info:
            return None
        return self.getexif()._get_merged_dict()

    def getexif(self):
        if "exif" not in self.info:
            self.load()

        return super().getexif()

    def getxmp(self):
        """
        Returns a dictionary containing the XMP tags.
        Requires defusedxml to be installed.

        :returns: XMP tags in a dictionary.
        """
        return (
            self._getxmp(self.info["XML:com.adobe.xmp"])
            if "XML:com.adobe.xmp" in self.info
            else {}
        )


# --------------------------------------------------------------------
# PNG writer

_OUTMODES = {
    # supported PIL modes, and corresponding rawmodes/bits/color combinations
    "1": ("1", b"\x01\x00"),
    "L;1": ("L;1", b"\x01\x00"),
    "L;2": ("L;2", b"\x02\x00"),
    "L;4": ("L;4", b"\x04\x00"),
    "L": ("L", b"\x08\x00"),
    "LA": ("LA", b"\x08\x04"),
    "I": ("I;16B", b"\x10\x00"),
    "I;16": ("I;16B", b"\x10\x00"),
    "I;16B": ("I;16B", b"\x10\x00"),
    "P;1": ("P;1", b"\x01\x03"),
    "P;2": ("P;2", b"\x02\x03"),
    "P;4": ("P;4", b"\x04\x03"),
    "P": ("P", b"\x08\x03"),
    "RGB": ("RGB", b"\x08\x02"),
    "RGBA": ("RGBA", b"\x08\x06"),
}


def putchunk(fp, cid, *data):
    """Write a PNG chunk (including CRC field)"""

    data = b"".join(data)

    fp.write(o32(len(data)) + cid)
    fp.write(data)
    crc = _crc32(data, _crc32(cid))
    fp.write(o32(crc))


class _idat:
    # wrap output from the encoder in IDAT chunks

    def __init__(self, fp, chunk):
        self.fp = fp
        self.chunk = chunk

    def write(self, data):
        self.chunk(self.fp, b"IDAT", data)


class _fdat:
    # wrap encoder output in fdAT chunks

    def __init__(self, fp, chunk, seq_num):
        self.fp = fp
        self.chunk = chunk
        self.seq_num = seq_num

    def write(self, data):
        self.chunk(self.fp, b"fdAT", o32(self.seq_num), data)
        self.seq_num += 1


def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images):
    duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
    loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
    disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
    blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))

    if default_image:
        chain = itertools.chain(append_images)
    else:
        chain = itertools.chain([im], append_images)

    im_frames = []
    frame_count = 0
    for im_seq in chain:
        for im_frame in ImageSequence.Iterator(im_seq):
            if im_frame.mode == rawmode:
                im_frame = im_frame.copy()
            else:
                im_frame = im_frame.convert(rawmode)
            encoderinfo = im.encoderinfo.copy()
            if isinstance(duration, (list, tuple)):
                encoderinfo["duration"] = duration[frame_count]
            if isinstance(disposal, (list, tuple)):
                encoderinfo["disposal"] = disposal[frame_count]
            if isinstance(blend, (list, tuple)):
                encoderinfo["blend"] = blend[frame_count]
            frame_count += 1

            if im_frames:
                previous = im_frames[-1]
                prev_disposal = previous["encoderinfo"].get("disposal")
                prev_blend = previous["encoderinfo"].get("blend")
                if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:
                    prev_disposal = Disposal.OP_BACKGROUND

                if prev_disposal == Disposal.OP_BACKGROUND:
                    base_im = previous["im"].copy()
                    dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0))
                    bbox = previous["bbox"]
                    if bbox:
                        dispose = dispose.crop(bbox)
                    else:
                        bbox = (0, 0) + im.size
                    base_im.paste(dispose, bbox)
                elif prev_disposal == Disposal.OP_PREVIOUS:
                    base_im = im_frames[-2]["im"]
                else:
                    base_im = previous["im"]
                delta = ImageChops.subtract_modulo(
                    im_frame.convert("RGBA"), base_im.convert("RGBA")
                )
                bbox = delta.getbbox(alpha_only=False)
                if (
                    not bbox
                    and prev_disposal == encoderinfo.get("disposal")
                    and prev_blend == encoderinfo.get("blend")
                ):
                    previous["encoderinfo"]["duration"] += encoderinfo.get(
                        "duration", duration
                    )
                    continue
            else:
                bbox = None
            if "duration" not in encoderinfo:
                encoderinfo["duration"] = duration
            im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})

    # animation control
    chunk(
        fp,
        b"acTL",
        o32(len(im_frames)),  # 0: num_frames
        o32(loop),  # 4: num_plays
    )

    # default image IDAT (if it exists)
    if default_image:
        if im.mode != rawmode:
            im = im.convert(rawmode)
        ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])

    seq_num = 0
    for frame, frame_data in enumerate(im_frames):
        im_frame = frame_data["im"]
        if not frame_data["bbox"]:
            bbox = (0, 0) + im_frame.size
        else:
            bbox = frame_data["bbox"]
            im_frame = im_frame.crop(bbox)
        size = im_frame.size
        encoderinfo = frame_data["encoderinfo"]
        frame_duration = int(round(encoderinfo["duration"]))
        frame_disposal = encoderinfo.get("disposal", disposal)
        frame_blend = encoderinfo.get("blend", blend)
        # frame control
        chunk(
            fp,
            b"fcTL",
            o32(seq_num),  # sequence_number
            o32(size[0]),  # width
            o32(size[1]),  # height
            o32(bbox[0]),  # x_offset
            o32(bbox[1]),  # y_offset
            o16(frame_duration),  # delay_numerator
            o16(1000),  # delay_denominator
            o8(frame_disposal),  # dispose_op
            o8(frame_blend),  # blend_op
        )
        seq_num += 1
        # frame data
        if frame == 0 and not default_image:
            # first frame must be in IDAT chunks for backwards compatibility
            ImageFile._save(
                im_frame,
                _idat(fp, chunk),
                [("zip", (0, 0) + im_frame.size, 0, rawmode)],
            )
        else:
            fdat_chunks = _fdat(fp, chunk, seq_num)
            ImageFile._save(
                im_frame,
                fdat_chunks,
                [("zip", (0, 0) + im_frame.size, 0, rawmode)],
            )
            seq_num = fdat_chunks.seq_num


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


def _save(im, fp, filename, chunk=putchunk, save_all=False):
    # save an image to disk (called by the save method)

    if save_all:
        default_image = im.encoderinfo.get(
            "default_image", im.info.get("default_image")
        )
        modes = set()
        append_images = im.encoderinfo.get("append_images", [])
        for im_seq in itertools.chain([im], append_images):
            for im_frame in ImageSequence.Iterator(im_seq):
                modes.add(im_frame.mode)
        for mode in ("RGBA", "RGB", "P"):
            if mode in modes:
                break
        else:
            mode = modes.pop()
    else:
        mode = im.mode

    if mode == "P":
        #
        # attempt to minimize storage requirements for palette images
        if "bits" in im.encoderinfo:
            # number of bits specified by user
            colors = min(1 << im.encoderinfo["bits"], 256)
        else:
            # check palette contents
            if im.palette:
                colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1)
            else:
                colors = 256

        if colors <= 16:
            if colors <= 2:
                bits = 1
            elif colors <= 4:
                bits = 2
            else:
                bits = 4
            mode = f"{mode};{bits}"

    # encoder options
    im.encoderconfig = (
        im.encoderinfo.get("optimize", False),
        im.encoderinfo.get("compress_level", -1),
        im.encoderinfo.get("compress_type", -1),
        im.encoderinfo.get("dictionary", b""),
    )

    # get the corresponding PNG mode
    try:
        rawmode, mode = _OUTMODES[mode]
    except KeyError as e:
        msg = f"cannot write mode {mode} as PNG"
        raise OSError(msg) from e

    #
    # write minimal PNG file

    fp.write(_MAGIC)

    chunk(
        fp,
        b"IHDR",
        o32(im.size[0]),  # 0: size
        o32(im.size[1]),
        mode,  # 8: depth/type
        b"\0",  # 10: compression
        b"\0",  # 11: filter category
        b"\0",  # 12: interlace flag
    )

    chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"]

    icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
    if icc:
        # ICC profile
        # according to PNG spec, the iCCP chunk contains:
        # Profile name  1-79 bytes (character string)
        # Null separator        1 byte (null character)
        # Compression method    1 byte (0)
        # Compressed profile    n bytes (zlib with deflate compression)
        name = b"ICC Profile"
        data = name + b"\0\0" + zlib.compress(icc)
        chunk(fp, b"iCCP", data)

        # You must either have sRGB or iCCP.
        # Disallow sRGB chunks when an iCCP-chunk has been emitted.
        chunks.remove(b"sRGB")

    info = im.encoderinfo.get("pnginfo")
    if info:
        chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"]
        for info_chunk in info.chunks:
            cid, data = info_chunk[:2]
            if cid in chunks:
                chunks.remove(cid)
                chunk(fp, cid, data)
            elif cid in chunks_multiple_allowed:
                chunk(fp, cid, data)
            elif cid[1:2].islower():
                # Private chunk
                after_idat = info_chunk[2:3]
                if not after_idat:
                    chunk(fp, cid, data)

    if im.mode == "P":
        palette_byte_number = colors * 3
        palette_bytes = im.im.getpalette("RGB")[:palette_byte_number]
        while len(palette_bytes) < palette_byte_number:
            palette_bytes += b"\0"
        chunk(fp, b"PLTE", palette_bytes)

    transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None))

    if transparency or transparency == 0:
        if im.mode == "P":
            # limit to actual palette size
            alpha_bytes = colors
            if isinstance(transparency, bytes):
                chunk(fp, b"tRNS", transparency[:alpha_bytes])
            else:
                transparency = max(0, min(255, transparency))
                alpha = b"\xFF" * transparency + b"\0"
                chunk(fp, b"tRNS", alpha[:alpha_bytes])
        elif im.mode in ("1", "L", "I"):
            transparency = max(0, min(65535, transparency))
            chunk(fp, b"tRNS", o16(transparency))
        elif im.mode == "RGB":
            red, green, blue = transparency
            chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue))
        else:
            if "transparency" in im.encoderinfo:
                # don't bother with transparency if it's an RGBA
                # and it's in the info dict. It's probably just stale.
                msg = "cannot use transparency for this mode"
                raise OSError(msg)
    else:
        if im.mode == "P" and im.im.getpalettemode() == "RGBA":
            alpha = im.im.getpalette("RGBA", "A")
            alpha_bytes = colors
            chunk(fp, b"tRNS", alpha[:alpha_bytes])

    dpi = im.encoderinfo.get("dpi")
    if dpi:
        chunk(
            fp,
            b"pHYs",
            o32(int(dpi[0] / 0.0254 + 0.5)),
            o32(int(dpi[1] / 0.0254 + 0.5)),
            b"\x01",
        )

    if info:
        chunks = [b"bKGD", b"hIST"]
        for info_chunk in info.chunks:
            cid, data = info_chunk[:2]
            if cid in chunks:
                chunks.remove(cid)
                chunk(fp, cid, data)

    exif = im.encoderinfo.get("exif")
    if exif:
        if isinstance(exif, Image.Exif):
            exif = exif.tobytes(8)
        if exif.startswith(b"Exif\x00\x00"):
            exif = exif[6:]
        chunk(fp, b"eXIf", exif)

    if save_all:
        _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
    else:
        ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])

    if info:
        for info_chunk in info.chunks:
            cid, data = info_chunk[:2]
            if cid[1:2].islower():
                # Private chunk
                after_idat = info_chunk[2:3]
                if after_idat:
                    chunk(fp, cid, data)

    chunk(fp, b"IEND", b"")

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


# --------------------------------------------------------------------
# PNG chunk converter


def getchunks(im, **params):
    """Return a list of PNG chunks representing this image."""

    class collector:
        data = []

        def write(self, data):
            pass

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

    def append(fp, cid, *data):
        data = b"".join(data)
        crc = o32(_crc32(data, _crc32(cid)))
        fp.append((cid, data, crc))

    fp = collector()

    try:
        im.encoderinfo = params
        _save(im, fp, None, append)
    finally:
        del im.encoderinfo

    return fp.data


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

Image.register_open(PngImageFile.format, PngImageFile, _accept)
Image.register_save(PngImageFile.format, _save)
Image.register_save_all(PngImageFile.format, _save_all)

Image.register_extensions(PngImageFile.format, [".png", ".apng"])

Image.register_mime(PngImageFile.format, "image/png")

```
--- 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:** Incorrect MIME type assigned when saving APNG files

**Description:**
When saving an animated PNG (APNG) with a single frame and without a default image, the resulting file's MIME type is incorrectly set to `"image/apng"` instead of the standard `"image/png"`.

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

# Create an APNG with a single frame and no default image
im = Image.new("RGBA", (100, 100), (255, 0, 0, 255))
test_file = "temp.png"
im.save(test_file, save_all=True)

with Image.open(test_file) as im_saved:
    mime_type = im_saved.get_format_mimetype()
    print(mime_type)  # Outputs "image/apng" instead of "image/png"
```

**Expected Behavior:**
Saving an APNG with a single frame and no default image should set the MIME type to `"image/png"`.

**Actual Behavior:**
The MIME type is incorrectly set to `"image/apng"`.

[/ISSUE]
{
    "\u001b[1mtest_apng_basic\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_fdat[Tests/images/apng/split_fdat.png]\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_fdat[Tests/images/apng/split_fdat_zero_chunk.png]\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_dispose\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_dispose_region\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_dispose_op_previous_frame\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_dispose_op_background_p_mode\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_blend\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_blend_transparency\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_chunk_order\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_delay\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_num_plays\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_mode\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_chunk_errors\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_syntax_errors\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_sequence_errors[sequence_start.png]\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_sequence_errors[sequence_gap.png]\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_sequence_errors[sequence_repeat.png]\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_sequence_errors[sequence_repeat_chunk.png]\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_sequence_errors[sequence_reorder.png]\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_sequence_errors[sequence_reorder_chunk.png]\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_sequence_errors[sequence_fdat_fctl.png]\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_save\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_save_alpha\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_save_split_fdat\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_save_duration_loop\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_save_disposal\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_save_disposal_previous\u001b[0m": "PASSED",
    "\u001b[1mtest_apng_save_blend\u001b[0m": "PASSED",
    "\u001b[1mtest_seek_after_close\u001b[0m": "PASSED",
    "\u001b[1mtest_different_modes_in_later_frames[True-True-RGBA]\u001b[0m": "PASSED",
    "\u001b[1mtest_different_modes_in_later_frames[True-True-RGB]\u001b[0m": "PASSED",
    "\u001b[1mtest_different_modes_in_later_frames[True-True-P]\u001b[0m": "PASSED",
    "\u001b[1mtest_different_modes_in_later_frames[True-False-RGBA]\u001b[0m": "PASSED",
    "\u001b[1mtest_different_modes_in_later_frames[True-False-RGB]\u001b[0m": "PASSED",
    "\u001b[1mtest_different_modes_in_later_frames[True-False-P]\u001b[0m": "PASSED",
    "\u001b[1mtest_different_modes_in_later_frames[False-True-RGBA]\u001b[0m": "PASSED",
    "\u001b[1mtest_different_modes_in_later_frames[False-True-RGB]\u001b[0m": "PASSED",
    "\u001b[1mtest_different_modes_in_later_frames[False-True-P]\u001b[0m": "PASSED",
    "\u001b[1mtest_different_modes_in_later_frames[False-False-RGBA]\u001b[0m": "PASSED",
    "\u001b[1mtest_different_modes_in_later_frames[False-False-RGB]\u001b[0m": "PASSED",
    "\u001b[1mtest_different_modes_in_later_frames[False-False-P]\u001b[0m": "PASSED"
}
diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py
index 1bd0f442f..dbcdee1c2 100644
--- a/src/PIL/PngImagePlugin.py
+++ b/src/PIL/PngImagePlugin.py
@@ -1156,6 +1156,9 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
                 encoderinfo["duration"] = duration
             im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
 
+    if len(im_frames) == 1 and not default_image:
+        return im_frames[0]["im"]
+
     # animation control
     chunk(
         fp,
@@ -1391,8 +1394,10 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
         chunk(fp, b"eXIf", exif)
 
     if save_all:
-        _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
-    else:
+        im = _write_multiple_frames(
+            im, fp, chunk, rawmode, default_image, append_images
+        )
+    if im:
         ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
 
     if info:
 

{"src/PIL/PngImagePlugin.py": "#\n# The Python Imaging Library.\n# $Id$\n#\n# PNG support code\n#\n# See \"PNG (Portable Network Graphics) Specification, version 1.0;\n# W3C Recommendation\", 1996-10-01, Thomas Boutell (ed.).\n#\n# history:\n# 1996-05-06 fl   Created (couldn't resist it)\n# 1996-12-14 fl   Upgraded, added read and verify support (0.2)\n# 1996-12-15 fl   Separate PNG stream parser\n# 1996-12-29 fl   Added write support, added getchunks\n# 1996-12-30 fl   Eliminated circular references in decoder (0.3)\n# 1998-07-12 fl   Read/write 16-bit images as mode I (0.4)\n# 2001-02-08 fl   Added transparency support (from Zircon) (0.5)\n# 2001-04-16 fl   Don't close data source in \"open\" method (0.6)\n# 2004-02-24 fl   Don't even pretend to support interlaced files (0.7)\n# 2004-08-31 fl   Do basic sanity check on chunk identifiers (0.8)\n# 2004-09-20 fl   Added PngInfo chunk container\n# 2004-12-18 fl   Added DPI read support (based on code by Niki Spahiev)\n# 2008-08-13 fl   Added tRNS support for RGB images\n# 2009-03-06 fl   Support for preserving ICC profiles (by Florian Hoech)\n# 2009-03-08 fl   Added zTXT support (from Lowell Alleman)\n# 2009-03-29 fl   Read interlaced PNG files (from Conrado Porto Lopes Gouvua)\n#\n# Copyright (c) 1997-2009 by Secret Labs AB\n# Copyright (c) 1996 by Fredrik Lundh\n#\n# See the README file for information on usage and redistribution.\n#\n\nimport itertools\nimport logging\nimport re\nimport struct\nimport warnings\nimport zlib\nfrom enum import IntEnum\n\nfrom . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence\nfrom ._binary import i16be as i16\nfrom ._binary import i32be as i32\nfrom ._binary import o8\nfrom ._binary import o16be as o16\nfrom ._binary import o32be as o32\n\nlogger = logging.getLogger(__name__)\n\nis_cid = re.compile(rb\"\\w\\w\\w\\w\").match\n\n\n_MAGIC = b\"\\211PNG\\r\\n\\032\\n\"\n\n\n_MODES = {\n    # supported bits/color combinations, and corresponding modes/rawmodes\n    # Grayscale\n    (1, 0): (\"1\", \"1\"),\n    (2, 0): (\"L\", \"L;2\"),\n    (4, 0): (\"L\", \"L;4\"),\n    (8, 0): (\"L\", \"L\"),\n    (16, 0): (\"I\", \"I;16B\"),\n    # Truecolour\n    (8, 2): (\"RGB\", \"RGB\"),\n    (16, 2): (\"RGB\", \"RGB;16B\"),\n    # Indexed-colour\n    (1, 3): (\"P\", \"P;1\"),\n    (2, 3): (\"P\", \"P;2\"),\n    (4, 3): (\"P\", \"P;4\"),\n    (8, 3): (\"P\", \"P\"),\n    # Grayscale with alpha\n    (8, 4): (\"LA\", \"LA\"),\n    (16, 4): (\"RGBA\", \"LA;16B\"),  # LA;16B->LA not yet available\n    # Truecolour with alpha\n    (8, 6): (\"RGBA\", \"RGBA\"),\n    (16, 6): (\"RGBA\", \"RGBA;16B\"),\n}\n\n\n_simple_palette = re.compile(b\"^\\xff*\\x00\\xff*$\")\n\nMAX_TEXT_CHUNK = ImageFile.SAFEBLOCK\n\"\"\"\nMaximum decompressed size for a iTXt or zTXt chunk.\nEliminates decompression bombs where compressed chunks can expand 1000x.\nSee :ref:`Text in PNG File Format<png-text>`.\n\"\"\"\nMAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK\n\"\"\"\nSet the maximum total text chunk size.\nSee :ref:`Text in PNG File Format<png-text>`.\n\"\"\"\n\n\n# APNG frame disposal modes\nclass Disposal(IntEnum):\n    OP_NONE = 0\n    \"\"\"\n    No disposal is done on this frame before rendering the next frame.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n    OP_BACKGROUND = 1\n    \"\"\"\n    This frame\u2019s modified region is cleared to fully transparent black before rendering\n    the next frame.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n    OP_PREVIOUS = 2\n    \"\"\"\n    This frame\u2019s modified region is reverted to the previous frame\u2019s contents before\n    rendering the next frame.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n\n\n# APNG frame blend modes\nclass Blend(IntEnum):\n    OP_SOURCE = 0\n    \"\"\"\n    All color components of this frame, including alpha, overwrite the previous output\n    image contents.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n    OP_OVER = 1\n    \"\"\"\n    This frame should be alpha composited with the previous output image contents.\n    See :ref:`Saving APNG sequences<apng-saving>`.\n    \"\"\"\n\n\ndef _safe_zlib_decompress(s):\n    dobj = zlib.decompressobj()\n    plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)\n    if dobj.unconsumed_tail:\n        msg = \"Decompressed Data Too Large\"\n        raise ValueError(msg)\n    return plaintext\n\n\ndef _crc32(data, seed=0):\n    return zlib.crc32(data, seed) & 0xFFFFFFFF\n\n\n# --------------------------------------------------------------------\n# Support classes.  Suitable for PNG and related formats like MNG etc.\n\n\nclass ChunkStream:\n    def __init__(self, fp):\n        self.fp = fp\n        self.queue = []\n\n    def read(self):\n        \"\"\"Fetch a new chunk. Returns header information.\"\"\"\n        cid = None\n\n        if self.queue:\n            cid, pos, length = self.queue.pop()\n            self.fp.seek(pos)\n        else:\n            s = self.fp.read(8)\n            cid = s[4:]\n            pos = self.fp.tell()\n            length = i32(s)\n\n        if not is_cid(cid):\n            if not ImageFile.LOAD_TRUNCATED_IMAGES:\n                msg = f\"broken PNG file (chunk {repr(cid)})\"\n                raise SyntaxError(msg)\n\n        return cid, pos, length\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, *args):\n        self.close()\n\n    def close(self):\n        self.queue = self.fp = None\n\n    def push(self, cid, pos, length):\n        self.queue.append((cid, pos, length))\n\n    def call(self, cid, pos, length):\n        \"\"\"Call the appropriate chunk handler\"\"\"\n\n        logger.debug(\"STREAM %r %s %s\", cid, pos, length)\n        return getattr(self, \"chunk_\" + cid.decode(\"ascii\"))(pos, length)\n\n    def crc(self, cid, data):\n        \"\"\"Read and verify checksum\"\"\"\n\n        # Skip CRC checks for ancillary chunks if allowed to load truncated\n        # images\n        # 5th byte of first char is 1 [specs, section 5.4]\n        if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1):\n            self.crc_skip(cid, data)\n            return\n\n        try:\n            crc1 = _crc32(data, _crc32(cid))\n            crc2 = i32(self.fp.read(4))\n            if crc1 != crc2:\n                msg = f\"broken PNG file (bad header checksum in {repr(cid)})\"\n                raise SyntaxError(msg)\n        except struct.error as e:\n            msg = f\"broken PNG file (incomplete checksum in {repr(cid)})\"\n            raise SyntaxError(msg) from e\n\n    def crc_skip(self, cid, data):\n        \"\"\"Read checksum\"\"\"\n\n        self.fp.read(4)\n\n    def verify(self, endchunk=b\"IEND\"):\n        # Simple approach; just calculate checksum for all remaining\n        # blocks.  Must be called directly after open.\n\n        cids = []\n\n        while True:\n            try:\n                cid, pos, length = self.read()\n            except struct.error as e:\n                msg = \"truncated PNG file\"\n                raise OSError(msg) from e\n\n            if cid == endchunk:\n                break\n            self.crc(cid, ImageFile._safe_read(self.fp, length))\n            cids.append(cid)\n\n        return cids\n\n\nclass iTXt(str):\n    \"\"\"\n    Subclass of string to allow iTXt chunks to look like strings while\n    keeping their extra information\n\n    \"\"\"\n\n    @staticmethod\n    def __new__(cls, text, lang=None, tkey=None):\n        \"\"\"\n        :param cls: the class to use when creating the instance\n        :param text: value for this key\n        :param lang: language code\n        :param tkey: UTF-8 version of the key name\n        \"\"\"\n\n        self = str.__new__(cls, text)\n        self.lang = lang\n        self.tkey = tkey\n        return self\n\n\nclass PngInfo:\n    \"\"\"\n    PNG chunk container (for use with save(pnginfo=))\n\n    \"\"\"\n\n    def __init__(self):\n        self.chunks = []\n\n    def add(self, cid, data, after_idat=False):\n        \"\"\"Appends an arbitrary chunk. Use with caution.\n\n        :param cid: a byte string, 4 bytes long.\n        :param data: a byte string of the encoded data\n        :param after_idat: for use with private chunks. Whether the chunk\n                           should be written after IDAT\n\n        \"\"\"\n\n        chunk = [cid, data]\n        if after_idat:\n            chunk.append(True)\n        self.chunks.append(tuple(chunk))\n\n    def add_itxt(self, key, value, lang=\"\", tkey=\"\", zip=False):\n        \"\"\"Appends an iTXt chunk.\n\n        :param key: latin-1 encodable text key name\n        :param value: value for this key\n        :param lang: language code\n        :param tkey: UTF-8 version of the key name\n        :param zip: compression flag\n\n        \"\"\"\n\n        if not isinstance(key, bytes):\n            key = key.encode(\"latin-1\", \"strict\")\n        if not isinstance(value, bytes):\n            value = value.encode(\"utf-8\", \"strict\")\n        if not isinstance(lang, bytes):\n            lang = lang.encode(\"utf-8\", \"strict\")\n        if not isinstance(tkey, bytes):\n            tkey = tkey.encode(\"utf-8\", \"strict\")\n\n        if zip:\n            self.add(\n                b\"iTXt\",\n                key + b\"\\0\\x01\\0\" + lang + b\"\\0\" + tkey + b\"\\0\" + zlib.compress(value),\n            )\n        else:\n            self.add(b\"iTXt\", key + b\"\\0\\0\\0\" + lang + b\"\\0\" + tkey + b\"\\0\" + value)\n\n    def add_text(self, key, value, zip=False):\n        \"\"\"Appends a text chunk.\n\n        :param key: latin-1 encodable text key name\n        :param value: value for this key, text or an\n           :py:class:`PIL.PngImagePlugin.iTXt` instance\n        :param zip: compression flag\n\n        \"\"\"\n        if isinstance(value, iTXt):\n            return self.add_itxt(key, value, value.lang, value.tkey, zip=zip)\n\n        # The tEXt chunk stores latin-1 text\n        if not isinstance(value, bytes):\n            try:\n                value = value.encode(\"latin-1\", \"strict\")\n            except UnicodeError:\n                return self.add_itxt(key, value, zip=zip)\n\n        if not isinstance(key, bytes):\n            key = key.encode(\"latin-1\", \"strict\")\n\n        if zip:\n            self.add(b\"zTXt\", key + b\"\\0\\0\" + zlib.compress(value))\n        else:\n            self.add(b\"tEXt\", key + b\"\\0\" + value)\n\n\n# --------------------------------------------------------------------\n# PNG image stream (IHDR/IEND)\n\n\nclass PngStream(ChunkStream):\n    def __init__(self, fp):\n        super().__init__(fp)\n\n        # local copies of Image attributes\n        self.im_info = {}\n        self.im_text = {}\n        self.im_size = (0, 0)\n        self.im_mode = None\n        self.im_tile = None\n        self.im_palette = None\n        self.im_custom_mimetype = None\n        self.im_n_frames = None\n        self._seq_num = None\n        self.rewind_state = None\n\n        self.text_memory = 0\n\n    def check_text_memory(self, chunklen):\n        self.text_memory += chunklen\n        if self.text_memory > MAX_TEXT_MEMORY:\n            msg = (\n                \"Too much memory used in text chunks: \"\n                f\"{self.text_memory}>MAX_TEXT_MEMORY\"\n            )\n            raise ValueError(msg)\n\n    def save_rewind(self):\n        self.rewind_state = {\n            \"info\": self.im_info.copy(),\n            \"tile\": self.im_tile,\n            \"seq_num\": self._seq_num,\n        }\n\n    def rewind(self):\n        self.im_info = self.rewind_state[\"info\"]\n        self.im_tile = self.rewind_state[\"tile\"]\n        self._seq_num = self.rewind_state[\"seq_num\"]\n\n    def chunk_iCCP(self, pos, length):\n        # ICC profile\n        s = ImageFile._safe_read(self.fp, length)\n        # according to PNG spec, the iCCP chunk contains:\n        # Profile name  1-79 bytes (character string)\n        # Null separator        1 byte (null character)\n        # Compression method    1 byte (0)\n        # Compressed profile    n bytes (zlib with deflate compression)\n        i = s.find(b\"\\0\")\n        logger.debug(\"iCCP profile name %r\", s[:i])\n        logger.debug(\"Compression method %s\", s[i])\n        comp_method = s[i]\n        if comp_method != 0:\n            msg = f\"Unknown compression method {comp_method} in iCCP chunk\"\n            raise SyntaxError(msg)\n        try:\n            icc_profile = _safe_zlib_decompress(s[i + 2 :])\n        except ValueError:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                icc_profile = None\n            else:\n                raise\n        except zlib.error:\n            icc_profile = None  # FIXME\n        self.im_info[\"icc_profile\"] = icc_profile\n        return s\n\n    def chunk_IHDR(self, pos, length):\n        # image header\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 13:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"Truncated IHDR chunk\"\n            raise ValueError(msg)\n        self.im_size = i32(s, 0), i32(s, 4)\n        try:\n            self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])]\n        except Exception:\n            pass\n        if s[12]:\n            self.im_info[\"interlace\"] = 1\n        if s[11]:\n            msg = \"unknown filter category\"\n            raise SyntaxError(msg)\n        return s\n\n    def chunk_IDAT(self, pos, length):\n        # image data\n        if \"bbox\" in self.im_info:\n            tile = [(\"zip\", self.im_info[\"bbox\"], pos, self.im_rawmode)]\n        else:\n            if self.im_n_frames is not None:\n                self.im_info[\"default_image\"] = True\n            tile = [(\"zip\", (0, 0) + self.im_size, pos, self.im_rawmode)]\n        self.im_tile = tile\n        self.im_idat = length\n        msg = \"image data found\"\n        raise EOFError(msg)\n\n    def chunk_IEND(self, pos, length):\n        msg = \"end of PNG image\"\n        raise EOFError(msg)\n\n    def chunk_PLTE(self, pos, length):\n        # palette\n        s = ImageFile._safe_read(self.fp, length)\n        if self.im_mode == \"P\":\n            self.im_palette = \"RGB\", s\n        return s\n\n    def chunk_tRNS(self, pos, length):\n        # transparency\n        s = ImageFile._safe_read(self.fp, length)\n        if self.im_mode == \"P\":\n            if _simple_palette.match(s):\n                # tRNS contains only one full-transparent entry,\n                # other entries are full opaque\n                i = s.find(b\"\\0\")\n                if i >= 0:\n                    self.im_info[\"transparency\"] = i\n            else:\n                # otherwise, we have a byte string with one alpha value\n                # for each palette entry\n                self.im_info[\"transparency\"] = s\n        elif self.im_mode in (\"1\", \"L\", \"I\"):\n            self.im_info[\"transparency\"] = i16(s)\n        elif self.im_mode == \"RGB\":\n            self.im_info[\"transparency\"] = i16(s), i16(s, 2), i16(s, 4)\n        return s\n\n    def chunk_gAMA(self, pos, length):\n        # gamma setting\n        s = ImageFile._safe_read(self.fp, length)\n        self.im_info[\"gamma\"] = i32(s) / 100000.0\n        return s\n\n    def chunk_cHRM(self, pos, length):\n        # chromaticity, 8 unsigned ints, actual value is scaled by 100,000\n        # WP x,y, Red x,y, Green x,y Blue x,y\n\n        s = ImageFile._safe_read(self.fp, length)\n        raw_vals = struct.unpack(\">%dI\" % (len(s) // 4), s)\n        self.im_info[\"chromaticity\"] = tuple(elt / 100000.0 for elt in raw_vals)\n        return s\n\n    def chunk_sRGB(self, pos, length):\n        # srgb rendering intent, 1 byte\n        # 0 perceptual\n        # 1 relative colorimetric\n        # 2 saturation\n        # 3 absolute colorimetric\n\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 1:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"Truncated sRGB chunk\"\n            raise ValueError(msg)\n        self.im_info[\"srgb\"] = s[0]\n        return s\n\n    def chunk_pHYs(self, pos, length):\n        # pixels per unit\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 9:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"Truncated pHYs chunk\"\n            raise ValueError(msg)\n        px, py = i32(s, 0), i32(s, 4)\n        unit = s[8]\n        if unit == 1:  # meter\n            dpi = px * 0.0254, py * 0.0254\n            self.im_info[\"dpi\"] = dpi\n        elif unit == 0:\n            self.im_info[\"aspect\"] = px, py\n        return s\n\n    def chunk_tEXt(self, pos, length):\n        # text\n        s = ImageFile._safe_read(self.fp, length)\n        try:\n            k, v = s.split(b\"\\0\", 1)\n        except ValueError:\n            # fallback for broken tEXt tags\n            k = s\n            v = b\"\"\n        if k:\n            k = k.decode(\"latin-1\", \"strict\")\n            v_str = v.decode(\"latin-1\", \"replace\")\n\n            self.im_info[k] = v if k == \"exif\" else v_str\n            self.im_text[k] = v_str\n            self.check_text_memory(len(v_str))\n\n        return s\n\n    def chunk_zTXt(self, pos, length):\n        # compressed text\n        s = ImageFile._safe_read(self.fp, length)\n        try:\n            k, v = s.split(b\"\\0\", 1)\n        except ValueError:\n            k = s\n            v = b\"\"\n        if v:\n            comp_method = v[0]\n        else:\n            comp_method = 0\n        if comp_method != 0:\n            msg = f\"Unknown compression method {comp_method} in zTXt chunk\"\n            raise SyntaxError(msg)\n        try:\n            v = _safe_zlib_decompress(v[1:])\n        except ValueError:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                v = b\"\"\n            else:\n                raise\n        except zlib.error:\n            v = b\"\"\n\n        if k:\n            k = k.decode(\"latin-1\", \"strict\")\n            v = v.decode(\"latin-1\", \"replace\")\n\n            self.im_info[k] = self.im_text[k] = v\n            self.check_text_memory(len(v))\n\n        return s\n\n    def chunk_iTXt(self, pos, length):\n        # international text\n        r = s = ImageFile._safe_read(self.fp, length)\n        try:\n            k, r = r.split(b\"\\0\", 1)\n        except ValueError:\n            return s\n        if len(r) < 2:\n            return s\n        cf, cm, r = r[0], r[1], r[2:]\n        try:\n            lang, tk, v = r.split(b\"\\0\", 2)\n        except ValueError:\n            return s\n        if cf != 0:\n            if cm == 0:\n                try:\n                    v = _safe_zlib_decompress(v)\n                except ValueError:\n                    if ImageFile.LOAD_TRUNCATED_IMAGES:\n                        return s\n                    else:\n                        raise\n                except zlib.error:\n                    return s\n            else:\n                return s\n        try:\n            k = k.decode(\"latin-1\", \"strict\")\n            lang = lang.decode(\"utf-8\", \"strict\")\n            tk = tk.decode(\"utf-8\", \"strict\")\n            v = v.decode(\"utf-8\", \"strict\")\n        except UnicodeError:\n            return s\n\n        self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk)\n        self.check_text_memory(len(v))\n\n        return s\n\n    def chunk_eXIf(self, pos, length):\n        s = ImageFile._safe_read(self.fp, length)\n        self.im_info[\"exif\"] = b\"Exif\\x00\\x00\" + s\n        return s\n\n    # APNG chunks\n    def chunk_acTL(self, pos, length):\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 8:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"APNG contains truncated acTL chunk\"\n            raise ValueError(msg)\n        if self.im_n_frames is not None:\n            self.im_n_frames = None\n            warnings.warn(\"Invalid APNG, will use default PNG image if possible\")\n            return s\n        n_frames = i32(s)\n        if n_frames == 0 or n_frames > 0x80000000:\n            warnings.warn(\"Invalid APNG, will use default PNG image if possible\")\n            return s\n        self.im_n_frames = n_frames\n        self.im_info[\"loop\"] = i32(s, 4)\n        self.im_custom_mimetype = \"image/apng\"\n        return s\n\n    def chunk_fcTL(self, pos, length):\n        s = ImageFile._safe_read(self.fp, length)\n        if length < 26:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                return s\n            msg = \"APNG contains truncated fcTL chunk\"\n            raise ValueError(msg)\n        seq = i32(s)\n        if (self._seq_num is None and seq != 0) or (\n            self._seq_num is not None and self._seq_num != seq - 1\n        ):\n            msg = \"APNG contains frame sequence errors\"\n            raise SyntaxError(msg)\n        self._seq_num = seq\n        width, height = i32(s, 4), i32(s, 8)\n        px, py = i32(s, 12), i32(s, 16)\n        im_w, im_h = self.im_size\n        if px + width > im_w or py + height > im_h:\n            msg = \"APNG contains invalid frames\"\n            raise SyntaxError(msg)\n        self.im_info[\"bbox\"] = (px, py, px + width, py + height)\n        delay_num, delay_den = i16(s, 20), i16(s, 22)\n        if delay_den == 0:\n            delay_den = 100\n        self.im_info[\"duration\"] = float(delay_num) / float(delay_den) * 1000\n        self.im_info[\"disposal\"] = s[24]\n        self.im_info[\"blend\"] = s[25]\n        return s\n\n    def chunk_fdAT(self, pos, length):\n        if length < 4:\n            if ImageFile.LOAD_TRUNCATED_IMAGES:\n                s = ImageFile._safe_read(self.fp, length)\n                return s\n            msg = \"APNG contains truncated fDAT chunk\"\n            raise ValueError(msg)\n        s = ImageFile._safe_read(self.fp, 4)\n        seq = i32(s)\n        if self._seq_num != seq - 1:\n            msg = \"APNG contains frame sequence errors\"\n            raise SyntaxError(msg)\n        self._seq_num = seq\n        return self.chunk_IDAT(pos + 4, length - 4)\n\n\n# --------------------------------------------------------------------\n# PNG reader\n\n\ndef _accept(prefix):\n    return prefix[:8] == _MAGIC\n\n\n##\n# Image plugin for PNG images.\n\n\nclass PngImageFile(ImageFile.ImageFile):\n    format = \"PNG\"\n    format_description = \"Portable network graphics\"\n\n    def _open(self):\n        if not _accept(self.fp.read(8)):\n            msg = \"not a PNG file\"\n            raise SyntaxError(msg)\n        self._fp = self.fp\n        self.__frame = 0\n\n        #\n        # Parse headers up to the first IDAT or fDAT chunk\n\n        self.private_chunks = []\n        self.png = PngStream(self.fp)\n\n        while True:\n            #\n            # get next chunk\n\n            cid, pos, length = self.png.read()\n\n            try:\n                s = self.png.call(cid, pos, length)\n            except EOFError:\n                break\n            except AttributeError:\n                logger.debug(\"%r %s %s (unknown)\", cid, pos, length)\n                s = ImageFile._safe_read(self.fp, length)\n                if cid[1:2].islower():\n                    self.private_chunks.append((cid, s))\n\n            self.png.crc(cid, s)\n\n        #\n        # Copy relevant attributes from the PngStream.  An alternative\n        # would be to let the PngStream class modify these attributes\n        # directly, but that introduces circular references which are\n        # difficult to break if things go wrong in the decoder...\n        # (believe me, I've tried ;-)\n\n        self._mode = self.png.im_mode\n        self._size = self.png.im_size\n        self.info = self.png.im_info\n        self._text = None\n        self.tile = self.png.im_tile\n        self.custom_mimetype = self.png.im_custom_mimetype\n        self.n_frames = self.png.im_n_frames or 1\n        self.default_image = self.info.get(\"default_image\", False)\n\n        if self.png.im_palette:\n            rawmode, data = self.png.im_palette\n            self.palette = ImagePalette.raw(rawmode, data)\n\n        if cid == b\"fdAT\":\n            self.__prepare_idat = length - 4\n        else:\n            self.__prepare_idat = length  # used by load_prepare()\n\n        if self.png.im_n_frames is not None:\n            self._close_exclusive_fp_after_loading = False\n            self.png.save_rewind()\n            self.__rewind_idat = self.__prepare_idat\n            self.__rewind = self._fp.tell()\n            if self.default_image:\n                # IDAT chunk contains default image and not first animation frame\n                self.n_frames += 1\n            self._seek(0)\n        self.is_animated = self.n_frames > 1\n\n    @property\n    def text(self):\n        # experimental\n        if self._text is None:\n            # iTxt, tEXt and zTXt chunks may appear at the end of the file\n            # So load the file to ensure that they are read\n            if self.is_animated:\n                frame = self.__frame\n                # for APNG, seek to the final frame before loading\n                self.seek(self.n_frames - 1)\n            self.load()\n            if self.is_animated:\n                self.seek(frame)\n        return self._text\n\n    def verify(self):\n        \"\"\"Verify PNG file\"\"\"\n\n        if self.fp is None:\n            msg = \"verify must be called directly after open\"\n            raise RuntimeError(msg)\n\n        # back up to beginning of IDAT block\n        self.fp.seek(self.tile[0][2] - 8)\n\n        self.png.verify()\n        self.png.close()\n\n        if self._exclusive_fp:\n            self.fp.close()\n        self.fp = None\n\n    def seek(self, frame):\n        if not self._seek_check(frame):\n            return\n        if frame < self.__frame:\n            self._seek(0, True)\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 APNG file\"\n                raise EOFError(msg) from e\n\n    def _seek(self, frame, rewind=False):\n        if frame == 0:\n            if rewind:\n                self._fp.seek(self.__rewind)\n                self.png.rewind()\n                self.__prepare_idat = self.__rewind_idat\n                self.im = None\n                if self.pyaccess:\n                    self.pyaccess = None\n                self.info = self.png.im_info\n                self.tile = self.png.im_tile\n                self.fp = self._fp\n            self._prev_im = None\n            self.dispose = None\n            self.default_image = self.info.get(\"default_image\", False)\n            self.dispose_op = self.info.get(\"disposal\")\n            self.blend_op = self.info.get(\"blend\")\n            self.dispose_extent = self.info.get(\"bbox\")\n            self.__frame = 0\n        else:\n            if frame != self.__frame + 1:\n                msg = f\"cannot seek to frame {frame}\"\n                raise ValueError(msg)\n\n            # ensure previous frame was loaded\n            self.load()\n\n            if self.dispose:\n                self.im.paste(self.dispose, self.dispose_extent)\n            self._prev_im = self.im.copy()\n\n            self.fp = self._fp\n\n            # advance to the next frame\n            if self.__prepare_idat:\n                ImageFile._safe_read(self.fp, self.__prepare_idat)\n                self.__prepare_idat = 0\n            frame_start = False\n            while True:\n                self.fp.read(4)  # CRC\n\n                try:\n                    cid, pos, length = self.png.read()\n                except (struct.error, SyntaxError):\n                    break\n\n                if cid == b\"IEND\":\n                    msg = \"No more images in APNG file\"\n                    raise EOFError(msg)\n                if cid == b\"fcTL\":\n                    if frame_start:\n                        # there must be at least one fdAT chunk between fcTL chunks\n                        msg = \"APNG missing frame data\"\n                        raise SyntaxError(msg)\n                    frame_start = True\n\n                try:\n                    self.png.call(cid, pos, length)\n                except UnicodeDecodeError:\n                    break\n                except EOFError:\n                    if cid == b\"fdAT\":\n                        length -= 4\n                        if frame_start:\n                            self.__prepare_idat = length\n                            break\n                    ImageFile._safe_read(self.fp, length)\n                except AttributeError:\n                    logger.debug(\"%r %s %s (unknown)\", cid, pos, length)\n                    ImageFile._safe_read(self.fp, length)\n\n            self.__frame = frame\n            self.tile = self.png.im_tile\n            self.dispose_op = self.info.get(\"disposal\")\n            self.blend_op = self.info.get(\"blend\")\n            self.dispose_extent = self.info.get(\"bbox\")\n\n            if not self.tile:\n                msg = \"image not found in APNG frame\"\n                raise EOFError(msg)\n\n        # setup frame disposal (actual disposal done when needed in the next _seek())\n        if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:\n            self.dispose_op = Disposal.OP_BACKGROUND\n\n        if self.dispose_op == Disposal.OP_PREVIOUS:\n            self.dispose = self._prev_im.copy()\n            self.dispose = self._crop(self.dispose, self.dispose_extent)\n        elif self.dispose_op == Disposal.OP_BACKGROUND:\n            self.dispose = Image.core.fill(self.mode, self.size)\n            self.dispose = self._crop(self.dispose, self.dispose_extent)\n        else:\n            self.dispose = None\n\n    def tell(self):\n        return self.__frame\n\n    def load_prepare(self):\n        \"\"\"internal: prepare to read PNG file\"\"\"\n\n        if self.info.get(\"interlace\"):\n            self.decoderconfig = self.decoderconfig + (1,)\n\n        self.__idat = self.__prepare_idat  # used by load_read()\n        ImageFile.ImageFile.load_prepare(self)\n\n    def load_read(self, read_bytes):\n        \"\"\"internal: read more image data\"\"\"\n\n        while self.__idat == 0:\n            # end of chunk, skip forward to next one\n\n            self.fp.read(4)  # CRC\n\n            cid, pos, length = self.png.read()\n\n            if cid not in [b\"IDAT\", b\"DDAT\", b\"fdAT\"]:\n                self.png.push(cid, pos, length)\n                return b\"\"\n\n            if cid == b\"fdAT\":\n                try:\n                    self.png.call(cid, pos, length)\n                except EOFError:\n                    pass\n                self.__idat = length - 4  # sequence_num has already been read\n            else:\n                self.__idat = length  # empty chunks are allowed\n\n        # read more data from this chunk\n        if read_bytes <= 0:\n            read_bytes = self.__idat\n        else:\n            read_bytes = min(read_bytes, self.__idat)\n\n        self.__idat = self.__idat - read_bytes\n\n        return self.fp.read(read_bytes)\n\n    def load_end(self):\n        \"\"\"internal: finished reading image data\"\"\"\n        if self.__idat != 0:\n            self.fp.read(self.__idat)\n        while True:\n            self.fp.read(4)  # CRC\n\n            try:\n                cid, pos, length = self.png.read()\n            except (struct.error, SyntaxError):\n                break\n\n            if cid == b\"IEND\":\n                break\n            elif cid == b\"fcTL\" and self.is_animated:\n                # start of the next frame, stop reading\n                self.__prepare_idat = 0\n                self.png.push(cid, pos, length)\n                break\n\n            try:\n                self.png.call(cid, pos, length)\n            except UnicodeDecodeError:\n                break\n            except EOFError:\n                if cid == b\"fdAT\":\n                    length -= 4\n                ImageFile._safe_read(self.fp, length)\n            except AttributeError:\n                logger.debug(\"%r %s %s (unknown)\", cid, pos, length)\n                s = ImageFile._safe_read(self.fp, length)\n                if cid[1:2].islower():\n                    self.private_chunks.append((cid, s, True))\n        self._text = self.png.im_text\n        if not self.is_animated:\n            self.png.close()\n            self.png = None\n        else:\n            if self._prev_im and self.blend_op == Blend.OP_OVER:\n                updated = self._crop(self.im, self.dispose_extent)\n                if self.im.mode == \"RGB\" and \"transparency\" in self.info:\n                    mask = updated.convert_transparent(\n                        \"RGBA\", self.info[\"transparency\"]\n                    )\n                else:\n                    mask = updated.convert(\"RGBA\")\n                self._prev_im.paste(updated, self.dispose_extent, mask)\n                self.im = self._prev_im\n                if self.pyaccess:\n                    self.pyaccess = None\n\n    def _getexif(self):\n        if \"exif\" not in self.info:\n            self.load()\n        if \"exif\" not in self.info and \"Raw profile type exif\" not in self.info:\n            return None\n        return self.getexif()._get_merged_dict()\n\n    def getexif(self):\n        if \"exif\" not in self.info:\n            self.load()\n\n        return super().getexif()\n\n    def getxmp(self):\n        \"\"\"\n        Returns a dictionary containing the XMP tags.\n        Requires defusedxml to be installed.\n\n        :returns: XMP tags in a dictionary.\n        \"\"\"\n        return (\n            self._getxmp(self.info[\"XML:com.adobe.xmp\"])\n            if \"XML:com.adobe.xmp\" in self.info\n            else {}\n        )\n\n\n# --------------------------------------------------------------------\n# PNG writer\n\n_OUTMODES = {\n    # supported PIL modes, and corresponding rawmodes/bits/color combinations\n    \"1\": (\"1\", b\"\\x01\\x00\"),\n    \"L;1\": (\"L;1\", b\"\\x01\\x00\"),\n    \"L;2\": (\"L;2\", b\"\\x02\\x00\"),\n    \"L;4\": (\"L;4\", b\"\\x04\\x00\"),\n    \"L\": (\"L\", b\"\\x08\\x00\"),\n    \"LA\": (\"LA\", b\"\\x08\\x04\"),\n    \"I\": (\"I;16B\", b\"\\x10\\x00\"),\n    \"I;16\": (\"I;16B\", b\"\\x10\\x00\"),\n    \"I;16B\": (\"I;16B\", b\"\\x10\\x00\"),\n    \"P;1\": (\"P;1\", b\"\\x01\\x03\"),\n    \"P;2\": (\"P;2\", b\"\\x02\\x03\"),\n    \"P;4\": (\"P;4\", b\"\\x04\\x03\"),\n    \"P\": (\"P\", b\"\\x08\\x03\"),\n    \"RGB\": (\"RGB\", b\"\\x08\\x02\"),\n    \"RGBA\": (\"RGBA\", b\"\\x08\\x06\"),\n}\n\n\ndef putchunk(fp, cid, *data):\n    \"\"\"Write a PNG chunk (including CRC field)\"\"\"\n\n    data = b\"\".join(data)\n\n    fp.write(o32(len(data)) + cid)\n    fp.write(data)\n    crc = _crc32(data, _crc32(cid))\n    fp.write(o32(crc))\n\n\nclass _idat:\n    # wrap output from the encoder in IDAT chunks\n\n    def __init__(self, fp, chunk):\n        self.fp = fp\n        self.chunk = chunk\n\n    def write(self, data):\n        self.chunk(self.fp, b\"IDAT\", data)\n\n\nclass _fdat:\n    # wrap encoder output in fdAT chunks\n\n    def __init__(self, fp, chunk, seq_num):\n        self.fp = fp\n        self.chunk = chunk\n        self.seq_num = seq_num\n\n    def write(self, data):\n        self.chunk(self.fp, b\"fdAT\", o32(self.seq_num), data)\n        self.seq_num += 1\n\n\ndef _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images):\n    duration = im.encoderinfo.get(\"duration\", im.info.get(\"duration\", 0))\n    loop = im.encoderinfo.get(\"loop\", im.info.get(\"loop\", 0))\n    disposal = im.encoderinfo.get(\"disposal\", im.info.get(\"disposal\", Disposal.OP_NONE))\n    blend = im.encoderinfo.get(\"blend\", im.info.get(\"blend\", Blend.OP_SOURCE))\n\n    if default_image:\n        chain = itertools.chain(append_images)\n    else:\n        chain = itertools.chain([im], append_images)\n\n    im_frames = []\n    frame_count = 0\n    for im_seq in chain:\n        for im_frame in ImageSequence.Iterator(im_seq):\n            if im_frame.mode == rawmode:\n                im_frame = im_frame.copy()\n            else:\n                im_frame = im_frame.convert(rawmode)\n            encoderinfo = im.encoderinfo.copy()\n            if isinstance(duration, (list, tuple)):\n                encoderinfo[\"duration\"] = duration[frame_count]\n            if isinstance(disposal, (list, tuple)):\n                encoderinfo[\"disposal\"] = disposal[frame_count]\n            if isinstance(blend, (list, tuple)):\n                encoderinfo[\"blend\"] = blend[frame_count]\n            frame_count += 1\n\n            if im_frames:\n                previous = im_frames[-1]\n                prev_disposal = previous[\"encoderinfo\"].get(\"disposal\")\n                prev_blend = previous[\"encoderinfo\"].get(\"blend\")\n                if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:\n                    prev_disposal = Disposal.OP_BACKGROUND\n\n                if prev_disposal == Disposal.OP_BACKGROUND:\n                    base_im = previous[\"im\"].copy()\n                    dispose = Image.core.fill(\"RGBA\", im.size, (0, 0, 0, 0))\n                    bbox = previous[\"bbox\"]\n                    if bbox:\n                        dispose = dispose.crop(bbox)\n                    else:\n                        bbox = (0, 0) + im.size\n                    base_im.paste(dispose, bbox)\n                elif prev_disposal == Disposal.OP_PREVIOUS:\n                    base_im = im_frames[-2][\"im\"]\n                else:\n                    base_im = previous[\"im\"]\n                delta = ImageChops.subtract_modulo(\n                    im_frame.convert(\"RGBA\"), base_im.convert(\"RGBA\")\n                )\n                bbox = delta.getbbox(alpha_only=False)\n                if (\n                    not bbox\n                    and prev_disposal == encoderinfo.get(\"disposal\")\n                    and prev_blend == encoderinfo.get(\"blend\")\n                ):\n                    previous[\"encoderinfo\"][\"duration\"] += encoderinfo.get(\n                        \"duration\", duration\n                    )\n                    continue\n            else:\n                bbox = None\n            if \"duration\" not in encoderinfo:\n                encoderinfo[\"duration\"] = duration\n            im_frames.append({\"im\": im_frame, \"bbox\": bbox, \"encoderinfo\": encoderinfo})\n\n    # animation control\n    chunk(\n        fp,\n        b\"acTL\",\n        o32(len(im_frames)),  # 0: num_frames\n        o32(loop),  # 4: num_plays\n    )\n\n    # default image IDAT (if it exists)\n    if default_image:\n        if im.mode != rawmode:\n            im = im.convert(rawmode)\n        ImageFile._save(im, _idat(fp, chunk), [(\"zip\", (0, 0) + im.size, 0, rawmode)])\n\n    seq_num = 0\n    for frame, frame_data in enumerate(im_frames):\n        im_frame = frame_data[\"im\"]\n        if not frame_data[\"bbox\"]:\n            bbox = (0, 0) + im_frame.size\n        else:\n            bbox = frame_data[\"bbox\"]\n            im_frame = im_frame.crop(bbox)\n        size = im_frame.size\n        encoderinfo = frame_data[\"encoderinfo\"]\n        frame_duration = int(round(encoderinfo[\"duration\"]))\n        frame_disposal = encoderinfo.get(\"disposal\", disposal)\n        frame_blend = encoderinfo.get(\"blend\", blend)\n        # frame control\n        chunk(\n            fp,\n            b\"fcTL\",\n            o32(seq_num),  # sequence_number\n            o32(size[0]),  # width\n            o32(size[1]),  # height\n            o32(bbox[0]),  # x_offset\n            o32(bbox[1]),  # y_offset\n            o16(frame_duration),  # delay_numerator\n            o16(1000),  # delay_denominator\n            o8(frame_disposal),  # dispose_op\n            o8(frame_blend),  # blend_op\n        )\n        seq_num += 1\n        # frame data\n        if frame == 0 and not default_image:\n            # first frame must be in IDAT chunks for backwards compatibility\n            ImageFile._save(\n                im_frame,\n                _idat(fp, chunk),\n                [(\"zip\", (0, 0) + im_frame.size, 0, rawmode)],\n            )\n        else:\n            fdat_chunks = _fdat(fp, chunk, seq_num)\n            ImageFile._save(\n                im_frame,\n                fdat_chunks,\n                [(\"zip\", (0, 0) + im_frame.size, 0, rawmode)],\n            )\n            seq_num = fdat_chunks.seq_num\n\n\ndef _save_all(im, fp, filename):\n    _save(im, fp, filename, save_all=True)\n\n\ndef _save(im, fp, filename, chunk=putchunk, save_all=False):\n    # save an image to disk (called by the save method)\n\n    if save_all:\n        default_image = im.encoderinfo.get(\n            \"default_image\", im.info.get(\"default_image\")\n        )\n        modes = set()\n        append_images = im.encoderinfo.get(\"append_images\", [])\n        for im_seq in itertools.chain([im], append_images):\n            for im_frame in ImageSequence.Iterator(im_seq):\n                modes.add(im_frame.mode)\n        for mode in (\"RGBA\", \"RGB\", \"P\"):\n            if mode in modes:\n                break\n        else:\n            mode = modes.pop()\n    else:\n        mode = im.mode\n\n    if mode == \"P\":\n        #\n        # attempt to minimize storage requirements for palette images\n        if \"bits\" in im.encoderinfo:\n            # number of bits specified by user\n            colors = min(1 << im.encoderinfo[\"bits\"], 256)\n        else:\n            # check palette contents\n            if im.palette:\n                colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1)\n            else:\n                colors = 256\n\n        if colors <= 16:\n            if colors <= 2:\n                bits = 1\n            elif colors <= 4:\n                bits = 2\n            else:\n                bits = 4\n            mode = f\"{mode};{bits}\"\n\n    # encoder options\n    im.encoderconfig = (\n        im.encoderinfo.get(\"optimize\", False),\n        im.encoderinfo.get(\"compress_level\", -1),\n        im.encoderinfo.get(\"compress_type\", -1),\n        im.encoderinfo.get(\"dictionary\", b\"\"),\n    )\n\n    # get the corresponding PNG mode\n    try:\n        rawmode, mode = _OUTMODES[mode]\n    except KeyError as e:\n        msg = f\"cannot write mode {mode} as PNG\"\n        raise OSError(msg) from e\n\n    #\n    # write minimal PNG file\n\n    fp.write(_MAGIC)\n\n    chunk(\n        fp,\n        b\"IHDR\",\n        o32(im.size[0]),  # 0: size\n        o32(im.size[1]),\n        mode,  # 8: depth/type\n        b\"\\0\",  # 10: compression\n        b\"\\0\",  # 11: filter category\n        b\"\\0\",  # 12: interlace flag\n    )\n\n    chunks = [b\"cHRM\", b\"gAMA\", b\"sBIT\", b\"sRGB\", b\"tIME\"]\n\n    icc = im.encoderinfo.get(\"icc_profile\", im.info.get(\"icc_profile\"))\n    if icc:\n        # ICC profile\n        # according to PNG spec, the iCCP chunk contains:\n        # Profile name  1-79 bytes (character string)\n        # Null separator        1 byte (null character)\n        # Compression method    1 byte (0)\n        # Compressed profile    n bytes (zlib with deflate compression)\n        name = b\"ICC Profile\"\n        data = name + b\"\\0\\0\" + zlib.compress(icc)\n        chunk(fp, b\"iCCP\", data)\n\n        # You must either have sRGB or iCCP.\n        # Disallow sRGB chunks when an iCCP-chunk has been emitted.\n        chunks.remove(b\"sRGB\")\n\n    info = im.encoderinfo.get(\"pnginfo\")\n    if info:\n        chunks_multiple_allowed = [b\"sPLT\", b\"iTXt\", b\"tEXt\", b\"zTXt\"]\n        for info_chunk in info.chunks:\n            cid, data = info_chunk[:2]\n            if cid in chunks:\n                chunks.remove(cid)\n                chunk(fp, cid, data)\n            elif cid in chunks_multiple_allowed:\n                chunk(fp, cid, data)\n            elif cid[1:2].islower():\n                # Private chunk\n                after_idat = info_chunk[2:3]\n                if not after_idat:\n                    chunk(fp, cid, data)\n\n    if im.mode == \"P\":\n        palette_byte_number = colors * 3\n        palette_bytes = im.im.getpalette(\"RGB\")[:palette_byte_number]\n        while len(palette_bytes) < palette_byte_number:\n            palette_bytes += b\"\\0\"\n        chunk(fp, b\"PLTE\", palette_bytes)\n\n    transparency = im.encoderinfo.get(\"transparency\", im.info.get(\"transparency\", None))\n\n    if transparency or transparency == 0:\n        if im.mode == \"P\":\n            # limit to actual palette size\n            alpha_bytes = colors\n            if isinstance(transparency, bytes):\n                chunk(fp, b\"tRNS\", transparency[:alpha_bytes])\n            else:\n                transparency = max(0, min(255, transparency))\n                alpha = b\"\\xFF\" * transparency + b\"\\0\"\n                chunk(fp, b\"tRNS\", alpha[:alpha_bytes])\n        elif im.mode in (\"1\", \"L\", \"I\"):\n            transparency = max(0, min(65535, transparency))\n            chunk(fp, b\"tRNS\", o16(transparency))\n        elif im.mode == \"RGB\":\n            red, green, blue = transparency\n            chunk(fp, b\"tRNS\", o16(red) + o16(green) + o16(blue))\n        else:\n            if \"transparency\" in im.encoderinfo:\n                # don't bother with transparency if it's an RGBA\n                # and it's in the info dict. It's probably just stale.\n                msg = \"cannot use transparency for this mode\"\n                raise OSError(msg)\n    else:\n        if im.mode == \"P\" and im.im.getpalettemode() == \"RGBA\":\n            alpha = im.im.getpalette(\"RGBA\", \"A\")\n            alpha_bytes = colors\n            chunk(fp, b\"tRNS\", alpha[:alpha_bytes])\n\n    dpi = im.encoderinfo.get(\"dpi\")\n    if dpi:\n        chunk(\n            fp,\n            b\"pHYs\",\n            o32(int(dpi[0] / 0.0254 + 0.5)),\n            o32(int(dpi[1] / 0.0254 + 0.5)),\n            b\"\\x01\",\n        )\n\n    if info:\n        chunks = [b\"bKGD\", b\"hIST\"]\n        for info_chunk in info.chunks:\n            cid, data = info_chunk[:2]\n            if cid in chunks:\n                chunks.remove(cid)\n                chunk(fp, cid, data)\n\n    exif = im.encoderinfo.get(\"exif\")\n    if exif:\n        if isinstance(exif, Image.Exif):\n            exif = exif.tobytes(8)\n        if exif.startswith(b\"Exif\\x00\\x00\"):\n            exif = exif[6:]\n        chunk(fp, b\"eXIf\", exif)\n\n    if save_all:\n        _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)\n    else:\n        ImageFile._save(im, _idat(fp, chunk), [(\"zip\", (0, 0) + im.size, 0, rawmode)])\n\n    if info:\n        for info_chunk in info.chunks:\n            cid, data = info_chunk[:2]\n            if cid[1:2].islower():\n                # Private chunk\n                after_idat = info_chunk[2:3]\n                if after_idat:\n                    chunk(fp, cid, data)\n\n    chunk(fp, b\"IEND\", b\"\")\n\n    if hasattr(fp, \"flush\"):\n        fp.flush()\n\n\n# --------------------------------------------------------------------\n# PNG chunk converter\n\n\ndef getchunks(im, **params):\n    \"\"\"Return a list of PNG chunks representing this image.\"\"\"\n\n    class collector:\n        data = []\n\n        def write(self, data):\n            pass\n\n        def append(self, chunk):\n            self.data.append(chunk)\n\n    def append(fp, cid, *data):\n        data = b\"\".join(data)\n        crc = o32(_crc32(data, _crc32(cid)))\n        fp.append((cid, data, crc))\n\n    fp = collector()\n\n    try:\n        im.encoderinfo = params\n        _save(im, fp, None, append)\n    finally:\n        del im.encoderinfo\n\n    return fp.data\n\n\n# --------------------------------------------------------------------\n# Registry\n\nImage.register_open(PngImageFile.format, PngImageFile, _accept)\nImage.register_save(PngImageFile.format, _save)\nImage.register_save_all(PngImageFile.format, _save_all)\n\nImage.register_extensions(PngImageFile.format, [\".png\", \".apng\"])\n\nImage.register_mime(PngImageFile.format, \"image/png\")\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:** Incorrect MIME type assigned when saving APNG files

**Description:**
When saving an animated PNG (APNG) with a single frame and without a default image, the resulting file's MIME type is incorrectly set to `"image/apng"` instead of the standard `"image/png"`.

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

# Create an APNG with a single frame and no default image
im = Image.new("RGBA", (100, 100), (255, 0, 0, 255))
test_file = "temp.png"
im.save(test_file, save_all=True)

with Image.open(test_file) as im_saved:
    mime_type = im_saved.get_format_mimetype()
    print(mime_type)  # Outputs "image/apng" instead of "image/png"
```

**Expected Behavior:**
Saving an APNG with a single frame and no default image should set the MIME type to `"image/png"`.

**Actual Behavior:**
The MIME type is incorrectly set to `"image/apng"`.

[/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/PngImagePlugin.py
#
# The Python Imaging Library.
# $Id$
#
# PNG support code
#
# See "PNG (Portable Network Graphics) Specification, version 1.0;
# W3C Recommendation", 1996-10-01, Thomas Boutell (ed.).
#
# history:
# 1996-05-06 fl   Created (couldn't resist it)
# 1996-12-14 fl   Upgraded, added read and verify support (0.2)
# 1996-12-15 fl   Separate PNG stream parser
# 1996-12-29 fl   Added write support, added getchunks
# 1996-12-30 fl   Eliminated circular references in decoder (0.3)
# 1998-07-12 fl   Read/write 16-bit images as mode I (0.4)
# 2001-02-08 fl   Added transparency support (from Zircon) (0.5)
# 2001-04-16 fl   Don't close data source in "open" method (0.6)
# 2004-02-24 fl   Don't even pretend to support interlaced files (0.7)
# 2004-08-31 fl   Do basic sanity check on chunk identifiers (0.8)
# 2004-09-20 fl   Added PngInfo chunk container
# 2004-12-18 fl   Added DPI read support (based on code by Niki Spahiev)
# 2008-08-13 fl   Added tRNS support for RGB images
# 2009-03-06 fl   Support for preserving ICC profiles (by Florian Hoech)
# 2009-03-08 fl   Added zTXT support (from Lowell Alleman)
# 2009-03-29 fl   Read interlaced PNG files (from Conrado Porto Lopes Gouvua)
#
# Copyright (c) 1997-2009 by Secret Labs AB
# Copyright (c) 1996 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#

import itertools
import logging
import re
import struct
import warnings
import zlib
from enum import IntEnum

from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import o8
from ._binary import o16be as o16
from ._binary import o32be as o32

logger = logging.getLogger(__name__)

is_cid = re.compile(rb"\w\w\w\w").match


_MAGIC = b"\211PNG\r\n\032\n"


_MODES = {
    # supported bits/color combinations, and corresponding modes/rawmodes
    # Grayscale
    (1, 0): ("1", "1"),
    (2, 0): ("L", "L;2"),
    (4, 0): ("L", "L;4"),
    (8, 0): ("L", "L"),
    (16, 0): ("I", "I;16B"),
    # Truecolour
    (8, 2): ("RGB", "RGB"),
    (16, 2): ("RGB", "RGB;16B"),
    # Indexed-colour
    (1, 3): ("P", "P;1"),
    (2, 3): ("P", "P;2"),
    (4, 3): ("P", "P;4"),
    (8, 3): ("P", "P"),
    # Grayscale with alpha
    (8, 4): ("LA", "LA"),
    (16, 4): ("RGBA", "LA;16B"),  # LA;16B->LA not yet available
    # Truecolour with alpha
    (8, 6): ("RGBA", "RGBA"),
    (16, 6): ("RGBA", "RGBA;16B"),
}


_simple_palette = re.compile(b"^\xff*\x00\xff*$")

MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK
"""
Maximum decompressed size for a iTXt or zTXt chunk.
Eliminates decompression bombs where compressed chunks can expand 1000x.
See :ref:`Text in PNG File Format<png-text>`.
"""
MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK
"""
Set the maximum total text chunk size.
See :ref:`Text in PNG File Format<png-text>`.
"""


# APNG frame disposal modes
class Disposal(IntEnum):
    OP_NONE = 0
    """
    No disposal is done on this frame before rendering the next frame.
    See :ref:`Saving APNG sequences<apng-saving>`.
    """
    OP_BACKGROUND = 1
    """
    This frame’s modified region is cleared to fully transparent black before rendering
    the next frame.
    See :ref:`Saving APNG sequences<apng-saving>`.
    """
    OP_PREVIOUS = 2
    """
    This frame’s modified region is reverted to the previous frame’s contents before
    rendering the next frame.
    See :ref:`Saving APNG sequences<apng-saving>`.
    """


# APNG frame blend modes
class Blend(IntEnum):
    OP_SOURCE = 0
    """
    All color components of this frame, including alpha, overwrite the previous output
    image contents.
    See :ref:`Saving APNG sequences<apng-saving>`.
    """
    OP_OVER = 1
    """
    This frame should be alpha composited with the previous output image contents.
    See :ref:`Saving APNG sequences<apng-saving>`.
    """


def _safe_zlib_decompress(s):
    dobj = zlib.decompressobj()
    plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)
    if dobj.unconsumed_tail:
        msg = "Decompressed Data Too Large"
        raise ValueError(msg)
    return plaintext


def _crc32(data, seed=0):
    return zlib.crc32(data, seed) & 0xFFFFFFFF


# --------------------------------------------------------------------
# Support classes.  Suitable for PNG and related formats like MNG etc.


class ChunkStream:
    def __init__(self, fp):
        self.fp = fp
        self.queue = []

    def read(self):
        """Fetch a new chunk. Returns header information."""
        cid = None

        if self.queue:
            cid, pos, length = self.queue.pop()
            self.fp.seek(pos)
        else:
            s = self.fp.read(8)
            cid = s[4:]
            pos = self.fp.tell()
            length = i32(s)

        if not is_cid(cid):
            if not ImageFile.LOAD_TRUNCATED_IMAGES:
                msg = f"broken PNG file (chunk {repr(cid)})"
                raise SyntaxError(msg)

        return cid, pos, length

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

    def close(self):
        self.queue = self.fp = None

    def push(self, cid, pos, length):
        self.queue.append((cid, pos, length))

    def call(self, cid, pos, length):
        """Call the appropriate chunk handler"""

        logger.debug("STREAM %r %s %s", cid, pos, length)
        return getattr(self, "chunk_" + cid.decode("ascii"))(pos, length)

    def crc(self, cid, data):
        """Read and verify checksum"""

        # Skip CRC checks for ancillary chunks if allowed to load truncated
        # images
        # 5th byte of first char is 1 [specs, section 5.4]
        if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1):
            self.crc_skip(cid, data)
            return

        try:
            crc1 = _crc32(data, _crc32(cid))
            crc2 = i32(self.fp.read(4))
            if crc1 != crc2:
                msg = f"broken PNG file (bad header checksum in {repr(cid)})"
                raise SyntaxError(msg)
        except struct.error as e:
            msg = f"broken PNG file (incomplete checksum in {repr(cid)})"
            raise SyntaxError(msg) from e

    def crc_skip(self, cid, data):
        """Read checksum"""

        self.fp.read(4)

    def verify(self, endchunk=b"IEND"):
        # Simple approach; just calculate checksum for all remaining
        # blocks.  Must be called directly after open.

        cids = []

        while True:
            try:
                cid, pos, length = self.read()
            except struct.error as e:
                msg = "truncated PNG file"
                raise OSError(msg) from e

            if cid == endchunk:
                break
            self.crc(cid, ImageFile._safe_read(self.fp, length))
            cids.append(cid)

        return cids


class iTXt(str):
    """
    Subclass of string to allow iTXt chunks to look like strings while
    keeping their extra information

    """

    @staticmethod
    def __new__(cls, text, lang=None, tkey=None):
        """
        :param cls: the class to use when creating the instance
        :param text: value for this key
        :param lang: language code
        :param tkey: UTF-8 version of the key name
        """

        self = str.__new__(cls, text)
        self.lang = lang
        self.tkey = tkey
        return self


class PngInfo:
    """
    PNG chunk container (for use with save(pnginfo=))

    """

    def __init__(self):
        self.chunks = []

    def add(self, cid, data, after_idat=False):
        """Appends an arbitrary chunk. Use with caution.

        :param cid: a byte string, 4 bytes long.
        :param data: a byte string of the encoded data
        :param after_idat: for use with private chunks. Whether the chunk
                           should be written after IDAT

        """

        chunk = [cid, data]
        if after_idat:
            chunk.append(True)
        self.chunks.append(tuple(chunk))

    def add_itxt(self, key, value, lang="", tkey="", zip=False):
        """Appends an iTXt chunk.

        :param key: latin-1 encodable text key name
        :param value: value for this key
        :param lang: language code
        :param tkey: UTF-8 version of the key name
        :param zip: compression flag

        """

        if not isinstance(key, bytes):
            key = key.encode("latin-1", "strict")
        if not isinstance(value, bytes):
            value = value.encode("utf-8", "strict")
        if not isinstance(lang, bytes):
            lang = lang.encode("utf-8", "strict")
        if not isinstance(tkey, bytes):
            tkey = tkey.encode("utf-8", "strict")

        if zip:
            self.add(
                b"iTXt",
                key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value),
            )
        else:
            self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value)

    def add_text(self, key, value, zip=False):
        """Appends a text chunk.

        :param key: latin-1 encodable text key name
        :param value: value for this key, text or an
           :py:class:`PIL.PngImagePlugin.iTXt` instance
        :param zip: compression flag

        """
        if isinstance(value, iTXt):
            return self.add_itxt(key, value, value.lang, value.tkey, zip=zip)

        # The tEXt chunk stores latin-1 text
        if not isinstance(value, bytes):
            try:
                value = value.encode("latin-1", "strict")
            except UnicodeError:
                return self.add_itxt(key, value, zip=zip)

        if not isinstance(key, bytes):
            key = key.encode("latin-1", "strict")

        if zip:
            self.add(b"zTXt", key + b"\0\0" + zlib.compress(value))
        else:
            self.add(b"tEXt", key + b"\0" + value)


# --------------------------------------------------------------------
# PNG image stream (IHDR/IEND)


class PngStream(ChunkStream):
    def __init__(self, fp):
        super().__init__(fp)

        # local copies of Image attributes
        self.im_info = {}
        self.im_text = {}
        self.im_size = (0, 0)
        self.im_mode = None
        self.im_tile = None
        self.im_palette = None
        self.im_custom_mimetype = None
        self.im_n_frames = None
        self._seq_num = None
        self.rewind_state = None

        self.text_memory = 0

    def check_text_memory(self, chunklen):
        self.text_memory += chunklen
        if self.text_memory > MAX_TEXT_MEMORY:
            msg = (
                "Too much memory used in text chunks: "
                f"{self.text_memory}>MAX_TEXT_MEMORY"
            )
            raise ValueError(msg)

    def save_rewind(self):
        self.rewind_state = {
            "info": self.im_info.copy(),
            "tile": self.im_tile,
            "seq_num": self._seq_num,
        }

    def rewind(self):
        self.im_info = self.rewind_state["info"]
        self.im_tile = self.rewind_state["tile"]
        self._seq_num = self.rewind_state["seq_num"]

    def chunk_iCCP(self, pos, length):
        # ICC profile
        s = ImageFile._safe_read(self.fp, length)
        # according to PNG spec, the iCCP chunk contains:
        # Profile name  1-79 bytes (character string)
        # Null separator        1 byte (null character)
        # Compression method    1 byte (0)
        # Compressed profile    n bytes (zlib with deflate compression)
        i = s.find(b"\0")
        logger.debug("iCCP profile name %r", s[:i])
        logger.debug("Compression method %s", s[i])
        comp_method = s[i]
        if comp_method != 0:
            msg = f"Unknown compression method {comp_method} in iCCP chunk"
            raise SyntaxError(msg)
        try:
            icc_profile = _safe_zlib_decompress(s[i + 2 :])
        except ValueError:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                icc_profile = None
            else:
                raise
        except zlib.error:
            icc_profile = None  # FIXME
        self.im_info["icc_profile"] = icc_profile
        return s

    def chunk_IHDR(self, pos, length):
        # image header
        s = ImageFile._safe_read(self.fp, length)
        if length < 13:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                return s
            msg = "Truncated IHDR chunk"
            raise ValueError(msg)
        self.im_size = i32(s, 0), i32(s, 4)
        try:
            self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])]
        except Exception:
            pass
        if s[12]:
            self.im_info["interlace"] = 1
        if s[11]:
            msg = "unknown filter category"
            raise SyntaxError(msg)
        return s

    def chunk_IDAT(self, pos, length):
        # image data
        if "bbox" in self.im_info:
            tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)]
        else:
            if self.im_n_frames is not None:
                self.im_info["default_image"] = True
            tile = [("zip", (0, 0) + self.im_size, pos, self.im_rawmode)]
        self.im_tile = tile
        self.im_idat = length
        msg = "image data found"
        raise EOFError(msg)

    def chunk_IEND(self, pos, length):
        msg = "end of PNG image"
        raise EOFError(msg)

    def chunk_PLTE(self, pos, length):
        # palette
        s = ImageFile._safe_read(self.fp, length)
        if self.im_mode == "P":
            self.im_palette = "RGB", s
        return s

    def chunk_tRNS(self, pos, length):
        # transparency
        s = ImageFile._safe_read(self.fp, length)
        if self.im_mode == "P":
            if _simple_palette.match(s):
                # tRNS contains only one full-transparent entry,
                # other entries are full opaque
                i = s.find(b"\0")
                if i >= 0:
                    self.im_info["transparency"] = i
            else:
                # otherwise, we have a byte string with one alpha value
                # for each palette entry
                self.im_info["transparency"] = s
        elif self.im_mode in ("1", "L", "I"):
            self.im_info["transparency"] = i16(s)
        elif self.im_mode == "RGB":
            self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4)
        return s

    def chunk_gAMA(self, pos, length):
        # gamma setting
        s = ImageFile._safe_read(self.fp, length)
        self.im_info["gamma"] = i32(s) / 100000.0
        return s

    def chunk_cHRM(self, pos, length):
        # chromaticity, 8 unsigned ints, actual value is scaled by 100,000
        # WP x,y, Red x,y, Green x,y Blue x,y

        s = ImageFile._safe_read(self.fp, length)
        raw_vals = struct.unpack(">%dI" % (len(s) // 4), s)
        self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals)
        return s

    def chunk_sRGB(self, pos, length):
        # srgb rendering intent, 1 byte
        # 0 perceptual
        # 1 relative colorimetric
        # 2 saturation
        # 3 absolute colorimetric

        s = ImageFile._safe_read(self.fp, length)
        if length < 1:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                return s
            msg = "Truncated sRGB chunk"
            raise ValueError(msg)
        self.im_info["srgb"] = s[0]
        return s

    def chunk_pHYs(self, pos, length):
        # pixels per unit
        s = ImageFile._safe_read(self.fp, length)
        if length < 9:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                return s
            msg = "Truncated pHYs chunk"
            raise ValueError(msg)
        px, py = i32(s, 0), i32(s, 4)
        unit = s[8]
        if unit == 1:  # meter
            dpi = px * 0.0254, py * 0.0254
            self.im_info["dpi"] = dpi
        elif unit == 0:
            self.im_info["aspect"] = px, py
        return s

    def chunk_tEXt(self, pos, length):
        # text
        s = ImageFile._safe_read(self.fp, length)
        try:
            k, v = s.split(b"\0", 1)
        except ValueError:
            # fallback for broken tEXt tags
            k = s
            v = b""
        if k:
            k = k.decode("latin-1", "strict")
            v_str = v.decode("latin-1", "replace")

            self.im_info[k] = v if k == "exif" else v_str
            self.im_text[k] = v_str
            self.check_text_memory(len(v_str))

        return s

    def chunk_zTXt(self, pos, length):
        # compressed text
        s = ImageFile._safe_read(self.fp, length)
        try:
            k, v = s.split(b"\0", 1)
        except ValueError:
            k = s
            v = b""
        if v:
            comp_method = v[0]
        else:
            comp_method = 0
        if comp_method != 0:
            msg = f"Unknown compression method {comp_method} in zTXt chunk"
            raise SyntaxError(msg)
        try:
            v = _safe_zlib_decompress(v[1:])
        except ValueError:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                v = b""
            else:
                raise
        except zlib.error:
            v = b""

        if k:
            k = k.decode("latin-1", "strict")
            v = v.decode("latin-1", "replace")

            self.im_info[k] = self.im_text[k] = v
            self.check_text_memory(len(v))

        return s

    def chunk_iTXt(self, pos, length):
        # international text
        r = s = ImageFile._safe_read(self.fp, length)
        try:
            k, r = r.split(b"\0", 1)
        except ValueError:
            return s
        if len(r) < 2:
            return s
        cf, cm, r = r[0], r[1], r[2:]
        try:
            lang, tk, v = r.split(b"\0", 2)
        except ValueError:
            return s
        if cf != 0:
            if cm == 0:
                try:
                    v = _safe_zlib_decompress(v)
                except ValueError:
                    if ImageFile.LOAD_TRUNCATED_IMAGES:
                        return s
                    else:
                        raise
                except zlib.error:
                    return s
            else:
                return s
        try:
            k = k.decode("latin-1", "strict")
            lang = lang.decode("utf-8", "strict")
            tk = tk.decode("utf-8", "strict")
            v = v.decode("utf-8", "strict")
        except UnicodeError:
            return s

        self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk)
        self.check_text_memory(len(v))

        return s

    def chunk_eXIf(self, pos, length):
        s = ImageFile._safe_read(self.fp, length)
        self.im_info["exif"] = b"Exif\x00\x00" + s
        return s

    # APNG chunks
    def chunk_acTL(self, pos, length):
        s = ImageFile._safe_read(self.fp, length)
        if length < 8:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                return s
            msg = "APNG contains truncated acTL chunk"
            raise ValueError(msg)
        if self.im_n_frames is not None:
            self.im_n_frames = None
            warnings.warn("Invalid APNG, will use default PNG image if possible")
            return s
        n_frames = i32(s)
        if n_frames == 0 or n_frames > 0x80000000:
            warnings.warn("Invalid APNG, will use default PNG image if possible")
            return s
        self.im_n_frames = n_frames
        self.im_info["loop"] = i32(s, 4)
        self.im_custom_mimetype = "image/apng"
        return s

    def chunk_fcTL(self, pos, length):
        s = ImageFile._safe_read(self.fp, length)
        if length < 26:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                return s
            msg = "APNG contains truncated fcTL chunk"
            raise ValueError(msg)
        seq = i32(s)
        if (self._seq_num is None and seq != 0) or (
            self._seq_num is not None and self._seq_num != seq - 1
        ):
            msg = "APNG contains frame sequence errors"
            raise SyntaxError(msg)
        self._seq_num = seq
        width, height = i32(s, 4), i32(s, 8)
        px, py = i32(s, 12), i32(s, 16)
        im_w, im_h = self.im_size
        if px + width > im_w or py + height > im_h:
            msg = "APNG contains invalid frames"
            raise SyntaxError(msg)
        self.im_info["bbox"] = (px, py, px + width, py + height)
        delay_num, delay_den = i16(s, 20), i16(s, 22)
        if delay_den == 0:
            delay_den = 100
        self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000
        self.im_info["disposal"] = s[24]
        self.im_info["blend"] = s[25]
        return s

    def chunk_fdAT(self, pos, length):
        if length < 4:
            if ImageFile.LOAD_TRUNCATED_IMAGES:
                s = ImageFile._safe_read(self.fp, length)
                return s
            msg = "APNG contains truncated fDAT chunk"
            raise ValueError(msg)
        s = ImageFile._safe_read(self.fp, 4)
        seq = i32(s)
        if self._seq_num != seq - 1:
            msg = "APNG contains frame sequence errors"
            raise SyntaxError(msg)
        self._seq_num = seq
        return self.chunk_IDAT(pos + 4, length - 4)


# --------------------------------------------------------------------
# PNG reader


def _accept(prefix):
    return prefix[:8] == _MAGIC


##
# Image plugin for PNG images.


class PngImageFile(ImageFile.ImageFile):
    format = "PNG"
    format_description = "Portable network graphics"

    def _open(self):
        if not _accept(self.fp.read(8)):
            msg = "not a PNG file"
            raise SyntaxError(msg)
        self._fp = self.fp
        self.__frame = 0

        #
        # Parse headers up to the first IDAT or fDAT chunk

        self.private_chunks = []
        self.png = PngStream(self.fp)

        while True:
            #
            # get next chunk

            cid, pos, length = self.png.read()

            try:
                s = self.png.call(cid, pos, length)
            except EOFError:
                break
            except AttributeError:
                logger.debug("%r %s %s (unknown)", cid, pos, length)
                s = ImageFile._safe_read(self.fp, length)
                if cid[1:2].islower():
                    self.private_chunks.append((cid, s))

            self.png.crc(cid, s)

        #
        # Copy relevant attributes from the PngStream.  An alternative
        # would be to let the PngStream class modify these attributes
        # directly, but that introduces circular references which are
        # difficult to break if things go wrong in the decoder...
        # (believe me, I've tried ;-)

        self._mode = self.png.im_mode
        self._size = self.png.im_size
        self.info = self.png.im_info
        self._text = None
        self.tile = self.png.im_tile
        self.custom_mimetype = self.png.im_custom_mimetype
        self.n_frames = self.png.im_n_frames or 1
        self.default_image = self.info.get("default_image", False)

        if self.png.im_palette:
            rawmode, data = self.png.im_palette
            self.palette = ImagePalette.raw(rawmode, data)

        if cid == b"fdAT":
            self.__prepare_idat = length - 4
        else:
            self.__prepare_idat = length  # used by load_prepare()

        if self.png.im_n_frames is not None:
            self._close_exclusive_fp_after_loading = False
            self.png.save_rewind()
            self.__rewind_idat = self.__prepare_idat
            self.__rewind = self._fp.tell()
            if self.default_image:
                # IDAT chunk contains default image and not first animation frame
                self.n_frames += 1
            self._seek(0)
        self.is_animated = self.n_frames > 1

    @property
    def text(self):
        # experimental
        if self._text is None:
            # iTxt, tEXt and zTXt chunks may appear at the end of the file
            # So load the file to ensure that they are read
            if self.is_animated:
                frame = self.__frame
                # for APNG, seek to the final frame before loading
                self.seek(self.n_frames - 1)
            self.load()
            if self.is_animated:
                self.seek(frame)
        return self._text

    def verify(self):
        """Verify PNG file"""

        if self.fp is None:
            msg = "verify must be called directly after open"
            raise RuntimeError(msg)

        # back up to beginning of IDAT block
        self.fp.seek(self.tile[0][2] - 8)

        self.png.verify()
        self.png.close()

        if self._exclusive_fp:
            self.fp.close()
        self.fp = None

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

        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 APNG file"
                raise EOFError(msg) from e

    def _seek(self, frame, rewind=False):
        if frame == 0:
            if rewind:
                self._fp.seek(self.__rewind)
                self.png.rewind()
                self.__prepare_idat = self.__rewind_idat
                self.im = None
                if self.pyaccess:
                    self.pyaccess = None
                self.info = self.png.im_info
                self.tile = self.png.im_tile
                self.fp = self._fp
            self._prev_im = None
            self.dispose = None
            self.default_image = self.info.get("default_image", False)
            self.dispose_op = self.info.get("disposal")
            self.blend_op = self.info.get("blend")
            self.dispose_extent = self.info.get("bbox")
            self.__frame = 0
        else:
            if frame != self.__frame + 1:
                msg = f"cannot seek to frame {frame}"
                raise ValueError(msg)

            # ensure previous frame was loaded
            self.load()

            if self.dispose:
                self.im.paste(self.dispose, self.dispose_extent)
            self._prev_im = self.im.copy()

            self.fp = self._fp

            # advance to the next frame
            if self.__prepare_idat:
                ImageFile._safe_read(self.fp, self.__prepare_idat)
                self.__prepare_idat = 0
            frame_start = False
            while True:
                self.fp.read(4)  # CRC

                try:
                    cid, pos, length = self.png.read()
                except (struct.error, SyntaxError):
                    break

                if cid == b"IEND":
                    msg = "No more images in APNG file"
                    raise EOFError(msg)
                if cid == b"fcTL":
                    if frame_start:
                        # there must be at least one fdAT chunk between fcTL chunks
                        msg = "APNG missing frame data"
                        raise SyntaxError(msg)
                    frame_start = True

                try:
                    self.png.call(cid, pos, length)
                except UnicodeDecodeError:
                    break
                except EOFError:
                    if cid == b"fdAT":
                        length -= 4
                        if frame_start:
                            self.__prepare_idat = length
                            break
                    ImageFile._safe_read(self.fp, length)
                except AttributeError:
                    logger.debug("%r %s %s (unknown)", cid, pos, length)
                    ImageFile._safe_read(self.fp, length)

            self.__frame = frame
            self.tile = self.png.im_tile
            self.dispose_op = self.info.get("disposal")
            self.blend_op = self.info.get("blend")
            self.dispose_extent = self.info.get("bbox")

            if not self.tile:
                msg = "image not found in APNG frame"
                raise EOFError(msg)

        # setup frame disposal (actual disposal done when needed in the next _seek())
        if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:
            self.dispose_op = Disposal.OP_BACKGROUND

        if self.dispose_op == Disposal.OP_PREVIOUS:
            self.dispose = self._prev_im.copy()
            self.dispose = self._crop(self.dispose, self.dispose_extent)
        elif self.dispose_op == Disposal.OP_BACKGROUND:
            self.dispose = Image.core.fill(self.mode, self.size)
            self.dispose = self._crop(self.dispose, self.dispose_extent)
        else:
            self.dispose = None

    def tell(self):
        return self.__frame

    def load_prepare(self):
        """internal: prepare to read PNG file"""

        if self.info.get("interlace"):
            self.decoderconfig = self.decoderconfig + (1,)

        self.__idat = self.__prepare_idat  # used by load_read()
        ImageFile.ImageFile.load_prepare(self)

    def load_read(self, read_bytes):
        """internal: read more image data"""

        while self.__idat == 0:
            # end of chunk, skip forward to next one

            self.fp.read(4)  # CRC

            cid, pos, length = self.png.read()

            if cid not in [b"IDAT", b"DDAT", b"fdAT"]:
                self.png.push(cid, pos, length)
                return b""

            if cid == b"fdAT":
                try:
                    self.png.call(cid, pos, length)
                except EOFError:
                    pass
                self.__idat = length - 4  # sequence_num has already been read
            else:
                self.__idat = length  # empty chunks are allowed

        # read more data from this chunk
        if read_bytes <= 0:
            read_bytes = self.__idat
        else:
            read_bytes = min(read_bytes, self.__idat)

        self.__idat = self.__idat - read_bytes

        return self.fp.read(read_bytes)

    def load_end(self):
        """internal: finished reading image data"""
        if self.__idat != 0:
            self.fp.read(self.__idat)
        while True:
            self.fp.read(4)  # CRC

            try:
                cid, pos, length = self.png.read()
            except (struct.error, SyntaxError):
                break

            if cid == b"IEND":
                break
            elif cid == b"fcTL" and self.is_animated:
                # start of the next frame, stop reading
                self.__prepare_idat = 0
                self.png.push(cid, pos, length)
                break

            try:
                self.png.call(cid, pos, length)
            except UnicodeDecodeError:
                break
            except EOFError:
                if cid == b"fdAT":
                    length -= 4
                ImageFile._safe_read(self.fp, length)
            except AttributeError:
                logger.debug("%r %s %s (unknown)", cid, pos, length)
                s = ImageFile._safe_read(self.fp, length)
                if cid[1:2].islower():
                    self.private_chunks.append((cid, s, True))
        self._text = self.png.im_text
        if not self.is_animated:
            self.png.close()
            self.png = None
        else:
            if self._prev_im and self.blend_op == Blend.OP_OVER:
                updated = self._crop(self.im, self.dispose_extent)
                if self.im.mode == "RGB" and "transparency" in self.info:
                    mask = updated.convert_transparent(
                        "RGBA", self.info["transparency"]
                    )
                else:
                    mask = updated.convert("RGBA")
                self._prev_im.paste(updated, self.dispose_extent, mask)
                self.im = self._prev_im
                if self.pyaccess:
                    self.pyaccess = None

    def _getexif(self):
        if "exif" not in self.info:
            self.load()
        if "exif" not in self.info and "Raw profile type exif" not in self.info:
            return None
        return self.getexif()._get_merged_dict()

    def getexif(self):
        if "exif" not in self.info:
            self.load()

        return super().getexif()

    def getxmp(self):
        """
        Returns a dictionary containing the XMP tags.
        Requires defusedxml to be installed.

        :returns: XMP tags in a dictionary.
        """
        return (
            self._getxmp(self.info["XML:com.adobe.xmp"])
            if "XML:com.adobe.xmp" in self.info
            else {}
        )


# --------------------------------------------------------------------
# PNG writer

_OUTMODES = {
    # supported PIL modes, and corresponding rawmodes/bits/color combinations
    "1": ("1", b"\x01\x00"),
    "L;1": ("L;1", b"\x01\x00"),
    "L;2": ("L;2", b"\x02\x00"),
    "L;4": ("L;4", b"\x04\x00"),
    "L": ("L", b"\x08\x00"),
    "LA": ("LA", b"\x08\x04"),
    "I": ("I;16B", b"\x10\x00"),
    "I;16": ("I;16B", b"\x10\x00"),
    "I;16B": ("I;16B", b"\x10\x00"),
    "P;1": ("P;1", b"\x01\x03"),
    "P;2": ("P;2", b"\x02\x03"),
    "P;4": ("P;4", b"\x04\x03"),
    "P": ("P", b"\x08\x03"),
    "RGB": ("RGB", b"\x08\x02"),
    "RGBA": ("RGBA", b"\x08\x06"),
}


def putchunk(fp, cid, *data):
    """Write a PNG chunk (including CRC field)"""

    data = b"".join(data)

    fp.write(o32(len(data)) + cid)
    fp.write(data)
    crc = _crc32(data, _crc32(cid))
    fp.write(o32(crc))


class _idat:
    # wrap output from the encoder in IDAT chunks

    def __init__(self, fp, chunk):
        self.fp = fp
        self.chunk = chunk

    def write(self, data):
        self.chunk(self.fp, b"IDAT", data)


class _fdat:
    # wrap encoder output in fdAT chunks

    def __init__(self, fp, chunk, seq_num):
        self.fp = fp
        self.chunk = chunk
        self.seq_num = seq_num

    def write(self, data):
        self.chunk(self.fp, b"fdAT", o32(self.seq_num), data)
        self.seq_num += 1


def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images):
    duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
    loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
    disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
    blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))

    if default_image:
        chain = itertools.chain(append_images)
    else:
        chain = itertools.chain([im], append_images)

    im_frames = []
    frame_count = 0
    for im_seq in chain:
        for im_frame in ImageSequence.Iterator(im_seq):
            if im_frame.mode == rawmode:
                im_frame = im_frame.copy()
            else:
                im_frame = im_frame.convert(rawmode)
            encoderinfo = im.encoderinfo.copy()
            if isinstance(duration, (list, tuple)):
                encoderinfo["duration"] = duration[frame_count]
            if isinstance(disposal, (list, tuple)):
                encoderinfo["disposal"] = disposal[frame_count]
            if isinstance(blend, (list, tuple)):
                encoderinfo["blend"] = blend[frame_count]
            frame_count += 1

            if im_frames:
                previous = im_frames[-1]
                prev_disposal = previous["encoderinfo"].get("disposal")
                prev_blend = previous["encoderinfo"].get("blend")
                if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:
                    prev_disposal = Disposal.OP_BACKGROUND

                if prev_disposal == Disposal.OP_BACKGROUND:
                    base_im = previous["im"].copy()
                    dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0))
                    bbox = previous["bbox"]
                    if bbox:
                        dispose = dispose.crop(bbox)
                    else:
                        bbox = (0, 0) + im.size
                    base_im.paste(dispose, bbox)
                elif prev_disposal == Disposal.OP_PREVIOUS:
                    base_im = im_frames[-2]["im"]
                else:
                    base_im = previous["im"]
                delta = ImageChops.subtract_modulo(
                    im_frame.convert("RGBA"), base_im.convert("RGBA")
                )
                bbox = delta.getbbox(alpha_only=False)
                if (
                    not bbox
                    and prev_disposal == encoderinfo.get("disposal")
                    and prev_blend == encoderinfo.get("blend")
                ):
                    previous["encoderinfo"]["duration"] += encoderinfo.get(
                        "duration", duration
                    )
                    continue
            else:
                bbox = None
            if "duration" not in encoderinfo:
                encoderinfo["duration"] = duration
            im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})

    # animation control
    chunk(
        fp,
        b"acTL",
        o32(len(im_frames)),  # 0: num_frames
        o32(loop),  # 4: num_plays
    )

    # default image IDAT (if it exists)
    if default_image:
        if im.mode != rawmode:
            im = im.convert(rawmode)
        ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])

    seq_num = 0
    for frame, frame_data in enumerate(im_frames):
        im_frame = frame_data["im"]
        if not frame_data["bbox"]:
            bbox = (0, 0) + im_frame.size
        else:
            bbox = frame_data["bbox"]
            im_frame = im_frame.crop(bbox)
        size = im_frame.size
        encoderinfo = frame_data["encoderinfo"]
        frame_duration = int(round(encoderinfo["duration"]))
        frame_disposal = encoderinfo.get("disposal", disposal)
        frame_blend = encoderinfo.get("blend", blend)
        # frame control
        chunk(
            fp,
            b"fcTL",
            o32(seq_num),  # sequence_number
            o32(size[0]),  # width
            o32(size[1]),  # height
            o32(bbox[0]),  # x_offset
            o32(bbox[1]),  # y_offset
            o16(frame_duration),  # delay_numerator
            o16(1000),  # delay_denominator
            o8(frame_disposal),  # dispose_op
            o8(frame_blend),  # blend_op
        )
        seq_num += 1
        # frame data
        if frame == 0 and not default_image:
            # first frame must be in IDAT chunks for backwards compatibility
            ImageFile._save(
                im_frame,
                _idat(fp, chunk),
                [("zip", (0, 0) + im_frame.size, 0, rawmode)],
            )
        else:
            fdat_chunks = _fdat(fp, chunk, seq_num)
            ImageFile._save(
                im_frame,
                fdat_chunks,
                [("zip", (0, 0) + im_frame.size, 0, rawmode)],
            )
            seq_num = fdat_chunks.seq_num


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


def _save(im, fp, filename, chunk=putchunk, save_all=False):
    # save an image to disk (called by the save method)

    if save_all:
        default_image = im.encoderinfo.get(
            "default_image", im.info.get("default_image")
        )
        modes = set()
        append_images = im.encoderinfo.get("append_images", [])
        for im_seq in itertools.chain([im], append_images):
            for im_frame in ImageSequence.Iterator(im_seq):
                modes.add(im_frame.mode)
        for mode in ("RGBA", "RGB", "P"):
            if mode in modes:
                break
        else:
            mode = modes.pop()
    else:
        mode = im.mode

    if mode == "P":
        #
        # attempt to minimize storage requirements for palette images
        if "bits" in im.encoderinfo:
            # number of bits specified by user
            colors = min(1 << im.encoderinfo["bits"], 256)
        else:
            # check palette contents
            if im.palette:
                colors = max(min(len(im.palette.getdata()[1]) // 3, 256), 1)
            else:
                colors = 256

        if colors <= 16:
            if colors <= 2:
                bits = 1
            elif colors <= 4:
                bits = 2
            else:
                bits = 4
            mode = f"{mode};{bits}"

    # encoder options
    im.encoderconfig = (
        im.encoderinfo.get("optimize", False),
        im.encoderinfo.get("compress_level", -1),
        im.encoderinfo.get("compress_type", -1),
        im.encoderinfo.get("dictionary", b""),
    )

    # get the corresponding PNG mode
    try:
        rawmode, mode = _OUTMODES[mode]
    except KeyError as e:
        msg = f"cannot write mode {mode} as PNG"
        raise OSError(msg) from e

    #
    # write minimal PNG file

    fp.write(_MAGIC)

    chunk(
        fp,
        b"IHDR",
        o32(im.size[0]),  # 0: size
        o32(im.size[1]),
        mode,  # 8: depth/type
        b"\0",  # 10: compression
        b"\0",  # 11: filter category
        b"\0",  # 12: interlace flag
    )

    chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"]

    icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile"))
    if icc:
        # ICC profile
        # according to PNG spec, the iCCP chunk contains:
        # Profile name  1-79 bytes (character string)
        # Null separator        1 byte (null character)
        # Compression method    1 byte (0)
        # Compressed profile    n bytes (zlib with deflate compression)
        name = b"ICC Profile"
        data = name + b"\0\0" + zlib.compress(icc)
        chunk(fp, b"iCCP", data)

        # You must either have sRGB or iCCP.
        # Disallow sRGB chunks when an iCCP-chunk has been emitted.
        chunks.remove(b"sRGB")

    info = im.encoderinfo.get("pnginfo")
    if info:
        chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"]
        for info_chunk in info.chunks:
            cid, data = info_chunk[:2]
            if cid in chunks:
                chunks.remove(cid)
                chunk(fp, cid, data)
            elif cid in chunks_multiple_allowed:
                chunk(fp, cid, data)
            elif cid[1:2].islower():
                # Private chunk
                after_idat = info_chunk[2:3]
                if not after_idat:
                    chunk(fp, cid, data)

    if im.mode == "P":
        palette_byte_number = colors * 3
        palette_bytes = im.im.getpalette("RGB")[:palette_byte_number]
        while len(palette_bytes) < palette_byte_number:
            palette_bytes += b"\0"
        chunk(fp, b"PLTE", palette_bytes)

    transparency = im.encoderinfo.get("transparency", im.info.get("transparency", None))

    if transparency or transparency == 0:
        if im.mode == "P":
            # limit to actual palette size
            alpha_bytes = colors
            if isinstance(transparency, bytes):
                chunk(fp, b"tRNS", transparency[:alpha_bytes])
            else:
                transparency = max(0, min(255, transparency))
                alpha = b"\xFF" * transparency + b"\0"
                chunk(fp, b"tRNS", alpha[:alpha_bytes])
        elif im.mode in ("1", "L", "I"):
            transparency = max(0, min(65535, transparency))
            chunk(fp, b"tRNS", o16(transparency))
        elif im.mode == "RGB":
            red, green, blue = transparency
            chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue))
        else:
            if "transparency" in im.encoderinfo:
                # don't bother with transparency if it's an RGBA
                # and it's in the info dict. It's probably just stale.
                msg = "cannot use transparency for this mode"
                raise OSError(msg)
    else:
        if im.mode == "P" and im.im.getpalettemode() == "RGBA":
            alpha = im.im.getpalette("RGBA", "A")
            alpha_bytes = colors
            chunk(fp, b"tRNS", alpha[:alpha_bytes])

    dpi = im.encoderinfo.get("dpi")
    if dpi:
        chunk(
            fp,
            b"pHYs",
            o32(int(dpi[0] / 0.0254 + 0.5)),
            o32(int(dpi[1] / 0.0254 + 0.5)),
            b"\x01",
        )

    if info:
        chunks = [b"bKGD", b"hIST"]
        for info_chunk in info.chunks:
            cid, data = info_chunk[:2]
            if cid in chunks:
                chunks.remove(cid)
                chunk(fp, cid, data)

    exif = im.encoderinfo.get("exif")
    if exif:
        if isinstance(exif, Image.Exif):
            exif = exif.tobytes(8)
        if exif.startswith(b"Exif\x00\x00"):
            exif = exif[6:]
        chunk(fp, b"eXIf", exif)

    if save_all:
        _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
    else:
        ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])

    if info:
        for info_chunk in info.chunks:
            cid, data = info_chunk[:2]
            if cid[1:2].islower():
                # Private chunk
                after_idat = info_chunk[2:3]
                if after_idat:
                    chunk(fp, cid, data)

    chunk(fp, b"IEND", b"")

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


# --------------------------------------------------------------------
# PNG chunk converter


def getchunks(im, **params):
    """Return a list of PNG chunks representing this image."""

    class collector:
        data = []

        def write(self, data):
            pass

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

    def append(fp, cid, *data):
        data = b"".join(data)
        crc = o32(_crc32(data, _crc32(cid)))
        fp.append((cid, data, crc))

    fp = collector()

    try:
        im.encoderinfo = params
        _save(im, fp, None, append)
    finally:
        del im.encoderinfo

    return fp.data


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

Image.register_open(PngImageFile.format, PngImageFile, _accept)
Image.register_save(PngImageFile.format, _save)
Image.register_save_all(PngImageFile.format, _save_all)

Image.register_extensions(PngImageFile.format, [".png", ".apng"])

Image.register_mime(PngImageFile.format, "image/png")

```
--- 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