{
    "file_diffs": [
        {
            "old_file_content": "from helper import unittest, PillowTestCase, hopper, netpbm_available\n\nfrom PIL import Image, ImagePalette, GifImagePlugin\n\nfrom io import BytesIO\n\ncodecs = dir(Image.core)\n\n# sample gif stream\nTEST_GIF = \"Tests/images/hopper.gif\"\n\nwith open(TEST_GIF, \"rb\") as f:\n    data = f.read()\n\n\nclass TestFileGif(PillowTestCase):\n\n    def setUp(self):\n        if \"gif_encoder\" not in codecs or \"gif_decoder\" not in codecs:\n            self.skipTest(\"gif support not available\")  # can this happen?\n\n    def test_sanity(self):\n        im = Image.open(TEST_GIF)\n        im.load()\n        self.assertEqual(im.mode, \"P\")\n        self.assertEqual(im.size, (128, 128))\n        self.assertEqual(im.format, \"GIF\")\n        self.assertEqual(im.info[\"version\"], b\"GIF89a\")\n\n    def test_invalid_file(self):\n        invalid_file = \"Tests/images/flower.jpg\"\n\n        self.assertRaises(SyntaxError,\n                          GifImagePlugin.GifImageFile, invalid_file)\n\n    def test_optimize(self):\n        def test_grayscale(optimize):\n            im = Image.new(\"L\", (1, 1), 0)\n            filename = BytesIO()\n            im.save(filename, \"GIF\", optimize=optimize)\n            return len(filename.getvalue())\n\n        def test_bilevel(optimize):\n            im = Image.new(\"1\", (1, 1), 0)\n            test_file = BytesIO()\n            im.save(test_file, \"GIF\", optimize=optimize)\n            return len(test_file.getvalue())\n\n        self.assertEqual(test_grayscale(0), 800)\n        self.assertEqual(test_grayscale(1), 38)\n        self.assertEqual(test_bilevel(0), 800)\n        self.assertEqual(test_bilevel(1), 800)\n\n    def test_optimize_correctness(self):\n        # 256 color Palette image, posterize to > 128 and < 128 levels\n        # Size bigger and smaller than 512x512\n        # Check the palette for number of colors allocated.\n        # Check for correctness after conversion back to RGB\n        def check(colors, size, expected_palette_length):\n            # make an image with empty colors in the start of the palette range\n            im = Image.frombytes('P', (colors, colors),\n                                 bytes(bytearray(range(256-colors, 256))*colors))\n            im = im.resize((size, size))\n            outfile = BytesIO()\n            im.save(outfile, 'GIF')\n            outfile.seek(0)\n            reloaded = Image.open(outfile)\n\n            # check palette length\n            palette_length = max(i+1 for i, v in enumerate(reloaded.histogram()) if v)\n            self.assertEqual(expected_palette_length, palette_length)\n\n            self.assert_image_equal(im.convert('RGB'), reloaded.convert('RGB'))\n\n        # These do optimize the palette\n        check(128, 511, 128)\n        check(64, 511, 64)\n        check(4, 511, 4)\n\n        # These don't optimize the palette\n        check(128, 513, 256)\n        check(64, 513, 256)\n        check(4, 513, 256)\n\n        # other limits that don't optimize the palette\n        check(129, 511, 256)\n        check(255, 511, 256)\n        check(256, 511, 256)\n\n    def test_optimize_full_l(self):\n        im = Image.frombytes(\"L\", (16, 16), bytes(bytearray(range(256))))\n        test_file = BytesIO()\n        im.save(test_file, \"GIF\", optimize=True)\n        self.assertEqual(im.mode, \"L\")\n\n    def test_roundtrip(self):\n        out = self.tempfile('temp.gif')\n        im = hopper()\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), im, 50)\n\n    def test_roundtrip2(self):\n        # see https://github.com/python-pillow/Pillow/issues/403\n        out = self.tempfile('temp.gif')\n        im = Image.open(TEST_GIF)\n        im2 = im.copy()\n        im2.save(out)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), hopper(), 50)\n\n    def test_roundtrip_save_all(self):\n        # Single frame image\n        out = self.tempfile('temp.gif')\n        im = hopper()\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), im, 50)\n\n        # Multiframe image\n        im = Image.open(\"Tests/images/dispose_bgnd.gif\")\n\n        out = self.tempfile('temp.gif')\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.n_frames, 5)\n\n    def test_headers_saving_for_animated_gifs(self):\n        important_headers = ['background', 'version', 'duration', 'loop']\n        # Multiframe image\n        im = Image.open(\"Tests/images/dispose_bgnd.gif\")\n\n        out = self.tempfile('temp.gif')\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        for header in important_headers:\n            self.assertEqual(\n                im.info[header],\n                reread.info[header]\n            )\n\n    def test_palette_handling(self):\n        # see https://github.com/python-pillow/Pillow/issues/513\n\n        im = Image.open(TEST_GIF)\n        im = im.convert('RGB')\n\n        im = im.resize((100, 100), Image.LANCZOS)\n        im2 = im.convert('P', palette=Image.ADAPTIVE, colors=256)\n\n        f = self.tempfile('temp.gif')\n        im2.save(f, optimize=True)\n\n        reloaded = Image.open(f)\n\n        self.assert_image_similar(im, reloaded.convert('RGB'), 10)\n\n    def test_palette_434(self):\n        # see https://github.com/python-pillow/Pillow/issues/434\n\n        def roundtrip(im, *args, **kwargs):\n            out = self.tempfile('temp.gif')\n            im.copy().save(out, *args, **kwargs)\n            reloaded = Image.open(out)\n\n            return reloaded\n\n        orig = \"Tests/images/test.colors.gif\"\n        im = Image.open(orig)\n\n        self.assert_image_similar(im, roundtrip(im), 1)\n        self.assert_image_similar(im, roundtrip(im, optimize=True), 1)\n\n        im = im.convert(\"RGB\")\n        # check automatic P conversion\n        reloaded = roundtrip(im).convert('RGB')\n        self.assert_image_equal(im, reloaded)\n\n    @unittest.skipUnless(netpbm_available(), \"netpbm not available\")\n    def test_save_netpbm_bmp_mode(self):\n        img = Image.open(TEST_GIF).convert(\"RGB\")\n\n        tempfile = self.tempfile(\"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        self.assert_image_similar(img, Image.open(tempfile).convert(\"RGB\"), 0)\n\n    @unittest.skipUnless(netpbm_available(), \"netpbm not available\")\n    def test_save_netpbm_l_mode(self):\n        img = Image.open(TEST_GIF).convert(\"L\")\n\n        tempfile = self.tempfile(\"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        self.assert_image_similar(img, Image.open(tempfile).convert(\"L\"), 0)\n\n    def test_seek(self):\n        img = Image.open(\"Tests/images/dispose_none.gif\")\n        framecount = 0\n        try:\n            while True:\n                framecount += 1\n                img.seek(img.tell() + 1)\n        except EOFError:\n            self.assertEqual(framecount, 5)\n\n    def test_n_frames(self):\n        for path, n_frames in [\n            [TEST_GIF, 1],\n            ['Tests/images/iss634.gif', 42]\n        ]:\n            # Test is_animated before n_frames\n            im = Image.open(path)\n            self.assertEqual(im.is_animated, n_frames != 1)\n\n            # Test is_animated after n_frames\n            im = Image.open(path)\n            self.assertEqual(im.n_frames, n_frames)\n            self.assertEqual(im.is_animated, n_frames != 1)\n\n    def test_eoferror(self):\n        im = Image.open(TEST_GIF)\n        n_frames = im.n_frames\n\n        # Test seeking past the last frame\n        self.assertRaises(EOFError, im.seek, n_frames)\n        self.assertLess(im.tell(), n_frames)\n\n        # Test that seeking to the last frame does not raise an error\n        im.seek(n_frames-1)\n\n    def test_dispose_none(self):\n        img = Image.open(\"Tests/images/dispose_none.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 1)\n        except EOFError:\n            pass\n\n    def test_dispose_background(self):\n        img = Image.open(\"Tests/images/dispose_bgnd.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 2)\n        except EOFError:\n            pass\n\n    def test_dispose_previous(self):\n        img = Image.open(\"Tests/images/dispose_prev.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 3)\n        except EOFError:\n            pass\n\n    def test_save_dispose(self):\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111'),\n            Image.new('L', (100, 100), '#222'),\n        ]\n        for method in range(0, 4):\n            im_list[0].save(\n                out,\n                save_all=True,\n                append_images=im_list[1:],\n                disposal=method\n            )\n            img = Image.open(out)\n            for _ in range(2):\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, method)\n\n        # check per frame disposal\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            disposal=tuple(range(len(im_list)))\n            )\n\n        img = Image.open(out)\n\n        for i in range(2):\n            img.seek(img.tell() + 1)\n            self.assertEqual(img.disposal_method, i+1)\n\n    def test_iss634(self):\n        img = Image.open(\"Tests/images/iss634.gif\")\n        # seek to the second frame\n        img.seek(img.tell() + 1)\n        # all transparent pixels should be replaced with the color from the\n        # first frame\n        self.assertEqual(img.histogram()[img.info['transparency']], 0)\n\n    def test_duration(self):\n        duration = 1000\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.save(out, duration=duration)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['duration'], duration)\n\n    def test_multiple_duration(self):\n        duration_list = [1000, 2000, 3000]\n\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111'),\n            Image.new('L', (100, 100), '#222')\n        ]\n\n        # duration as list\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=duration_list\n        )\n        reread = Image.open(out)\n\n        for duration in duration_list:\n            self.assertEqual(reread.info['duration'], duration)\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n        # duration as tuple\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=tuple(duration_list)\n        )\n        reread = Image.open(out)\n\n        for duration in duration_list:\n            self.assertEqual(reread.info['duration'], duration)\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n    def test_identical_frames(self):\n        duration_list = [1000, 1500, 2000, 4000]\n\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111')\n        ]\n\n        # duration as list\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=duration_list\n        )\n        reread = Image.open(out)\n\n        # Assert that the first three frames were combined\n        self.assertEqual(reread.n_frames, 2)\n\n        # Assert that the new duration is the total of the identical frames\n        self.assertEqual(reread.info['duration'], 4500)\n\n    def test_number_of_loops(self):\n        number_of_loops = 2\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.save(out, loop=number_of_loops)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['loop'], number_of_loops)\n\n    def test_background(self):\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.info['background'] = 1\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['background'], im.info['background'])\n\n    def test_comment(self):\n        im = Image.open(TEST_GIF)\n        self.assertEqual(im.info['comment'], b\"File written by Adobe Photoshop\\xa8 4.0\")\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.info['comment'] = b\"Test comment text\"\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['comment'], im.info['comment'])\n\n    def test_version(self):\n        out = self.tempfile('temp.gif')\n\n        def assertVersionAfterSave(im, version):\n            im.save(out)\n            reread = Image.open(out)\n            self.assertEqual(reread.info[\"version\"], version)\n\n        # Test that GIF87a is used by default\n        im = Image.new('L', (100, 100), '#000')\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n        # Test setting the version to 89a\n        im = Image.new('L', (100, 100), '#000')\n        im.info[\"version\"] = b\"89a\"\n        assertVersionAfterSave(im, b\"GIF89a\")\n\n        # Test that adding a GIF89a feature changes the version\n        im.info[\"transparency\"] = 1\n        assertVersionAfterSave(im, b\"GIF89a\")\n\n        # Test that a GIF87a image is also saved in that format\n        im = Image.open(\"Tests/images/test.colors.gif\")\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n        # Test that a GIF89a image is also saved in that format\n        im.info[\"version\"] = b\"GIF89a\"\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n    def test_append_images(self):\n        out = self.tempfile('temp.gif')\n\n        # Test appending single frame images\n        im = Image.new('RGB', (100, 100), '#f00')\n        ims = [Image.new('RGB', (100, 100), color) for color\n               in ['#0f0', '#00f']]\n        im.copy().save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 3)\n\n        # Tests appending using a generator\n        def imGenerator(ims):\n            for im in ims:\n                yield im\n        im.save(out, save_all=True, append_images=imGenerator(ims))\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 3)\n\n        # Tests appending single and multiple frame images\n        im = Image.open(\"Tests/images/dispose_none.gif\")\n        ims = [Image.open(\"Tests/images/dispose_prev.gif\")]\n        im.save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 10)\n\n    def test_transparent_optimize(self):\n        # from issue #2195, if the transparent color is incorrectly\n        # optimized out, gif loses transparency\n        # Need a palette that isn't using the 0 color, and one\n        # that's > 128 items where the transparent color is actually\n        # the top palette entry to trigger the bug.\n\n        data = bytes(bytearray(range(1, 254)))\n        palette = ImagePalette.ImagePalette(\"RGB\", list(range(256))*3)\n\n        im = Image.new('L', (253, 1))\n        im.frombytes(data)\n        im.putpalette(palette)\n\n        out = self.tempfile('temp.gif')\n        im.save(out, transparency=253)\n        reloaded = Image.open(out)\n\n        self.assertEqual(reloaded.info['transparency'], 253)\n\n    def test_bbox(self):\n        out = self.tempfile('temp.gif')\n\n        im = Image.new('RGB', (100, 100), '#fff')\n        ims = [Image.new(\"RGB\", (100, 100), '#000')]\n        im.save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 2)\n\n    def test_palette_save_L(self):\n        # generate an L mode image with a separate palette\n\n        im = hopper('P')\n        im_l = Image.frombytes('L', im.size, im.tobytes())\n        palette = bytes(bytearray(im.getpalette()))\n\n        out = self.tempfile('temp.gif')\n        im_l.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n\n        self.assert_image_equal(reloaded.convert('RGB'), im.convert('RGB'))\n\n    def test_palette_save_P(self):\n        # pass in a different palette, then construct what the image\n        # would look like.\n        # Forcing a non-straight grayscale palette.\n\n        im = hopper('P')\n        palette = bytes(bytearray([255-i//3 for i in range(768)]))\n\n        out = self.tempfile('temp.gif')\n        im.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n        im.putpalette(palette)\n        self.assert_image_equal(reloaded, im)\n\n    def test_palette_save_ImagePalette(self):\n        # pass in a different palette, as an ImagePalette.ImagePalette\n        # effectively the same as test_palette_save_P\n\n        im = hopper('P')\n        palette = ImagePalette.ImagePalette('RGB', list(range(256))[::-1]*3)\n\n        out = self.tempfile('temp.gif')\n        im.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n        im.putpalette(palette)\n        self.assert_image_equal(reloaded, im)\n\n    def test_save_I(self):\n        # Test saving something that would trigger the auto-convert to 'L'\n\n        im = hopper('I')\n\n        out = self.tempfile('temp.gif')\n        im.save(out)\n\n        reloaded = Image.open(out)\n        self.assert_image_equal(reloaded.convert('L'), im.convert('L'))\n\n    def test_getdata(self):\n        # test getheader/getdata against legacy values\n        # Create a 'P' image with holes in the palette\n        im = Image._wedge().resize((16, 16))\n        im.putpalette(ImagePalette.ImagePalette('RGB'))\n        im.info = {'background': 0}\n\n        passed_palette = bytes(bytearray([255-i//3 for i in range(768)]))\n\n        GifImagePlugin._FORCE_OPTIMIZE = True\n        try:\n            h = GifImagePlugin.getheader(im, passed_palette)\n            d = GifImagePlugin.getdata(im)\n\n            import pickle\n            # Enable to get target values on pre-refactor version\n            # with open('Tests/images/gif_header_data.pkl', 'wb') as f:\n            #    pickle.dump((h, d), f, 1)\n            with open('Tests/images/gif_header_data.pkl', 'rb') as f:\n                (h_target, d_target) = pickle.load(f)\n\n            self.assertEqual(h, h_target)\n            self.assertEqual(d, d_target)\n        finally:\n            GifImagePlugin._FORCE_OPTIMIZE = False\n\n    def test_lzw_bits(self):\n        # see https://github.com/python-pillow/Pillow/issues/2811\n        im = Image.open('Tests/images/issue_2811.gif')\n\n        self.assertEqual(im.tile[0][3][0], 11)  # LZW bits\n        # codec error prepatch\n        im.load()\n\n\nif __name__ == '__main__':\n    unittest.main()\n",
            "new_file_content": "from helper import unittest, PillowTestCase, hopper, netpbm_available\n\nfrom PIL import Image, ImagePalette, GifImagePlugin\n\nfrom io import BytesIO\n\ncodecs = dir(Image.core)\n\n# sample gif stream\nTEST_GIF = \"Tests/images/hopper.gif\"\n\nwith open(TEST_GIF, \"rb\") as f:\n    data = f.read()\n\n\nclass TestFileGif(PillowTestCase):\n\n    def setUp(self):\n        if \"gif_encoder\" not in codecs or \"gif_decoder\" not in codecs:\n            self.skipTest(\"gif support not available\")  # can this happen?\n\n    def test_sanity(self):\n        im = Image.open(TEST_GIF)\n        im.load()\n        self.assertEqual(im.mode, \"P\")\n        self.assertEqual(im.size, (128, 128))\n        self.assertEqual(im.format, \"GIF\")\n        self.assertEqual(im.info[\"version\"], b\"GIF89a\")\n\n    def test_invalid_file(self):\n        invalid_file = \"Tests/images/flower.jpg\"\n\n        self.assertRaises(SyntaxError,\n                          GifImagePlugin.GifImageFile, invalid_file)\n\n    def test_optimize(self):\n        def test_grayscale(optimize):\n            im = Image.new(\"L\", (1, 1), 0)\n            filename = BytesIO()\n            im.save(filename, \"GIF\", optimize=optimize)\n            return len(filename.getvalue())\n\n        def test_bilevel(optimize):\n            im = Image.new(\"1\", (1, 1), 0)\n            test_file = BytesIO()\n            im.save(test_file, \"GIF\", optimize=optimize)\n            return len(test_file.getvalue())\n\n        self.assertEqual(test_grayscale(0), 800)\n        self.assertEqual(test_grayscale(1), 38)\n        self.assertEqual(test_bilevel(0), 800)\n        self.assertEqual(test_bilevel(1), 800)\n\n    def test_optimize_correctness(self):\n        # 256 color Palette image, posterize to > 128 and < 128 levels\n        # Size bigger and smaller than 512x512\n        # Check the palette for number of colors allocated.\n        # Check for correctness after conversion back to RGB\n        def check(colors, size, expected_palette_length):\n            # make an image with empty colors in the start of the palette range\n            im = Image.frombytes('P', (colors, colors),\n                                 bytes(bytearray(range(256-colors, 256))*colors))\n            im = im.resize((size, size))\n            outfile = BytesIO()\n            im.save(outfile, 'GIF')\n            outfile.seek(0)\n            reloaded = Image.open(outfile)\n\n            # check palette length\n            palette_length = max(i+1 for i, v in enumerate(reloaded.histogram()) if v)\n            self.assertEqual(expected_palette_length, palette_length)\n\n            self.assert_image_equal(im.convert('RGB'), reloaded.convert('RGB'))\n\n        # These do optimize the palette\n        check(128, 511, 128)\n        check(64, 511, 64)\n        check(4, 511, 4)\n\n        # These don't optimize the palette\n        check(128, 513, 256)\n        check(64, 513, 256)\n        check(4, 513, 256)\n\n        # other limits that don't optimize the palette\n        check(129, 511, 256)\n        check(255, 511, 256)\n        check(256, 511, 256)\n\n    def test_optimize_full_l(self):\n        im = Image.frombytes(\"L\", (16, 16), bytes(bytearray(range(256))))\n        test_file = BytesIO()\n        im.save(test_file, \"GIF\", optimize=True)\n        self.assertEqual(im.mode, \"L\")\n\n    def test_roundtrip(self):\n        out = self.tempfile('temp.gif')\n        im = hopper()\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), im, 50)\n\n    def test_roundtrip2(self):\n        # see https://github.com/python-pillow/Pillow/issues/403\n        out = self.tempfile('temp.gif')\n        im = Image.open(TEST_GIF)\n        im2 = im.copy()\n        im2.save(out)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), hopper(), 50)\n\n    def test_roundtrip_save_all(self):\n        # Single frame image\n        out = self.tempfile('temp.gif')\n        im = hopper()\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), im, 50)\n\n        # Multiframe image\n        im = Image.open(\"Tests/images/dispose_bgnd.gif\")\n\n        out = self.tempfile('temp.gif')\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.n_frames, 5)\n\n    def test_headers_saving_for_animated_gifs(self):\n        important_headers = ['background', 'version', 'duration', 'loop']\n        # Multiframe image\n        im = Image.open(\"Tests/images/dispose_bgnd.gif\")\n\n        out = self.tempfile('temp.gif')\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        for header in important_headers:\n            self.assertEqual(\n                im.info[header],\n                reread.info[header]\n            )\n\n    def test_palette_handling(self):\n        # see https://github.com/python-pillow/Pillow/issues/513\n\n        im = Image.open(TEST_GIF)\n        im = im.convert('RGB')\n\n        im = im.resize((100, 100), Image.LANCZOS)\n        im2 = im.convert('P', palette=Image.ADAPTIVE, colors=256)\n\n        f = self.tempfile('temp.gif')\n        im2.save(f, optimize=True)\n\n        reloaded = Image.open(f)\n\n        self.assert_image_similar(im, reloaded.convert('RGB'), 10)\n\n    def test_palette_434(self):\n        # see https://github.com/python-pillow/Pillow/issues/434\n\n        def roundtrip(im, *args, **kwargs):\n            out = self.tempfile('temp.gif')\n            im.copy().save(out, *args, **kwargs)\n            reloaded = Image.open(out)\n\n            return reloaded\n\n        orig = \"Tests/images/test.colors.gif\"\n        im = Image.open(orig)\n\n        self.assert_image_similar(im, roundtrip(im), 1)\n        self.assert_image_similar(im, roundtrip(im, optimize=True), 1)\n\n        im = im.convert(\"RGB\")\n        # check automatic P conversion\n        reloaded = roundtrip(im).convert('RGB')\n        self.assert_image_equal(im, reloaded)\n\n    @unittest.skipUnless(netpbm_available(), \"netpbm not available\")\n    def test_save_netpbm_bmp_mode(self):\n        img = Image.open(TEST_GIF).convert(\"RGB\")\n\n        tempfile = self.tempfile(\"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        self.assert_image_similar(img, Image.open(tempfile).convert(\"RGB\"), 0)\n\n    @unittest.skipUnless(netpbm_available(), \"netpbm not available\")\n    def test_save_netpbm_l_mode(self):\n        img = Image.open(TEST_GIF).convert(\"L\")\n\n        tempfile = self.tempfile(\"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        self.assert_image_similar(img, Image.open(tempfile).convert(\"L\"), 0)\n\n    def test_seek(self):\n        img = Image.open(\"Tests/images/dispose_none.gif\")\n        framecount = 0\n        try:\n            while True:\n                framecount += 1\n                img.seek(img.tell() + 1)\n        except EOFError:\n            self.assertEqual(framecount, 5)\n\n    def test_n_frames(self):\n        for path, n_frames in [\n            [TEST_GIF, 1],\n            ['Tests/images/iss634.gif', 42]\n        ]:\n            # Test is_animated before n_frames\n            im = Image.open(path)\n            self.assertEqual(im.is_animated, n_frames != 1)\n\n            # Test is_animated after n_frames\n            im = Image.open(path)\n            self.assertEqual(im.n_frames, n_frames)\n            self.assertEqual(im.is_animated, n_frames != 1)\n\n    def test_eoferror(self):\n        im = Image.open(TEST_GIF)\n        n_frames = im.n_frames\n\n        # Test seeking past the last frame\n        self.assertRaises(EOFError, im.seek, n_frames)\n        self.assertLess(im.tell(), n_frames)\n\n        # Test that seeking to the last frame does not raise an error\n        im.seek(n_frames-1)\n\n    def test_dispose_none(self):\n        img = Image.open(\"Tests/images/dispose_none.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 1)\n        except EOFError:\n            pass\n\n    def test_dispose_background(self):\n        img = Image.open(\"Tests/images/dispose_bgnd.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 2)\n        except EOFError:\n            pass\n\n    def test_dispose_previous(self):\n        img = Image.open(\"Tests/images/dispose_prev.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 3)\n        except EOFError:\n            pass\n\n    def test_save_dispose(self):\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111'),\n            Image.new('L', (100, 100), '#222'),\n        ]\n        for method in range(0, 4):\n            im_list[0].save(\n                out,\n                save_all=True,\n                append_images=im_list[1:],\n                disposal=method\n            )\n            img = Image.open(out)\n            for _ in range(2):\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, method)\n\n        # check per frame disposal\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            disposal=tuple(range(len(im_list)))\n            )\n\n        img = Image.open(out)\n\n        for i in range(2):\n            img.seek(img.tell() + 1)\n            self.assertEqual(img.disposal_method, i+1)\n\n    def test_iss634(self):\n        img = Image.open(\"Tests/images/iss634.gif\")\n        # seek to the second frame\n        img.seek(img.tell() + 1)\n        # all transparent pixels should be replaced with the color from the\n        # first frame\n        self.assertEqual(img.histogram()[img.info['transparency']], 0)\n\n    def test_duration(self):\n        duration = 1000\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n\n        # Check that the argument has priority over the info settings\n        im.info['duration'] = 100\n        im.save(out, duration=duration)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.info['duration'], duration)\n\n    def test_multiple_duration(self):\n        duration_list = [1000, 2000, 3000]\n\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111'),\n            Image.new('L', (100, 100), '#222')\n        ]\n\n        # duration as list\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=duration_list\n        )\n        reread = Image.open(out)\n\n        for duration in duration_list:\n            self.assertEqual(reread.info['duration'], duration)\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n        # duration as tuple\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=tuple(duration_list)\n        )\n        reread = Image.open(out)\n\n        for duration in duration_list:\n            self.assertEqual(reread.info['duration'], duration)\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n    def test_identical_frames(self):\n        duration_list = [1000, 1500, 2000, 4000]\n\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111')\n        ]\n\n        # duration as list\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=duration_list\n        )\n        reread = Image.open(out)\n\n        # Assert that the first three frames were combined\n        self.assertEqual(reread.n_frames, 2)\n\n        # Assert that the new duration is the total of the identical frames\n        self.assertEqual(reread.info['duration'], 4500)\n\n    def test_number_of_loops(self):\n        number_of_loops = 2\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.save(out, loop=number_of_loops)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['loop'], number_of_loops)\n\n    def test_background(self):\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.info['background'] = 1\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['background'], im.info['background'])\n\n    def test_comment(self):\n        im = Image.open(TEST_GIF)\n        self.assertEqual(im.info['comment'], b\"File written by Adobe Photoshop\\xa8 4.0\")\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.info['comment'] = b\"Test comment text\"\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['comment'], im.info['comment'])\n\n    def test_version(self):\n        out = self.tempfile('temp.gif')\n\n        def assertVersionAfterSave(im, version):\n            im.save(out)\n            reread = Image.open(out)\n            self.assertEqual(reread.info[\"version\"], version)\n\n        # Test that GIF87a is used by default\n        im = Image.new('L', (100, 100), '#000')\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n        # Test setting the version to 89a\n        im = Image.new('L', (100, 100), '#000')\n        im.info[\"version\"] = b\"89a\"\n        assertVersionAfterSave(im, b\"GIF89a\")\n\n        # Test that adding a GIF89a feature changes the version\n        im.info[\"transparency\"] = 1\n        assertVersionAfterSave(im, b\"GIF89a\")\n\n        # Test that a GIF87a image is also saved in that format\n        im = Image.open(\"Tests/images/test.colors.gif\")\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n        # Test that a GIF89a image is also saved in that format\n        im.info[\"version\"] = b\"GIF89a\"\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n    def test_append_images(self):\n        out = self.tempfile('temp.gif')\n\n        # Test appending single frame images\n        im = Image.new('RGB', (100, 100), '#f00')\n        ims = [Image.new('RGB', (100, 100), color) for color\n               in ['#0f0', '#00f']]\n        im.copy().save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 3)\n\n        # Tests appending using a generator\n        def imGenerator(ims):\n            for im in ims:\n                yield im\n        im.save(out, save_all=True, append_images=imGenerator(ims))\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 3)\n\n        # Tests appending single and multiple frame images\n        im = Image.open(\"Tests/images/dispose_none.gif\")\n        ims = [Image.open(\"Tests/images/dispose_prev.gif\")]\n        im.save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 10)\n\n    def test_transparent_optimize(self):\n        # from issue #2195, if the transparent color is incorrectly\n        # optimized out, gif loses transparency\n        # Need a palette that isn't using the 0 color, and one\n        # that's > 128 items where the transparent color is actually\n        # the top palette entry to trigger the bug.\n\n        data = bytes(bytearray(range(1, 254)))\n        palette = ImagePalette.ImagePalette(\"RGB\", list(range(256))*3)\n\n        im = Image.new('L', (253, 1))\n        im.frombytes(data)\n        im.putpalette(palette)\n\n        out = self.tempfile('temp.gif')\n        im.save(out, transparency=253)\n        reloaded = Image.open(out)\n\n        self.assertEqual(reloaded.info['transparency'], 253)\n\n    def test_bbox(self):\n        out = self.tempfile('temp.gif')\n\n        im = Image.new('RGB', (100, 100), '#fff')\n        ims = [Image.new(\"RGB\", (100, 100), '#000')]\n        im.save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 2)\n\n    def test_palette_save_L(self):\n        # generate an L mode image with a separate palette\n\n        im = hopper('P')\n        im_l = Image.frombytes('L', im.size, im.tobytes())\n        palette = bytes(bytearray(im.getpalette()))\n\n        out = self.tempfile('temp.gif')\n        im_l.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n\n        self.assert_image_equal(reloaded.convert('RGB'), im.convert('RGB'))\n\n    def test_palette_save_P(self):\n        # pass in a different palette, then construct what the image\n        # would look like.\n        # Forcing a non-straight grayscale palette.\n\n        im = hopper('P')\n        palette = bytes(bytearray([255-i//3 for i in range(768)]))\n\n        out = self.tempfile('temp.gif')\n        im.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n        im.putpalette(palette)\n        self.assert_image_equal(reloaded, im)\n\n    def test_palette_save_ImagePalette(self):\n        # pass in a different palette, as an ImagePalette.ImagePalette\n        # effectively the same as test_palette_save_P\n\n        im = hopper('P')\n        palette = ImagePalette.ImagePalette('RGB', list(range(256))[::-1]*3)\n\n        out = self.tempfile('temp.gif')\n        im.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n        im.putpalette(palette)\n        self.assert_image_equal(reloaded, im)\n\n    def test_save_I(self):\n        # Test saving something that would trigger the auto-convert to 'L'\n\n        im = hopper('I')\n\n        out = self.tempfile('temp.gif')\n        im.save(out)\n\n        reloaded = Image.open(out)\n        self.assert_image_equal(reloaded.convert('L'), im.convert('L'))\n\n    def test_getdata(self):\n        # test getheader/getdata against legacy values\n        # Create a 'P' image with holes in the palette\n        im = Image._wedge().resize((16, 16))\n        im.putpalette(ImagePalette.ImagePalette('RGB'))\n        im.info = {'background': 0}\n\n        passed_palette = bytes(bytearray([255-i//3 for i in range(768)]))\n\n        GifImagePlugin._FORCE_OPTIMIZE = True\n        try:\n            h = GifImagePlugin.getheader(im, passed_palette)\n            d = GifImagePlugin.getdata(im)\n\n            import pickle\n            # Enable to get target values on pre-refactor version\n            # with open('Tests/images/gif_header_data.pkl', 'wb') as f:\n            #    pickle.dump((h, d), f, 1)\n            with open('Tests/images/gif_header_data.pkl', 'rb') as f:\n                (h_target, d_target) = pickle.load(f)\n\n            self.assertEqual(h, h_target)\n            self.assertEqual(d, d_target)\n        finally:\n            GifImagePlugin._FORCE_OPTIMIZE = False\n\n    def test_lzw_bits(self):\n        # see https://github.com/python-pillow/Pillow/issues/2811\n        im = Image.open('Tests/images/issue_2811.gif')\n\n        self.assertEqual(im.tile[0][3][0], 11)  # LZW bits\n        # codec error prepatch\n        im.load()\n\n\nif __name__ == '__main__':\n    unittest.main()\n",
            "header": {
                "file": {
                    "path": "Tests/test_file_gif.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "c8f3836fc",
                "new_commit_hash": "b1006a630",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Tests/test_file_gif.py"
            },
            "plus_file": {
                "path": "b/Tests/test_file_gif.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 305,
                            "length": 9
                        },
                        "new_range": {
                            "start": 305,
                            "length": 12
                        },
                        "section": "class TestFileGif(PillowTestCase):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        out = self.tempfile('temp.gif')",
                                "type": "context"
                            },
                            {
                                "content": "        im = Image.new('L', (100, 100), '#000')",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        # Check that the argument has priority over the info settings",
                                "type": "added"
                            },
                            {
                                "content": "        im.info['duration'] = 100",
                                "type": "added"
                            },
                            {
                                "content": "        im.save(out, duration=duration)",
                                "type": "context"
                            },
                            {
                                "content": "        reread = Image.open(out)",
                                "type": "deleted"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        reread = Image.open(out)",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertEqual(reread.info['duration'], duration)",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def test_multiple_duration(self):",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Tests/test_file_gif.py",
                            "type": "class",
                            "name": "TestFileGif",
                            "content": "class TestFileGif(PillowTestCase):\n\n    def setUp(self):\n        if \"gif_encoder\" not in codecs or \"gif_decoder\" not in codecs:\n            self.skipTest(\"gif support not available\")  # can this happen?\n\n    def test_sanity(self):\n        im = Image.open(TEST_GIF)\n        im.load()\n        self.assertEqual(im.mode, \"P\")\n        self.assertEqual(im.size, (128, 128))\n        self.assertEqual(im.format, \"GIF\")\n        self.assertEqual(im.info[\"version\"], b\"GIF89a\")\n\n    def test_invalid_file(self):\n        invalid_file = \"Tests/images/flower.jpg\"\n\n        self.assertRaises(SyntaxError,\n                          GifImagePlugin.GifImageFile, invalid_file)\n\n    def test_optimize(self):\n        def test_grayscale(optimize):\n            im = Image.new(\"L\", (1, 1), 0)\n            filename = BytesIO()\n            im.save(filename, \"GIF\", optimize=optimize)\n            return len(filename.getvalue())\n\n        def test_bilevel(optimize):\n            im = Image.new(\"1\", (1, 1), 0)\n            test_file = BytesIO()\n            im.save(test_file, \"GIF\", optimize=optimize)\n            return len(test_file.getvalue())\n\n        self.assertEqual(test_grayscale(0), 800)\n        self.assertEqual(test_grayscale(1), 38)\n        self.assertEqual(test_bilevel(0), 800)\n        self.assertEqual(test_bilevel(1), 800)\n\n    def test_optimize_correctness(self):\n        # 256 color Palette image, posterize to > 128 and < 128 levels\n        # Size bigger and smaller than 512x512\n        # Check the palette for number of colors allocated.\n        # Check for correctness after conversion back to RGB\n        def check(colors, size, expected_palette_length):\n            # make an image with empty colors in the start of the palette range\n            im = Image.frombytes('P', (colors, colors),\n                                 bytes(bytearray(range(256-colors, 256))*colors))\n            im = im.resize((size, size))\n            outfile = BytesIO()\n            im.save(outfile, 'GIF')\n            outfile.seek(0)\n            reloaded = Image.open(outfile)\n\n            # check palette length\n            palette_length = max(i+1 for i, v in enumerate(reloaded.histogram()) if v)\n            self.assertEqual(expected_palette_length, palette_length)\n\n            self.assert_image_equal(im.convert('RGB'), reloaded.convert('RGB'))\n\n        # These do optimize the palette\n        check(128, 511, 128)\n        check(64, 511, 64)\n        check(4, 511, 4)\n\n        # These don't optimize the palette\n        check(128, 513, 256)\n        check(64, 513, 256)\n        check(4, 513, 256)\n\n        # other limits that don't optimize the palette\n        check(129, 511, 256)\n        check(255, 511, 256)\n        check(256, 511, 256)\n\n    def test_optimize_full_l(self):\n        im = Image.frombytes(\"L\", (16, 16), bytes(bytearray(range(256))))\n        test_file = BytesIO()\n        im.save(test_file, \"GIF\", optimize=True)\n        self.assertEqual(im.mode, \"L\")\n\n    def test_roundtrip(self):\n        out = self.tempfile('temp.gif')\n        im = hopper()\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), im, 50)\n\n    def test_roundtrip2(self):\n        # see https://github.com/python-pillow/Pillow/issues/403\n        out = self.tempfile('temp.gif')\n        im = Image.open(TEST_GIF)\n        im2 = im.copy()\n        im2.save(out)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), hopper(), 50)\n\n    def test_roundtrip_save_all(self):\n        # Single frame image\n        out = self.tempfile('temp.gif')\n        im = hopper()\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), im, 50)\n\n        # Multiframe image\n        im = Image.open(\"Tests/images/dispose_bgnd.gif\")\n\n        out = self.tempfile('temp.gif')\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.n_frames, 5)\n\n    def test_headers_saving_for_animated_gifs(self):\n        important_headers = ['background', 'version', 'duration', 'loop']\n        # Multiframe image\n        im = Image.open(\"Tests/images/dispose_bgnd.gif\")\n\n        out = self.tempfile('temp.gif')\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        for header in important_headers:\n            self.assertEqual(\n                im.info[header],\n                reread.info[header]\n            )\n\n    def test_palette_handling(self):\n        # see https://github.com/python-pillow/Pillow/issues/513\n\n        im = Image.open(TEST_GIF)\n        im = im.convert('RGB')\n\n        im = im.resize((100, 100), Image.LANCZOS)\n        im2 = im.convert('P', palette=Image.ADAPTIVE, colors=256)\n\n        f = self.tempfile('temp.gif')\n        im2.save(f, optimize=True)\n\n        reloaded = Image.open(f)\n\n        self.assert_image_similar(im, reloaded.convert('RGB'), 10)\n\n    def test_palette_434(self):\n        # see https://github.com/python-pillow/Pillow/issues/434\n\n        def roundtrip(im, *args, **kwargs):\n            out = self.tempfile('temp.gif')\n            im.copy().save(out, *args, **kwargs)\n            reloaded = Image.open(out)\n\n            return reloaded\n\n        orig = \"Tests/images/test.colors.gif\"\n        im = Image.open(orig)\n\n        self.assert_image_similar(im, roundtrip(im), 1)\n        self.assert_image_similar(im, roundtrip(im, optimize=True), 1)\n\n        im = im.convert(\"RGB\")\n        # check automatic P conversion\n        reloaded = roundtrip(im).convert('RGB')\n        self.assert_image_equal(im, reloaded)\n\n    @unittest.skipUnless(netpbm_available(), \"netpbm not available\")\n    def test_save_netpbm_bmp_mode(self):\n        img = Image.open(TEST_GIF).convert(\"RGB\")\n\n        tempfile = self.tempfile(\"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        self.assert_image_similar(img, Image.open(tempfile).convert(\"RGB\"), 0)\n\n    @unittest.skipUnless(netpbm_available(), \"netpbm not available\")\n    def test_save_netpbm_l_mode(self):\n        img = Image.open(TEST_GIF).convert(\"L\")\n\n        tempfile = self.tempfile(\"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        self.assert_image_similar(img, Image.open(tempfile).convert(\"L\"), 0)\n\n    def test_seek(self):\n        img = Image.open(\"Tests/images/dispose_none.gif\")\n        framecount = 0\n        try:\n            while True:\n                framecount += 1\n                img.seek(img.tell() + 1)\n        except EOFError:\n            self.assertEqual(framecount, 5)\n\n    def test_n_frames(self):\n        for path, n_frames in [\n            [TEST_GIF, 1],\n            ['Tests/images/iss634.gif', 42]\n        ]:\n            # Test is_animated before n_frames\n            im = Image.open(path)\n            self.assertEqual(im.is_animated, n_frames != 1)\n\n            # Test is_animated after n_frames\n            im = Image.open(path)\n            self.assertEqual(im.n_frames, n_frames)\n            self.assertEqual(im.is_animated, n_frames != 1)\n\n    def test_eoferror(self):\n        im = Image.open(TEST_GIF)\n        n_frames = im.n_frames\n\n        # Test seeking past the last frame\n        self.assertRaises(EOFError, im.seek, n_frames)\n        self.assertLess(im.tell(), n_frames)\n\n        # Test that seeking to the last frame does not raise an error\n        im.seek(n_frames-1)\n\n    def test_dispose_none(self):\n        img = Image.open(\"Tests/images/dispose_none.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 1)\n        except EOFError:\n            pass\n\n    def test_dispose_background(self):\n        img = Image.open(\"Tests/images/dispose_bgnd.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 2)\n        except EOFError:\n            pass\n\n    def test_dispose_previous(self):\n        img = Image.open(\"Tests/images/dispose_prev.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 3)\n        except EOFError:\n            pass\n\n    def test_save_dispose(self):\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111'),\n            Image.new('L', (100, 100), '#222'),\n        ]\n        for method in range(0, 4):\n            im_list[0].save(\n                out,\n                save_all=True,\n                append_images=im_list[1:],\n                disposal=method\n            )\n            img = Image.open(out)\n            for _ in range(2):\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, method)\n\n        # check per frame disposal\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            disposal=tuple(range(len(im_list)))\n            )\n\n        img = Image.open(out)\n\n        for i in range(2):\n            img.seek(img.tell() + 1)\n            self.assertEqual(img.disposal_method, i+1)\n\n    def test_iss634(self):\n        img = Image.open(\"Tests/images/iss634.gif\")\n        # seek to the second frame\n        img.seek(img.tell() + 1)\n        # all transparent pixels should be replaced with the color from the\n        # first frame\n        self.assertEqual(img.histogram()[img.info['transparency']], 0)\n\n    def test_duration(self):\n        duration = 1000\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.save(out, duration=duration)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['duration'], duration)\n\n    def test_multiple_duration(self):\n        duration_list = [1000, 2000, 3000]\n\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111'),\n            Image.new('L', (100, 100), '#222')\n        ]\n\n        # duration as list\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=duration_list\n        )\n        reread = Image.open(out)\n\n        for duration in duration_list:\n            self.assertEqual(reread.info['duration'], duration)\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n        # duration as tuple\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=tuple(duration_list)\n        )\n        reread = Image.open(out)\n\n        for duration in duration_list:\n            self.assertEqual(reread.info['duration'], duration)\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n    def test_identical_frames(self):\n        duration_list = [1000, 1500, 2000, 4000]\n\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111')\n        ]\n\n        # duration as list\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=duration_list\n        )\n        reread = Image.open(out)\n\n        # Assert that the first three frames were combined\n        self.assertEqual(reread.n_frames, 2)\n\n        # Assert that the new duration is the total of the identical frames\n        self.assertEqual(reread.info['duration'], 4500)\n\n    def test_number_of_loops(self):\n        number_of_loops = 2\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.save(out, loop=number_of_loops)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['loop'], number_of_loops)\n\n    def test_background(self):\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.info['background'] = 1\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['background'], im.info['background'])\n\n    def test_comment(self):\n        im = Image.open(TEST_GIF)\n        self.assertEqual(im.info['comment'], b\"File written by Adobe Photoshop\\xa8 4.0\")\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.info['comment'] = b\"Test comment text\"\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['comment'], im.info['comment'])\n\n    def test_version(self):\n        out = self.tempfile('temp.gif')\n\n        def assertVersionAfterSave(im, version):\n            im.save(out)\n            reread = Image.open(out)\n            self.assertEqual(reread.info[\"version\"], version)\n\n        # Test that GIF87a is used by default\n        im = Image.new('L', (100, 100), '#000')\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n        # Test setting the version to 89a\n        im = Image.new('L', (100, 100), '#000')\n        im.info[\"version\"] = b\"89a\"\n        assertVersionAfterSave(im, b\"GIF89a\")\n\n        # Test that adding a GIF89a feature changes the version\n        im.info[\"transparency\"] = 1\n        assertVersionAfterSave(im, b\"GIF89a\")\n\n        # Test that a GIF87a image is also saved in that format\n        im = Image.open(\"Tests/images/test.colors.gif\")\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n        # Test that a GIF89a image is also saved in that format\n        im.info[\"version\"] = b\"GIF89a\"\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n    def test_append_images(self):\n        out = self.tempfile('temp.gif')\n\n        # Test appending single frame images\n        im = Image.new('RGB', (100, 100), '#f00')\n        ims = [Image.new('RGB', (100, 100), color) for color\n               in ['#0f0', '#00f']]\n        im.copy().save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 3)\n\n        # Tests appending using a generator\n        def imGenerator(ims):\n            for im in ims:\n                yield im\n        im.save(out, save_all=True, append_images=imGenerator(ims))\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 3)\n\n        # Tests appending single and multiple frame images\n        im = Image.open(\"Tests/images/dispose_none.gif\")\n        ims = [Image.open(\"Tests/images/dispose_prev.gif\")]\n        im.save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 10)\n\n    def test_transparent_optimize(self):\n        # from issue #2195, if the transparent color is incorrectly\n        # optimized out, gif loses transparency\n        # Need a palette that isn't using the 0 color, and one\n        # that's > 128 items where the transparent color is actually\n        # the top palette entry to trigger the bug.\n\n        data = bytes(bytearray(range(1, 254)))\n        palette = ImagePalette.ImagePalette(\"RGB\", list(range(256))*3)\n\n        im = Image.new('L', (253, 1))\n        im.frombytes(data)\n        im.putpalette(palette)\n\n        out = self.tempfile('temp.gif')\n        im.save(out, transparency=253)\n        reloaded = Image.open(out)\n\n        self.assertEqual(reloaded.info['transparency'], 253)\n\n    def test_bbox(self):\n        out = self.tempfile('temp.gif')\n\n        im = Image.new('RGB', (100, 100), '#fff')\n        ims = [Image.new(\"RGB\", (100, 100), '#000')]\n        im.save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 2)\n\n    def test_palette_save_L(self):\n        # generate an L mode image with a separate palette\n\n        im = hopper('P')\n        im_l = Image.frombytes('L', im.size, im.tobytes())\n        palette = bytes(bytearray(im.getpalette()))\n\n        out = self.tempfile('temp.gif')\n        im_l.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n\n        self.assert_image_equal(reloaded.convert('RGB'), im.convert('RGB'))\n\n    def test_palette_save_P(self):\n        # pass in a different palette, then construct what the image\n        # would look like.\n        # Forcing a non-straight grayscale palette.\n\n        im = hopper('P')\n        palette = bytes(bytearray([255-i//3 for i in range(768)]))\n\n        out = self.tempfile('temp.gif')\n        im.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n        im.putpalette(palette)\n        self.assert_image_equal(reloaded, im)\n\n    def test_palette_save_ImagePalette(self):\n        # pass in a different palette, as an ImagePalette.ImagePalette\n        # effectively the same as test_palette_save_P\n\n        im = hopper('P')\n        palette = ImagePalette.ImagePalette('RGB', list(range(256))[::-1]*3)\n\n        out = self.tempfile('temp.gif')\n        im.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n        im.putpalette(palette)\n        self.assert_image_equal(reloaded, im)\n\n    def test_save_I(self):\n        # Test saving something that would trigger the auto-convert to 'L'\n\n        im = hopper('I')\n\n        out = self.tempfile('temp.gif')\n        im.save(out)\n\n        reloaded = Image.open(out)\n        self.assert_image_equal(reloaded.convert('L'), im.convert('L'))\n\n    def test_getdata(self):\n        # test getheader/getdata against legacy values\n        # Create a 'P' image with holes in the palette\n        im = Image._wedge().resize((16, 16))\n        im.putpalette(ImagePalette.ImagePalette('RGB'))\n        im.info = {'background': 0}\n\n        passed_palette = bytes(bytearray([255-i//3 for i in range(768)]))\n\n        GifImagePlugin._FORCE_OPTIMIZE = True\n        try:\n            h = GifImagePlugin.getheader(im, passed_palette)\n            d = GifImagePlugin.getdata(im)\n\n            import pickle\n            # Enable to get target values on pre-refactor version\n            # with open('Tests/images/gif_header_data.pkl', 'wb') as f:\n            #    pickle.dump((h, d), f, 1)\n            with open('Tests/images/gif_header_data.pkl', 'rb') as f:\n                (h_target, d_target) = pickle.load(f)\n\n            self.assertEqual(h, h_target)\n            self.assertEqual(d, d_target)\n        finally:\n            GifImagePlugin._FORCE_OPTIMIZE = False\n\n    def test_lzw_bits(self):\n        # see https://github.com/python-pillow/Pillow/issues/2811\n        im = Image.open('Tests/images/issue_2811.gif')\n\n        self.assertEqual(im.tile[0][3][0], 11)  # LZW bits\n        # codec error prepatch\n        im.load()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 16,
                            "end_lineno": 586,
                            "parent": null
                        },
                        {
                            "file_name": "Tests/test_file_gif.py",
                            "type": "method",
                            "name": "TestFileGif.test_duration",
                            "content": "class TestFileGif(PillowTestCase):\n\n    def setUp(self):\n        if \"gif_encoder\" not in codecs or \"gif_decoder\" not in codecs:\n            self.skipTest(\"gif support not available\")  # can this happen?\n\n    def test_sanity(self):\n        im = Image.open(TEST_GIF)\n        im.load()\n        self.assertEqual(im.mode, \"P\")\n        self.assertEqual(im.size, (128, 128))\n        self.assertEqual(im.format, \"GIF\")\n        self.assertEqual(im.info[\"version\"], b\"GIF89a\")\n\n    def test_invalid_file(self):\n        invalid_file = \"Tests/images/flower.jpg\"\n\n        self.assertRaises(SyntaxError,\n                          GifImagePlugin.GifImageFile, invalid_file)\n\n    def test_optimize(self):\n        def test_grayscale(optimize):\n            im = Image.new(\"L\", (1, 1), 0)\n            filename = BytesIO()\n            im.save(filename, \"GIF\", optimize=optimize)\n            return len(filename.getvalue())\n\n        def test_bilevel(optimize):\n            im = Image.new(\"1\", (1, 1), 0)\n            test_file = BytesIO()\n            im.save(test_file, \"GIF\", optimize=optimize)\n            return len(test_file.getvalue())\n\n        self.assertEqual(test_grayscale(0), 800)\n        self.assertEqual(test_grayscale(1), 38)\n        self.assertEqual(test_bilevel(0), 800)\n        self.assertEqual(test_bilevel(1), 800)\n\n    def test_optimize_correctness(self):\n        # 256 color Palette image, posterize to > 128 and < 128 levels\n        # Size bigger and smaller than 512x512\n        # Check the palette for number of colors allocated.\n        # Check for correctness after conversion back to RGB\n        def check(colors, size, expected_palette_length):\n            # make an image with empty colors in the start of the palette range\n            im = Image.frombytes('P', (colors, colors),\n                                 bytes(bytearray(range(256-colors, 256))*colors))\n            im = im.resize((size, size))\n            outfile = BytesIO()\n            im.save(outfile, 'GIF')\n            outfile.seek(0)\n            reloaded = Image.open(outfile)\n\n            # check palette length\n            palette_length = max(i+1 for i, v in enumerate(reloaded.histogram()) if v)\n            self.assertEqual(expected_palette_length, palette_length)\n\n            self.assert_image_equal(im.convert('RGB'), reloaded.convert('RGB'))\n\n        # These do optimize the palette\n        check(128, 511, 128)\n        check(64, 511, 64)\n        check(4, 511, 4)\n\n        # These don't optimize the palette\n        check(128, 513, 256)\n        check(64, 513, 256)\n        check(4, 513, 256)\n\n        # other limits that don't optimize the palette\n        check(129, 511, 256)\n        check(255, 511, 256)\n        check(256, 511, 256)\n\n    def test_optimize_full_l(self):\n        im = Image.frombytes(\"L\", (16, 16), bytes(bytearray(range(256))))\n        test_file = BytesIO()\n        im.save(test_file, \"GIF\", optimize=True)\n        self.assertEqual(im.mode, \"L\")\n\n    def test_roundtrip(self):\n        out = self.tempfile('temp.gif')\n        im = hopper()\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), im, 50)\n\n    def test_roundtrip2(self):\n        # see https://github.com/python-pillow/Pillow/issues/403\n        out = self.tempfile('temp.gif')\n        im = Image.open(TEST_GIF)\n        im2 = im.copy()\n        im2.save(out)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), hopper(), 50)\n\n    def test_roundtrip_save_all(self):\n        # Single frame image\n        out = self.tempfile('temp.gif')\n        im = hopper()\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), im, 50)\n\n        # Multiframe image\n        im = Image.open(\"Tests/images/dispose_bgnd.gif\")\n\n        out = self.tempfile('temp.gif')\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.n_frames, 5)\n\n    def test_headers_saving_for_animated_gifs(self):\n        important_headers = ['background', 'version', 'duration', 'loop']\n        # Multiframe image\n        im = Image.open(\"Tests/images/dispose_bgnd.gif\")\n\n        out = self.tempfile('temp.gif')\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        for header in important_headers:\n            self.assertEqual(\n                im.info[header],\n                reread.info[header]\n            )\n\n    def test_palette_handling(self):\n        # see https://github.com/python-pillow/Pillow/issues/513\n\n        im = Image.open(TEST_GIF)\n        im = im.convert('RGB')\n\n        im = im.resize((100, 100), Image.LANCZOS)\n        im2 = im.convert('P', palette=Image.ADAPTIVE, colors=256)\n\n        f = self.tempfile('temp.gif')\n        im2.save(f, optimize=True)\n\n        reloaded = Image.open(f)\n\n        self.assert_image_similar(im, reloaded.convert('RGB'), 10)\n\n    def test_palette_434(self):\n        # see https://github.com/python-pillow/Pillow/issues/434\n\n        def roundtrip(im, *args, **kwargs):\n            out = self.tempfile('temp.gif')\n            im.copy().save(out, *args, **kwargs)\n            reloaded = Image.open(out)\n\n            return reloaded\n\n        orig = \"Tests/images/test.colors.gif\"\n        im = Image.open(orig)\n\n        self.assert_image_similar(im, roundtrip(im), 1)\n        self.assert_image_similar(im, roundtrip(im, optimize=True), 1)\n\n        im = im.convert(\"RGB\")\n        # check automatic P conversion\n        reloaded = roundtrip(im).convert('RGB')\n        self.assert_image_equal(im, reloaded)\n\n    @unittest.skipUnless(netpbm_available(), \"netpbm not available\")\n    def test_save_netpbm_bmp_mode(self):\n        img = Image.open(TEST_GIF).convert(\"RGB\")\n\n        tempfile = self.tempfile(\"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        self.assert_image_similar(img, Image.open(tempfile).convert(\"RGB\"), 0)\n\n    @unittest.skipUnless(netpbm_available(), \"netpbm not available\")\n    def test_save_netpbm_l_mode(self):\n        img = Image.open(TEST_GIF).convert(\"L\")\n\n        tempfile = self.tempfile(\"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        self.assert_image_similar(img, Image.open(tempfile).convert(\"L\"), 0)\n\n    def test_seek(self):\n        img = Image.open(\"Tests/images/dispose_none.gif\")\n        framecount = 0\n        try:\n            while True:\n                framecount += 1\n                img.seek(img.tell() + 1)\n        except EOFError:\n            self.assertEqual(framecount, 5)\n\n    def test_n_frames(self):\n        for path, n_frames in [\n            [TEST_GIF, 1],\n            ['Tests/images/iss634.gif', 42]\n        ]:\n            # Test is_animated before n_frames\n            im = Image.open(path)\n            self.assertEqual(im.is_animated, n_frames != 1)\n\n            # Test is_animated after n_frames\n            im = Image.open(path)\n            self.assertEqual(im.n_frames, n_frames)\n            self.assertEqual(im.is_animated, n_frames != 1)\n\n    def test_eoferror(self):\n        im = Image.open(TEST_GIF)\n        n_frames = im.n_frames\n\n        # Test seeking past the last frame\n        self.assertRaises(EOFError, im.seek, n_frames)\n        self.assertLess(im.tell(), n_frames)\n\n        # Test that seeking to the last frame does not raise an error\n        im.seek(n_frames-1)\n\n    def test_dispose_none(self):\n        img = Image.open(\"Tests/images/dispose_none.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 1)\n        except EOFError:\n            pass\n\n    def test_dispose_background(self):\n        img = Image.open(\"Tests/images/dispose_bgnd.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 2)\n        except EOFError:\n            pass\n\n    def test_dispose_previous(self):\n        img = Image.open(\"Tests/images/dispose_prev.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 3)\n        except EOFError:\n            pass\n\n    def test_save_dispose(self):\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111'),\n            Image.new('L', (100, 100), '#222'),\n        ]\n        for method in range(0, 4):\n            im_list[0].save(\n                out,\n                save_all=True,\n                append_images=im_list[1:],\n                disposal=method\n            )\n            img = Image.open(out)\n            for _ in range(2):\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, method)\n\n        # check per frame disposal\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            disposal=tuple(range(len(im_list)))\n            )\n\n        img = Image.open(out)\n\n        for i in range(2):\n            img.seek(img.tell() + 1)\n            self.assertEqual(img.disposal_method, i+1)\n\n    def test_iss634(self):\n        img = Image.open(\"Tests/images/iss634.gif\")\n        # seek to the second frame\n        img.seek(img.tell() + 1)\n        # all transparent pixels should be replaced with the color from the\n        # first frame\n        self.assertEqual(img.histogram()[img.info['transparency']], 0)\n\n    def test_duration(self):\n        duration = 1000\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.save(out, duration=duration)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['duration'], duration)\n\n    def test_multiple_duration(self):\n        duration_list = [1000, 2000, 3000]\n\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111'),\n            Image.new('L', (100, 100), '#222')\n        ]\n\n        # duration as list\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=duration_list\n        )\n        reread = Image.open(out)\n\n        for duration in duration_list:\n            self.assertEqual(reread.info['duration'], duration)\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n        # duration as tuple\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=tuple(duration_list)\n        )\n        reread = Image.open(out)\n\n        for duration in duration_list:\n            self.assertEqual(reread.info['duration'], duration)\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n    def test_identical_frames(self):\n        duration_list = [1000, 1500, 2000, 4000]\n\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111')\n        ]\n\n        # duration as list\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=duration_list\n        )\n        reread = Image.open(out)\n\n        # Assert that the first three frames were combined\n        self.assertEqual(reread.n_frames, 2)\n\n        # Assert that the new duration is the total of the identical frames\n        self.assertEqual(reread.info['duration'], 4500)\n\n    def test_number_of_loops(self):\n        number_of_loops = 2\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.save(out, loop=number_of_loops)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['loop'], number_of_loops)\n\n    def test_background(self):\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.info['background'] = 1\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['background'], im.info['background'])\n\n    def test_comment(self):\n        im = Image.open(TEST_GIF)\n        self.assertEqual(im.info['comment'], b\"File written by Adobe Photoshop\\xa8 4.0\")\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.info['comment'] = b\"Test comment text\"\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['comment'], im.info['comment'])\n\n    def test_version(self):\n        out = self.tempfile('temp.gif')\n\n        def assertVersionAfterSave(im, version):\n            im.save(out)\n            reread = Image.open(out)\n            self.assertEqual(reread.info[\"version\"], version)\n\n        # Test that GIF87a is used by default\n        im = Image.new('L', (100, 100), '#000')\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n        # Test setting the version to 89a\n        im = Image.new('L', (100, 100), '#000')\n        im.info[\"version\"] = b\"89a\"\n        assertVersionAfterSave(im, b\"GIF89a\")\n\n        # Test that adding a GIF89a feature changes the version\n        im.info[\"transparency\"] = 1\n        assertVersionAfterSave(im, b\"GIF89a\")\n\n        # Test that a GIF87a image is also saved in that format\n        im = Image.open(\"Tests/images/test.colors.gif\")\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n        # Test that a GIF89a image is also saved in that format\n        im.info[\"version\"] = b\"GIF89a\"\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n    def test_append_images(self):\n        out = self.tempfile('temp.gif')\n\n        # Test appending single frame images\n        im = Image.new('RGB', (100, 100), '#f00')\n        ims = [Image.new('RGB', (100, 100), color) for color\n               in ['#0f0', '#00f']]\n        im.copy().save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 3)\n\n        # Tests appending using a generator\n        def imGenerator(ims):\n            for im in ims:\n                yield im\n        im.save(out, save_all=True, append_images=imGenerator(ims))\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 3)\n\n        # Tests appending single and multiple frame images\n        im = Image.open(\"Tests/images/dispose_none.gif\")\n        ims = [Image.open(\"Tests/images/dispose_prev.gif\")]\n        im.save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 10)\n\n    def test_transparent_optimize(self):\n        # from issue #2195, if the transparent color is incorrectly\n        # optimized out, gif loses transparency\n        # Need a palette that isn't using the 0 color, and one\n        # that's > 128 items where the transparent color is actually\n        # the top palette entry to trigger the bug.\n\n        data = bytes(bytearray(range(1, 254)))\n        palette = ImagePalette.ImagePalette(\"RGB\", list(range(256))*3)\n\n        im = Image.new('L', (253, 1))\n        im.frombytes(data)\n        im.putpalette(palette)\n\n        out = self.tempfile('temp.gif')\n        im.save(out, transparency=253)\n        reloaded = Image.open(out)\n\n        self.assertEqual(reloaded.info['transparency'], 253)\n\n    def test_bbox(self):\n        out = self.tempfile('temp.gif')\n\n        im = Image.new('RGB', (100, 100), '#fff')\n        ims = [Image.new(\"RGB\", (100, 100), '#000')]\n        im.save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 2)\n\n    def test_palette_save_L(self):\n        # generate an L mode image with a separate palette\n\n        im = hopper('P')\n        im_l = Image.frombytes('L', im.size, im.tobytes())\n        palette = bytes(bytearray(im.getpalette()))\n\n        out = self.tempfile('temp.gif')\n        im_l.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n\n        self.assert_image_equal(reloaded.convert('RGB'), im.convert('RGB'))\n\n    def test_palette_save_P(self):\n        # pass in a different palette, then construct what the image\n        # would look like.\n        # Forcing a non-straight grayscale palette.\n\n        im = hopper('P')\n        palette = bytes(bytearray([255-i//3 for i in range(768)]))\n\n        out = self.tempfile('temp.gif')\n        im.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n        im.putpalette(palette)\n        self.assert_image_equal(reloaded, im)\n\n    def test_palette_save_ImagePalette(self):\n        # pass in a different palette, as an ImagePalette.ImagePalette\n        # effectively the same as test_palette_save_P\n\n        im = hopper('P')\n        palette = ImagePalette.ImagePalette('RGB', list(range(256))[::-1]*3)\n\n        out = self.tempfile('temp.gif')\n        im.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n        im.putpalette(palette)\n        self.assert_image_equal(reloaded, im)\n\n    def test_save_I(self):\n        # Test saving something that would trigger the auto-convert to 'L'\n\n        im = hopper('I')\n\n        out = self.tempfile('temp.gif')\n        im.save(out)\n\n        reloaded = Image.open(out)\n        self.assert_image_equal(reloaded.convert('L'), im.convert('L'))\n\n    def test_getdata(self):\n        # test getheader/getdata against legacy values\n        # Create a 'P' image with holes in the palette\n        im = Image._wedge().resize((16, 16))\n        im.putpalette(ImagePalette.ImagePalette('RGB'))\n        im.info = {'background': 0}\n\n        passed_palette = bytes(bytearray([255-i//3 for i in range(768)]))\n\n        GifImagePlugin._FORCE_OPTIMIZE = True\n        try:\n            h = GifImagePlugin.getheader(im, passed_palette)\n            d = GifImagePlugin.getdata(im)\n\n            import pickle\n            # Enable to get target values on pre-refactor version\n            # with open('Tests/images/gif_header_data.pkl', 'wb') as f:\n            #    pickle.dump((h, d), f, 1)\n            with open('Tests/images/gif_header_data.pkl', 'rb') as f:\n                (h_target, d_target) = pickle.load(f)\n\n            self.assertEqual(h, h_target)\n            self.assertEqual(d, d_target)\n        finally:\n            GifImagePlugin._FORCE_OPTIMIZE = False\n\n    def test_lzw_bits(self):\n        # see https://github.com/python-pillow/Pillow/issues/2811\n        im = Image.open('Tests/images/issue_2811.gif')\n\n        self.assertEqual(im.tile[0][3][0], 11)  # LZW bits\n        # codec error prepatch\n        im.load()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 303,
                            "end_lineno": 311,
                            "parent": {
                                "file_name": "Tests/test_file_gif.py",
                                "type": "class",
                                "name": "TestFileGif",
                                "content": "class TestFileGif(PillowTestCase):\n\n    def setUp(self):\n        if \"gif_encoder\" not in codecs or \"gif_decoder\" not in codecs:\n            self.skipTest(\"gif support not available\")  # can this happen?\n\n    def test_sanity(self):\n        im = Image.open(TEST_GIF)\n        im.load()\n        self.assertEqual(im.mode, \"P\")\n        self.assertEqual(im.size, (128, 128))\n        self.assertEqual(im.format, \"GIF\")\n        self.assertEqual(im.info[\"version\"], b\"GIF89a\")\n\n    def test_invalid_file(self):\n        invalid_file = \"Tests/images/flower.jpg\"\n\n        self.assertRaises(SyntaxError,\n                          GifImagePlugin.GifImageFile, invalid_file)\n\n    def test_optimize(self):\n        def test_grayscale(optimize):\n            im = Image.new(\"L\", (1, 1), 0)\n            filename = BytesIO()\n            im.save(filename, \"GIF\", optimize=optimize)\n            return len(filename.getvalue())\n\n        def test_bilevel(optimize):\n            im = Image.new(\"1\", (1, 1), 0)\n            test_file = BytesIO()\n            im.save(test_file, \"GIF\", optimize=optimize)\n            return len(test_file.getvalue())\n\n        self.assertEqual(test_grayscale(0), 800)\n        self.assertEqual(test_grayscale(1), 38)\n        self.assertEqual(test_bilevel(0), 800)\n        self.assertEqual(test_bilevel(1), 800)\n\n    def test_optimize_correctness(self):\n        # 256 color Palette image, posterize to > 128 and < 128 levels\n        # Size bigger and smaller than 512x512\n        # Check the palette for number of colors allocated.\n        # Check for correctness after conversion back to RGB\n        def check(colors, size, expected_palette_length):\n            # make an image with empty colors in the start of the palette range\n            im = Image.frombytes('P', (colors, colors),\n                                 bytes(bytearray(range(256-colors, 256))*colors))\n            im = im.resize((size, size))\n            outfile = BytesIO()\n            im.save(outfile, 'GIF')\n            outfile.seek(0)\n            reloaded = Image.open(outfile)\n\n            # check palette length\n            palette_length = max(i+1 for i, v in enumerate(reloaded.histogram()) if v)\n            self.assertEqual(expected_palette_length, palette_length)\n\n            self.assert_image_equal(im.convert('RGB'), reloaded.convert('RGB'))\n\n        # These do optimize the palette\n        check(128, 511, 128)\n        check(64, 511, 64)\n        check(4, 511, 4)\n\n        # These don't optimize the palette\n        check(128, 513, 256)\n        check(64, 513, 256)\n        check(4, 513, 256)\n\n        # other limits that don't optimize the palette\n        check(129, 511, 256)\n        check(255, 511, 256)\n        check(256, 511, 256)\n\n    def test_optimize_full_l(self):\n        im = Image.frombytes(\"L\", (16, 16), bytes(bytearray(range(256))))\n        test_file = BytesIO()\n        im.save(test_file, \"GIF\", optimize=True)\n        self.assertEqual(im.mode, \"L\")\n\n    def test_roundtrip(self):\n        out = self.tempfile('temp.gif')\n        im = hopper()\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), im, 50)\n\n    def test_roundtrip2(self):\n        # see https://github.com/python-pillow/Pillow/issues/403\n        out = self.tempfile('temp.gif')\n        im = Image.open(TEST_GIF)\n        im2 = im.copy()\n        im2.save(out)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), hopper(), 50)\n\n    def test_roundtrip_save_all(self):\n        # Single frame image\n        out = self.tempfile('temp.gif')\n        im = hopper()\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), im, 50)\n\n        # Multiframe image\n        im = Image.open(\"Tests/images/dispose_bgnd.gif\")\n\n        out = self.tempfile('temp.gif')\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.n_frames, 5)\n\n    def test_headers_saving_for_animated_gifs(self):\n        important_headers = ['background', 'version', 'duration', 'loop']\n        # Multiframe image\n        im = Image.open(\"Tests/images/dispose_bgnd.gif\")\n\n        out = self.tempfile('temp.gif')\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        for header in important_headers:\n            self.assertEqual(\n                im.info[header],\n                reread.info[header]\n            )\n\n    def test_palette_handling(self):\n        # see https://github.com/python-pillow/Pillow/issues/513\n\n        im = Image.open(TEST_GIF)\n        im = im.convert('RGB')\n\n        im = im.resize((100, 100), Image.LANCZOS)\n        im2 = im.convert('P', palette=Image.ADAPTIVE, colors=256)\n\n        f = self.tempfile('temp.gif')\n        im2.save(f, optimize=True)\n\n        reloaded = Image.open(f)\n\n        self.assert_image_similar(im, reloaded.convert('RGB'), 10)\n\n    def test_palette_434(self):\n        # see https://github.com/python-pillow/Pillow/issues/434\n\n        def roundtrip(im, *args, **kwargs):\n            out = self.tempfile('temp.gif')\n            im.copy().save(out, *args, **kwargs)\n            reloaded = Image.open(out)\n\n            return reloaded\n\n        orig = \"Tests/images/test.colors.gif\"\n        im = Image.open(orig)\n\n        self.assert_image_similar(im, roundtrip(im), 1)\n        self.assert_image_similar(im, roundtrip(im, optimize=True), 1)\n\n        im = im.convert(\"RGB\")\n        # check automatic P conversion\n        reloaded = roundtrip(im).convert('RGB')\n        self.assert_image_equal(im, reloaded)\n\n    @unittest.skipUnless(netpbm_available(), \"netpbm not available\")\n    def test_save_netpbm_bmp_mode(self):\n        img = Image.open(TEST_GIF).convert(\"RGB\")\n\n        tempfile = self.tempfile(\"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        self.assert_image_similar(img, Image.open(tempfile).convert(\"RGB\"), 0)\n\n    @unittest.skipUnless(netpbm_available(), \"netpbm not available\")\n    def test_save_netpbm_l_mode(self):\n        img = Image.open(TEST_GIF).convert(\"L\")\n\n        tempfile = self.tempfile(\"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        self.assert_image_similar(img, Image.open(tempfile).convert(\"L\"), 0)\n\n    def test_seek(self):\n        img = Image.open(\"Tests/images/dispose_none.gif\")\n        framecount = 0\n        try:\n            while True:\n                framecount += 1\n                img.seek(img.tell() + 1)\n        except EOFError:\n            self.assertEqual(framecount, 5)\n\n    def test_n_frames(self):\n        for path, n_frames in [\n            [TEST_GIF, 1],\n            ['Tests/images/iss634.gif', 42]\n        ]:\n            # Test is_animated before n_frames\n            im = Image.open(path)\n            self.assertEqual(im.is_animated, n_frames != 1)\n\n            # Test is_animated after n_frames\n            im = Image.open(path)\n            self.assertEqual(im.n_frames, n_frames)\n            self.assertEqual(im.is_animated, n_frames != 1)\n\n    def test_eoferror(self):\n        im = Image.open(TEST_GIF)\n        n_frames = im.n_frames\n\n        # Test seeking past the last frame\n        self.assertRaises(EOFError, im.seek, n_frames)\n        self.assertLess(im.tell(), n_frames)\n\n        # Test that seeking to the last frame does not raise an error\n        im.seek(n_frames-1)\n\n    def test_dispose_none(self):\n        img = Image.open(\"Tests/images/dispose_none.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 1)\n        except EOFError:\n            pass\n\n    def test_dispose_background(self):\n        img = Image.open(\"Tests/images/dispose_bgnd.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 2)\n        except EOFError:\n            pass\n\n    def test_dispose_previous(self):\n        img = Image.open(\"Tests/images/dispose_prev.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 3)\n        except EOFError:\n            pass\n\n    def test_save_dispose(self):\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111'),\n            Image.new('L', (100, 100), '#222'),\n        ]\n        for method in range(0, 4):\n            im_list[0].save(\n                out,\n                save_all=True,\n                append_images=im_list[1:],\n                disposal=method\n            )\n            img = Image.open(out)\n            for _ in range(2):\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, method)\n\n        # check per frame disposal\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            disposal=tuple(range(len(im_list)))\n            )\n\n        img = Image.open(out)\n\n        for i in range(2):\n            img.seek(img.tell() + 1)\n            self.assertEqual(img.disposal_method, i+1)\n\n    def test_iss634(self):\n        img = Image.open(\"Tests/images/iss634.gif\")\n        # seek to the second frame\n        img.seek(img.tell() + 1)\n        # all transparent pixels should be replaced with the color from the\n        # first frame\n        self.assertEqual(img.histogram()[img.info['transparency']], 0)\n\n    def test_duration(self):\n        duration = 1000\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.save(out, duration=duration)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['duration'], duration)\n\n    def test_multiple_duration(self):\n        duration_list = [1000, 2000, 3000]\n\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111'),\n            Image.new('L', (100, 100), '#222')\n        ]\n\n        # duration as list\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=duration_list\n        )\n        reread = Image.open(out)\n\n        for duration in duration_list:\n            self.assertEqual(reread.info['duration'], duration)\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n        # duration as tuple\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=tuple(duration_list)\n        )\n        reread = Image.open(out)\n\n        for duration in duration_list:\n            self.assertEqual(reread.info['duration'], duration)\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n    def test_identical_frames(self):\n        duration_list = [1000, 1500, 2000, 4000]\n\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111')\n        ]\n\n        # duration as list\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=duration_list\n        )\n        reread = Image.open(out)\n\n        # Assert that the first three frames were combined\n        self.assertEqual(reread.n_frames, 2)\n\n        # Assert that the new duration is the total of the identical frames\n        self.assertEqual(reread.info['duration'], 4500)\n\n    def test_number_of_loops(self):\n        number_of_loops = 2\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.save(out, loop=number_of_loops)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['loop'], number_of_loops)\n\n    def test_background(self):\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.info['background'] = 1\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['background'], im.info['background'])\n\n    def test_comment(self):\n        im = Image.open(TEST_GIF)\n        self.assertEqual(im.info['comment'], b\"File written by Adobe Photoshop\\xa8 4.0\")\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.info['comment'] = b\"Test comment text\"\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['comment'], im.info['comment'])\n\n    def test_version(self):\n        out = self.tempfile('temp.gif')\n\n        def assertVersionAfterSave(im, version):\n            im.save(out)\n            reread = Image.open(out)\n            self.assertEqual(reread.info[\"version\"], version)\n\n        # Test that GIF87a is used by default\n        im = Image.new('L', (100, 100), '#000')\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n        # Test setting the version to 89a\n        im = Image.new('L', (100, 100), '#000')\n        im.info[\"version\"] = b\"89a\"\n        assertVersionAfterSave(im, b\"GIF89a\")\n\n        # Test that adding a GIF89a feature changes the version\n        im.info[\"transparency\"] = 1\n        assertVersionAfterSave(im, b\"GIF89a\")\n\n        # Test that a GIF87a image is also saved in that format\n        im = Image.open(\"Tests/images/test.colors.gif\")\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n        # Test that a GIF89a image is also saved in that format\n        im.info[\"version\"] = b\"GIF89a\"\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n    def test_append_images(self):\n        out = self.tempfile('temp.gif')\n\n        # Test appending single frame images\n        im = Image.new('RGB', (100, 100), '#f00')\n        ims = [Image.new('RGB', (100, 100), color) for color\n               in ['#0f0', '#00f']]\n        im.copy().save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 3)\n\n        # Tests appending using a generator\n        def imGenerator(ims):\n            for im in ims:\n                yield im\n        im.save(out, save_all=True, append_images=imGenerator(ims))\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 3)\n\n        # Tests appending single and multiple frame images\n        im = Image.open(\"Tests/images/dispose_none.gif\")\n        ims = [Image.open(\"Tests/images/dispose_prev.gif\")]\n        im.save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 10)\n\n    def test_transparent_optimize(self):\n        # from issue #2195, if the transparent color is incorrectly\n        # optimized out, gif loses transparency\n        # Need a palette that isn't using the 0 color, and one\n        # that's > 128 items where the transparent color is actually\n        # the top palette entry to trigger the bug.\n\n        data = bytes(bytearray(range(1, 254)))\n        palette = ImagePalette.ImagePalette(\"RGB\", list(range(256))*3)\n\n        im = Image.new('L', (253, 1))\n        im.frombytes(data)\n        im.putpalette(palette)\n\n        out = self.tempfile('temp.gif')\n        im.save(out, transparency=253)\n        reloaded = Image.open(out)\n\n        self.assertEqual(reloaded.info['transparency'], 253)\n\n    def test_bbox(self):\n        out = self.tempfile('temp.gif')\n\n        im = Image.new('RGB', (100, 100), '#fff')\n        ims = [Image.new(\"RGB\", (100, 100), '#000')]\n        im.save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 2)\n\n    def test_palette_save_L(self):\n        # generate an L mode image with a separate palette\n\n        im = hopper('P')\n        im_l = Image.frombytes('L', im.size, im.tobytes())\n        palette = bytes(bytearray(im.getpalette()))\n\n        out = self.tempfile('temp.gif')\n        im_l.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n\n        self.assert_image_equal(reloaded.convert('RGB'), im.convert('RGB'))\n\n    def test_palette_save_P(self):\n        # pass in a different palette, then construct what the image\n        # would look like.\n        # Forcing a non-straight grayscale palette.\n\n        im = hopper('P')\n        palette = bytes(bytearray([255-i//3 for i in range(768)]))\n\n        out = self.tempfile('temp.gif')\n        im.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n        im.putpalette(palette)\n        self.assert_image_equal(reloaded, im)\n\n    def test_palette_save_ImagePalette(self):\n        # pass in a different palette, as an ImagePalette.ImagePalette\n        # effectively the same as test_palette_save_P\n\n        im = hopper('P')\n        palette = ImagePalette.ImagePalette('RGB', list(range(256))[::-1]*3)\n\n        out = self.tempfile('temp.gif')\n        im.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n        im.putpalette(palette)\n        self.assert_image_equal(reloaded, im)\n\n    def test_save_I(self):\n        # Test saving something that would trigger the auto-convert to 'L'\n\n        im = hopper('I')\n\n        out = self.tempfile('temp.gif')\n        im.save(out)\n\n        reloaded = Image.open(out)\n        self.assert_image_equal(reloaded.convert('L'), im.convert('L'))\n\n    def test_getdata(self):\n        # test getheader/getdata against legacy values\n        # Create a 'P' image with holes in the palette\n        im = Image._wedge().resize((16, 16))\n        im.putpalette(ImagePalette.ImagePalette('RGB'))\n        im.info = {'background': 0}\n\n        passed_palette = bytes(bytearray([255-i//3 for i in range(768)]))\n\n        GifImagePlugin._FORCE_OPTIMIZE = True\n        try:\n            h = GifImagePlugin.getheader(im, passed_palette)\n            d = GifImagePlugin.getdata(im)\n\n            import pickle\n            # Enable to get target values on pre-refactor version\n            # with open('Tests/images/gif_header_data.pkl', 'wb') as f:\n            #    pickle.dump((h, d), f, 1)\n            with open('Tests/images/gif_header_data.pkl', 'rb') as f:\n                (h_target, d_target) = pickle.load(f)\n\n            self.assertEqual(h, h_target)\n            self.assertEqual(d, d_target)\n        finally:\n            GifImagePlugin._FORCE_OPTIMIZE = False\n\n    def test_lzw_bits(self):\n        # see https://github.com/python-pillow/Pillow/issues/2811\n        im = Image.open('Tests/images/issue_2811.gif')\n\n        self.assertEqual(im.tile[0][3][0], 11)  # LZW bits\n        # codec error prepatch\n        im.load()",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 16,
                                "end_lineno": 586,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "#\n# The Python Imaging Library.\n# $Id$\n#\n# GIF file handling\n#\n# History:\n# 1995-09-01 fl   Created\n# 1996-12-14 fl   Added interlace support\n# 1996-12-30 fl   Added animation support\n# 1997-01-05 fl   Added write support, fixed local colour map bug\n# 1997-02-23 fl   Make sure to load raster data in getdata()\n# 1997-07-05 fl   Support external decoder (0.4)\n# 1998-07-09 fl   Handle all modes when saving (0.5)\n# 1998-07-15 fl   Renamed offset attribute to avoid name clash\n# 2001-04-16 fl   Added rewind support (seek to frame 0) (0.6)\n# 2001-04-17 fl   Added palette optimization (0.7)\n# 2002-06-06 fl   Added transparency support for save (0.8)\n# 2004-02-24 fl   Disable interlacing for small images\n#\n# Copyright (c) 1997-2004 by Secret Labs AB\n# Copyright (c) 1995-2004 by Fredrik Lundh\n#\n# See the README file for information on usage and redistribution.\n#\n\nfrom . import Image, ImageFile, ImagePalette, ImageChops, ImageSequence\nfrom ._binary import i8, i16le as i16, o8, o16le as o16\n\nimport itertools\n\n__version__ = \"0.9\"\n\n\n# --------------------------------------------------------------------\n# Identify/read GIF files\n\ndef _accept(prefix):\n    return prefix[:6] in [b\"GIF87a\", b\"GIF89a\"]\n\n\n##\n# Image plugin for GIF images.  This plugin supports both GIF87 and\n# GIF89 images.\n\nclass GifImageFile(ImageFile.ImageFile):\n\n    format = \"GIF\"\n    format_description = \"Compuserve GIF\"\n    _close_exclusive_fp_after_loading = False\n\n    global_palette = None\n\n    def data(self):\n        s = self.fp.read(1)\n        if s and i8(s):\n            return self.fp.read(i8(s))\n        return None\n\n    def _open(self):\n\n        # Screen\n        s = self.fp.read(13)\n        if s[:6] not in [b\"GIF87a\", b\"GIF89a\"]:\n            raise SyntaxError(\"not a GIF file\")\n\n        self.info[\"version\"] = s[:6]\n        self.size = i16(s[6:]), i16(s[8:])\n        self.tile = []\n        flags = i8(s[10])\n        bits = (flags & 7) + 1\n\n        if flags & 128:\n            # get global palette\n            self.info[\"background\"] = i8(s[11])\n            # check if palette contains colour indices\n            p = self.fp.read(3 << bits)\n            for i in range(0, len(p), 3):\n                if not (i//3 == i8(p[i]) == i8(p[i+1]) == i8(p[i+2])):\n                    p = ImagePalette.raw(\"RGB\", p)\n                    self.global_palette = self.palette = p\n                    break\n\n        self.__fp = self.fp  # FIXME: hack\n        self.__rewind = self.fp.tell()\n        self._n_frames = None\n        self._is_animated = None\n        self._seek(0)  # get ready to read first frame\n\n    @property\n    def n_frames(self):\n        if self._n_frames is None:\n            current = self.tell()\n            try:\n                while True:\n                    self.seek(self.tell() + 1)\n            except EOFError:\n                self._n_frames = self.tell() + 1\n            self.seek(current)\n        return self._n_frames\n\n    @property\n    def is_animated(self):\n        if self._is_animated is None:\n            if self._n_frames is not None:\n                self._is_animated = self._n_frames != 1\n            else:\n                current = self.tell()\n\n                try:\n                    self.seek(1)\n                    self._is_animated = True\n                except EOFError:\n                    self._is_animated = False\n\n                self.seek(current)\n        return self._is_animated\n\n    def seek(self, frame):\n        if not self._seek_check(frame):\n            return\n        if frame < self.__frame:\n            self._seek(0)\n\n        last_frame = self.__frame\n        for f in range(self.__frame + 1, frame + 1):\n            try:\n                self._seek(f)\n            except EOFError:\n                self.seek(last_frame)\n                raise EOFError(\"no more images in GIF file\")\n\n    def _seek(self, frame):\n\n        if frame == 0:\n            # rewind\n            self.__offset = 0\n            self.dispose = None\n            self.dispose_extent = [0, 0, 0, 0]  # x0, y0, x1, y1\n            self.__frame = -1\n            self.__fp.seek(self.__rewind)\n            self._prev_im = None\n            self.disposal_method = 0\n        else:\n            # ensure that the previous frame was loaded\n            if not self.im:\n                self.load()\n\n        if frame != self.__frame + 1:\n            raise ValueError(\"cannot seek to frame %d\" % frame)\n        self.__frame = frame\n\n        self.tile = []\n\n        self.fp = self.__fp\n        if self.__offset:\n            # backup to last frame\n            self.fp.seek(self.__offset)\n            while self.data():\n                pass\n            self.__offset = 0\n\n        if self.dispose:\n            self.im.paste(self.dispose, self.dispose_extent)\n\n        from copy import copy\n        self.palette = copy(self.global_palette)\n\n        while True:\n\n            s = self.fp.read(1)\n            if not s or s == b\";\":\n                break\n\n            elif s == b\"!\":\n                #\n                # extensions\n                #\n                s = self.fp.read(1)\n                block = self.data()\n                if i8(s) == 249:\n                    #\n                    # graphic control extension\n                    #\n                    flags = i8(block[0])\n                    if flags & 1:\n                        self.info[\"transparency\"] = i8(block[3])\n                    self.info[\"duration\"] = i16(block[1:3]) * 10\n\n                    # disposal method - find the value of bits 4 - 6\n                    dispose_bits = 0b00011100 & flags\n                    dispose_bits = dispose_bits >> 2\n                    if dispose_bits:\n                        # only set the dispose if it is not\n                        # unspecified. I'm not sure if this is\n                        # correct, but it seems to prevent the last\n                        # frame from looking odd for some animations\n                        self.disposal_method = dispose_bits\n                elif i8(s) == 254:\n                    #\n                    # comment extension\n                    #\n                    self.info[\"comment\"] = block\n                elif i8(s) == 255:\n                    #\n                    # application extension\n                    #\n                    self.info[\"extension\"] = block, self.fp.tell()\n                    if block[:11] == b\"NETSCAPE2.0\":\n                        block = self.data()\n                        if len(block) >= 3 and i8(block[0]) == 1:\n                            self.info[\"loop\"] = i16(block[1:3])\n                while self.data():\n                    pass\n\n            elif s == b\",\":\n                #\n                # local image\n                #\n                s = self.fp.read(9)\n\n                # extent\n                x0, y0 = i16(s[0:]), i16(s[2:])\n                x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:])\n                self.dispose_extent = x0, y0, x1, y1\n                flags = i8(s[8])\n\n                interlace = (flags & 64) != 0\n\n                if flags & 128:\n                    bits = (flags & 7) + 1\n                    self.palette =\\\n                        ImagePalette.raw(\"RGB\", self.fp.read(3 << bits))\n\n                # image data\n                bits = i8(self.fp.read(1))\n                self.__offset = self.fp.tell()\n                self.tile = [(\"gif\",\n                             (x0, y0, x1, y1),\n                             self.__offset,\n                             (bits, interlace))]\n                break\n\n            else:\n                pass\n                # raise IOError, \"illegal GIF tag `%x`\" % i8(s)\n\n        try:\n            if self.disposal_method < 2:\n                # do not dispose or none specified\n                self.dispose = None\n            elif self.disposal_method == 2:\n                # replace with background colour\n                self.dispose = Image.core.fill(\"P\", self.size,\n                                               self.info[\"background\"])\n            else:\n                # replace with previous contents\n                if self.im:\n                    self.dispose = self.im.copy()\n\n            # only dispose the extent in this frame\n            if self.dispose:\n                self.dispose = self._crop(self.dispose, self.dispose_extent)\n        except (AttributeError, KeyError):\n            pass\n\n        if not self.tile:\n            # self.__fp = None\n            raise EOFError\n\n        self.mode = \"L\"\n        if self.palette:\n            self.mode = \"P\"\n\n    def tell(self):\n        return self.__frame\n\n    def load_end(self):\n        ImageFile.ImageFile.load_end(self)\n\n        # if the disposal method is 'do not dispose', transparent\n        # pixels should show the content of the previous frame\n        if self._prev_im and self.disposal_method == 1:\n            # we do this by pasting the updated area onto the previous\n            # frame which we then use as the current image content\n            updated = self._crop(self.im, self.dispose_extent)\n            self._prev_im.paste(updated, self.dispose_extent,\n                                updated.convert('RGBA'))\n            self.im = self._prev_im\n        self._prev_im = self.im.copy()\n\n# --------------------------------------------------------------------\n# Write GIF files\n\n\nRAWMODE = {\n    \"1\": \"L\",\n    \"L\": \"L\",\n    \"P\": \"P\"\n}\n\n\ndef _normalize_mode(im, initial_call=False):\n    \"\"\"\n    Takes an image (or frame), returns an image in a mode that is appropriate\n    for saving in a Gif.\n\n    It may return the original image, or it may return an image converted to\n    palette or 'L' mode.\n\n    UNDONE: What is the point of mucking with the initial call palette, for\n    an image that shouldn't have a palette, or it would be a mode 'P' and\n    get returned in the RAWMODE clause.\n\n    :param im: Image object\n    :param initial_call: Default false, set to true for a single frame.\n    :returns: Image object\n    \"\"\"\n    if im.mode in RAWMODE:\n        im.load()\n        return im\n    if Image.getmodebase(im.mode) == \"RGB\":\n        if initial_call:\n            palette_size = 256\n            if im.palette:\n                palette_size = len(im.palette.getdata()[1]) // 3\n            return im.convert(\"P\", palette=Image.ADAPTIVE, colors=palette_size)\n        else:\n            return im.convert(\"P\")\n    return im.convert(\"L\")\n\n\ndef _normalize_palette(im, palette, info):\n    \"\"\"\n    Normalizes the palette for image.\n      - Sets the palette to the incoming palette, if provided.\n      - Ensures that there's a palette for L mode images\n      - Optimizes the palette if necessary/desired.\n\n    :param im: Image object\n    :param palette: bytes object containing the source palette, or ....\n    :param info: encoderinfo\n    :returns: Image object\n    \"\"\"\n    source_palette = None\n    if palette:\n        # a bytes palette\n        if isinstance(palette, (bytes, bytearray, list)):\n            source_palette = bytearray(palette[:768])\n        if isinstance(palette, ImagePalette.ImagePalette):\n            source_palette = bytearray(itertools.chain.from_iterable(\n                                zip(palette.palette[:256],\n                                    palette.palette[256:512],\n                                    palette.palette[512:768])))\n\n    if im.mode == \"P\":\n        if not source_palette:\n            source_palette = im.im.getpalette(\"RGB\")[:768]\n    else:  # L-mode\n        if not source_palette:\n            source_palette = bytearray(i//3 for i in range(768))\n        im.palette = ImagePalette.ImagePalette(\"RGB\",\n                                               palette=source_palette)\n\n    used_palette_colors = _get_optimize(im, info)\n    if used_palette_colors is not None:\n        return im.remap_palette(used_palette_colors, source_palette)\n\n    im.palette.palette = source_palette\n    return im\n\n\ndef _write_single_frame(im, fp, palette):\n    im_out = _normalize_mode(im, True)\n    im_out = _normalize_palette(im_out, palette, im.encoderinfo)\n\n    for s in _get_global_header(im_out, im.encoderinfo):\n        fp.write(s)\n\n    # local image header\n    flags = 0\n    if get_interlace(im):\n        flags = flags | 64\n    _write_local_header(fp, im, (0, 0), flags)\n\n    im_out.encoderconfig = (8, get_interlace(im))\n    ImageFile._save(im_out, fp, [(\"gif\", (0, 0)+im.size, 0,\n                                  RAWMODE[im_out.mode])])\n\n    fp.write(b\"\\0\")  # end of image data\n\n\ndef _write_multiple_frames(im, fp, palette):\n\n    duration = im.encoderinfo.get(\"duration\", None)\n    disposal = im.encoderinfo.get('disposal', None)\n\n    im_frames = []\n    frame_count = 0\n    for imSequence in itertools.chain([im], im.encoderinfo.get(\"append_images\", [])):\n        for im_frame in ImageSequence.Iterator(imSequence):\n            # a copy is required here since seek can still mutate the image\n            im_frame = _normalize_mode(im_frame.copy())\n            im_frame = _normalize_palette(im_frame, palette, im.encoderinfo)\n\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            frame_count += 1\n\n            if im_frames:\n                # delta frame\n                previous = im_frames[-1]\n                if _get_palette_bytes(im_frame) == _get_palette_bytes(previous['im']):\n                    delta = ImageChops.subtract_modulo(im_frame,\n                                                       previous['im'])\n                else:\n                    delta = ImageChops.subtract_modulo(im_frame.convert('RGB'),\n                                                       previous['im'].convert('RGB'))\n                bbox = delta.getbbox()\n                if not bbox:\n                    # This frame is identical to the previous frame\n                    if duration:\n                        previous['encoderinfo']['duration'] += encoderinfo['duration']\n                    continue\n            else:\n                bbox = None\n            im_frames.append({\n                'im': im_frame,\n                'bbox': bbox,\n                'encoderinfo': encoderinfo\n            })\n\n    if len(im_frames) > 1:\n        for frame_data in im_frames:\n            im_frame = frame_data['im']\n            if not frame_data['bbox']:\n                # global header\n                for s in _get_global_header(im_frame,\n                                            frame_data['encoderinfo']):\n                    fp.write(s)\n                offset = (0, 0)\n            else:\n                # compress difference\n                frame_data['encoderinfo']['include_color_table'] = True\n\n                im_frame = im_frame.crop(frame_data['bbox'])\n                offset = frame_data['bbox'][:2]\n            _write_frame_data(fp, im_frame, offset, frame_data['encoderinfo'])\n        return True\n\n\ndef _save_all(im, fp, filename):\n    _save(im, fp, filename, save_all=True)\n\n\ndef _save(im, fp, filename, save_all=False):\n    im.encoderinfo.update(im.info)\n    # header\n    try:\n        palette = im.encoderinfo[\"palette\"]\n    except KeyError:\n        palette = None\n        im.encoderinfo[\"optimize\"] = im.encoderinfo.get(\"optimize\", True)\n\n    if not save_all or not _write_multiple_frames(im, fp, palette):\n        _write_single_frame(im, fp, palette)\n\n    fp.write(b\";\")  # end of file\n\n    if hasattr(fp, \"flush\"):\n        fp.flush()\n\n\ndef get_interlace(im):\n    interlace = im.encoderinfo.get(\"interlace\", 1)\n\n    # workaround for @PIL153\n    if min(im.size) < 16:\n        interlace = 0\n\n    return interlace\n\n\ndef _write_local_header(fp, im, offset, flags):\n    transparent_color_exists = False\n    try:\n        transparency = im.encoderinfo[\"transparency\"]\n    except KeyError:\n        pass\n    else:\n        transparency = int(transparency)\n        # optimize the block away if transparent color is not used\n        transparent_color_exists = True\n\n        used_palette_colors = _get_optimize(im, im.encoderinfo)\n        if used_palette_colors is not None:\n            # adjust the transparency index after optimize\n            try:\n                transparency = used_palette_colors.index(transparency)\n            except ValueError:\n                transparent_color_exists = False\n\n    if \"duration\" in im.encoderinfo:\n        duration = int(im.encoderinfo[\"duration\"] / 10)\n    else:\n        duration = 0\n\n    disposal = int(im.encoderinfo.get('disposal', 0))\n\n    if transparent_color_exists or duration != 0 or disposal:\n        packed_flag = 1 if transparent_color_exists else 0\n        packed_flag |= disposal << 2\n        if not transparent_color_exists:\n            transparency = 0\n\n        fp.write(b\"!\" +\n                 o8(249) +                # extension intro\n                 o8(4) +                  # length\n                 o8(packed_flag) +        # packed fields\n                 o16(duration) +          # duration\n                 o8(transparency) +       # transparency index\n                 o8(0))\n\n    if \"comment\" in im.encoderinfo and 1 <= len(im.encoderinfo[\"comment\"]) <= 255:\n        fp.write(b\"!\" +\n                 o8(254) +                # extension intro\n                 o8(len(im.encoderinfo[\"comment\"])) +\n                 im.encoderinfo[\"comment\"] +\n                 o8(0))\n    if \"loop\" in im.encoderinfo:\n        number_of_loops = im.encoderinfo[\"loop\"]\n        fp.write(b\"!\" +\n                 o8(255) +                # extension intro\n                 o8(11) +\n                 b\"NETSCAPE2.0\" +\n                 o8(3) +\n                 o8(1) +\n                 o16(number_of_loops) +   # number of loops\n                 o8(0))\n    include_color_table = im.encoderinfo.get('include_color_table')\n    if include_color_table:\n        palette = im.encoderinfo.get(\"palette\", None)\n        palette_bytes = _get_palette_bytes(im)\n        color_table_size = _get_color_table_size(palette_bytes)\n        if color_table_size:\n            flags = flags | 128               # local color table flag\n            flags = flags | color_table_size\n\n    fp.write(b\",\" +\n             o16(offset[0]) +             # offset\n             o16(offset[1]) +\n             o16(im.size[0]) +            # size\n             o16(im.size[1]) +\n             o8(flags))                   # flags\n    if include_color_table and color_table_size:\n        fp.write(_get_header_palette(palette_bytes))\n    fp.write(o8(8))                       # bits\n\n\ndef _save_netpbm(im, fp, filename):\n\n    # Unused by default.\n    # To use, uncomment the register_save call at the end of the file.\n    #\n    # If you need real GIF compression and/or RGB quantization, you\n    # can use the external NETPBM/PBMPLUS utilities.  See comments\n    # below for information on how to enable this.\n\n    import os\n    from subprocess import Popen, check_call, PIPE, CalledProcessError\n    file = im._dump()\n\n    with open(filename, 'wb') as f:\n        if im.mode != \"RGB\":\n            with open(os.devnull, 'wb') as devnull:\n                check_call([\"ppmtogif\", file], stdout=f, stderr=devnull)\n        else:\n            # Pipe ppmquant output into ppmtogif\n            # \"ppmquant 256 %s | ppmtogif > %s\" % (file, filename)\n            quant_cmd = [\"ppmquant\", \"256\", file]\n            togif_cmd = [\"ppmtogif\"]\n            with open(os.devnull, 'wb') as devnull:\n                quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=devnull)\n                togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout,\n                                   stdout=f, stderr=devnull)\n\n            # Allow ppmquant to receive SIGPIPE if ppmtogif exits\n            quant_proc.stdout.close()\n\n            retcode = quant_proc.wait()\n            if retcode:\n                raise CalledProcessError(retcode, quant_cmd)\n\n            retcode = togif_proc.wait()\n            if retcode:\n                raise CalledProcessError(retcode, togif_cmd)\n\n    try:\n        os.unlink(file)\n    except OSError:\n        pass\n\n\n# Force optimization so that we can test performance against\n# cases where it took lots of memory and time previously.\n_FORCE_OPTIMIZE = False\n\n\ndef _get_optimize(im, info):\n    \"\"\"\n    Palette optimization is a potentially expensive operation.\n\n    This function determines if the palette should be optimized using\n    some heuristics, then returns the list of palette entries in use.\n\n    :param im: Image object\n    :param info: encoderinfo\n    :returns: list of indexes of palette entries in use, or None\n    \"\"\"\n    if im.mode in (\"P\", \"L\") and info and info.get(\"optimize\", 0):\n        # Potentially expensive operation.\n\n        # The palette saves 3 bytes per color not used, but palette\n        # lengths are restricted to 3*(2**N) bytes. Max saving would\n        # be 768 -> 6 bytes if we went all the way down to 2 colors.\n        # * If we're over 128 colors, we can't save any space.\n        # * If there aren't any holes, it's not worth collapsing.\n        # * If we have a 'large' image, the palette is in the noise.\n\n        # create the new palette if not every color is used\n        optimise = _FORCE_OPTIMIZE or im.mode == 'L'\n        if optimise or im.width * im.height < 512 * 512:\n            # check which colors are used\n            used_palette_colors = []\n            for i, count in enumerate(im.histogram()):\n                if count:\n                    used_palette_colors.append(i)\n\n            if optimise or (len(used_palette_colors) <= 128 and\n               max(used_palette_colors) > len(used_palette_colors)):\n                return used_palette_colors\n\n\ndef _get_color_table_size(palette_bytes):\n    # calculate the palette size for the header\n    import math\n    color_table_size = int(math.ceil(math.log(len(palette_bytes)//3, 2)))-1\n    if color_table_size < 0:\n        color_table_size = 0\n    return color_table_size\n\n\ndef _get_header_palette(palette_bytes):\n    \"\"\"\n    Returns the palette, null padded to the next power of 2 (*3) bytes\n    suitable for direct inclusion in the GIF header\n\n    :param palette_bytes: Unpadded palette bytes, in RGBRGB form\n    :returns: Null padded palette\n    \"\"\"\n    color_table_size = _get_color_table_size(palette_bytes)\n\n    # add the missing amount of bytes\n    # the palette has to be 2<<n in size\n    actual_target_size_diff = (2 << color_table_size) - len(palette_bytes)//3\n    if actual_target_size_diff > 0:\n        palette_bytes += o8(0) * 3 * actual_target_size_diff\n    return palette_bytes\n\n\ndef _get_palette_bytes(im):\n    \"\"\"\n    Gets the palette for inclusion in the gif header\n\n    :param im: Image object\n    :returns: Bytes, len<=768 suitable for inclusion in gif header\n    \"\"\"\n    return im.palette.palette\n\n\ndef _get_global_header(im, info):\n    \"\"\"Return a list of strings representing a GIF header\"\"\"\n\n    # Header Block\n    # http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp\n\n    version = b\"87a\"\n    for extensionKey in [\"transparency\", \"duration\", \"loop\", \"comment\"]:\n        if info and extensionKey in info:\n            if ((extensionKey == \"duration\" and info[extensionKey] == 0) or\n               (extensionKey == \"comment\" and not (1 <= len(info[extensionKey]) <= 255))):\n                continue\n            version = b\"89a\"\n            break\n    else:\n        if im.info.get(\"version\") == b\"89a\":\n            version = b\"89a\"\n\n    palette_bytes = _get_palette_bytes(im)\n    color_table_size = _get_color_table_size(palette_bytes)\n\n    background = info[\"background\"] if \"background\" in info else 0\n\n    return [\n        b\"GIF\"+version +               # signature + version\n        o16(im.size[0]) +              # canvas width\n        o16(im.size[1]),               # canvas height\n\n        # Logical Screen Descriptor\n        # size of global color table + global color table flag\n        o8(color_table_size + 128),   # packed fields\n        # background + reserved/aspect\n        o8(background) + o8(0),\n\n        # Global Color Table\n        _get_header_palette(palette_bytes)\n    ]\n\n\ndef _write_frame_data(fp, im_frame, offset, params):\n    try:\n        im_frame.encoderinfo = params\n\n        # local image header\n        _write_local_header(fp, im_frame, offset, 0)\n\n        ImageFile._save(im_frame, fp, [(\"gif\", (0, 0)+im_frame.size, 0,\n                                        RAWMODE[im_frame.mode])])\n\n        fp.write(b\"\\0\")  # end of image data\n    finally:\n        del im_frame.encoderinfo\n\n# --------------------------------------------------------------------\n# Legacy GIF utilities\n\n\ndef getheader(im, palette=None, info=None):\n    \"\"\"\n    Legacy Method to get Gif data from image.\n\n    Warning:: May modify image data.\n\n    :param im: Image object\n    :param palette: bytes object containing the source palette, or ....\n    :param info: encoderinfo\n    :returns: tuple of(list of header items, optimized palette)\n\n    \"\"\"\n    used_palette_colors = _get_optimize(im, info)\n\n    if info is None:\n        info = {}\n\n    if \"background\" not in info and \"background\" in im.info:\n        info[\"background\"] = im.info[\"background\"]\n\n    im_mod = _normalize_palette(im, palette, info)\n    im.palette = im_mod.palette\n    im.im = im_mod.im\n    header = _get_global_header(im, info)\n\n    return header, used_palette_colors\n\n\n# To specify duration, add the time in milliseconds to getdata(),\n# e.g. getdata(im_frame, duration=1000)\ndef getdata(im, offset=(0, 0), **params):\n    \"\"\"\n    Legacy Method\n\n    Return a list of strings representing this image.\n    The first string is a local image header, the rest contains\n    encoded image data.\n\n    :param im: Image object\n    :param offset: Tuple of (x, y) pixels. Defaults to (0,0)\n    :param \\\\**params: E.g. duration or other encoder info parameters\n    :returns: List of Bytes containing gif encoded frame data\n\n    \"\"\"\n    class Collector(object):\n        data = []\n\n        def write(self, data):\n            self.data.append(data)\n\n    im.load()  # make sure raster data is available\n\n    fp = Collector()\n\n    _write_frame_data(fp, im, offset, params)\n\n    return fp.data\n\n\n# --------------------------------------------------------------------\n# Registry\n\nImage.register_open(GifImageFile.format, GifImageFile, _accept)\nImage.register_save(GifImageFile.format, _save)\nImage.register_save_all(GifImageFile.format, _save_all)\nImage.register_extension(GifImageFile.format, \".gif\")\nImage.register_mime(GifImageFile.format, \"image/gif\")\n\n#\n# Uncomment the following line if you wish to use NETPBM/PBMPLUS\n# instead of the built-in \"uncompressed\" GIF encoder\n\n# Image.register_save(GifImageFile.format, _save_netpbm)\n",
            "new_file_content": "#\n# The Python Imaging Library.\n# $Id$\n#\n# GIF file handling\n#\n# History:\n# 1995-09-01 fl   Created\n# 1996-12-14 fl   Added interlace support\n# 1996-12-30 fl   Added animation support\n# 1997-01-05 fl   Added write support, fixed local colour map bug\n# 1997-02-23 fl   Make sure to load raster data in getdata()\n# 1997-07-05 fl   Support external decoder (0.4)\n# 1998-07-09 fl   Handle all modes when saving (0.5)\n# 1998-07-15 fl   Renamed offset attribute to avoid name clash\n# 2001-04-16 fl   Added rewind support (seek to frame 0) (0.6)\n# 2001-04-17 fl   Added palette optimization (0.7)\n# 2002-06-06 fl   Added transparency support for save (0.8)\n# 2004-02-24 fl   Disable interlacing for small images\n#\n# Copyright (c) 1997-2004 by Secret Labs AB\n# Copyright (c) 1995-2004 by Fredrik Lundh\n#\n# See the README file for information on usage and redistribution.\n#\n\nfrom . import Image, ImageFile, ImagePalette, ImageChops, ImageSequence\nfrom ._binary import i8, i16le as i16, o8, o16le as o16\n\nimport itertools\n\n__version__ = \"0.9\"\n\n\n# --------------------------------------------------------------------\n# Identify/read GIF files\n\ndef _accept(prefix):\n    return prefix[:6] in [b\"GIF87a\", b\"GIF89a\"]\n\n\n##\n# Image plugin for GIF images.  This plugin supports both GIF87 and\n# GIF89 images.\n\nclass GifImageFile(ImageFile.ImageFile):\n\n    format = \"GIF\"\n    format_description = \"Compuserve GIF\"\n    _close_exclusive_fp_after_loading = False\n\n    global_palette = None\n\n    def data(self):\n        s = self.fp.read(1)\n        if s and i8(s):\n            return self.fp.read(i8(s))\n        return None\n\n    def _open(self):\n\n        # Screen\n        s = self.fp.read(13)\n        if s[:6] not in [b\"GIF87a\", b\"GIF89a\"]:\n            raise SyntaxError(\"not a GIF file\")\n\n        self.info[\"version\"] = s[:6]\n        self.size = i16(s[6:]), i16(s[8:])\n        self.tile = []\n        flags = i8(s[10])\n        bits = (flags & 7) + 1\n\n        if flags & 128:\n            # get global palette\n            self.info[\"background\"] = i8(s[11])\n            # check if palette contains colour indices\n            p = self.fp.read(3 << bits)\n            for i in range(0, len(p), 3):\n                if not (i//3 == i8(p[i]) == i8(p[i+1]) == i8(p[i+2])):\n                    p = ImagePalette.raw(\"RGB\", p)\n                    self.global_palette = self.palette = p\n                    break\n\n        self.__fp = self.fp  # FIXME: hack\n        self.__rewind = self.fp.tell()\n        self._n_frames = None\n        self._is_animated = None\n        self._seek(0)  # get ready to read first frame\n\n    @property\n    def n_frames(self):\n        if self._n_frames is None:\n            current = self.tell()\n            try:\n                while True:\n                    self.seek(self.tell() + 1)\n            except EOFError:\n                self._n_frames = self.tell() + 1\n            self.seek(current)\n        return self._n_frames\n\n    @property\n    def is_animated(self):\n        if self._is_animated is None:\n            if self._n_frames is not None:\n                self._is_animated = self._n_frames != 1\n            else:\n                current = self.tell()\n\n                try:\n                    self.seek(1)\n                    self._is_animated = True\n                except EOFError:\n                    self._is_animated = False\n\n                self.seek(current)\n        return self._is_animated\n\n    def seek(self, frame):\n        if not self._seek_check(frame):\n            return\n        if frame < self.__frame:\n            self._seek(0)\n\n        last_frame = self.__frame\n        for f in range(self.__frame + 1, frame + 1):\n            try:\n                self._seek(f)\n            except EOFError:\n                self.seek(last_frame)\n                raise EOFError(\"no more images in GIF file\")\n\n    def _seek(self, frame):\n\n        if frame == 0:\n            # rewind\n            self.__offset = 0\n            self.dispose = None\n            self.dispose_extent = [0, 0, 0, 0]  # x0, y0, x1, y1\n            self.__frame = -1\n            self.__fp.seek(self.__rewind)\n            self._prev_im = None\n            self.disposal_method = 0\n        else:\n            # ensure that the previous frame was loaded\n            if not self.im:\n                self.load()\n\n        if frame != self.__frame + 1:\n            raise ValueError(\"cannot seek to frame %d\" % frame)\n        self.__frame = frame\n\n        self.tile = []\n\n        self.fp = self.__fp\n        if self.__offset:\n            # backup to last frame\n            self.fp.seek(self.__offset)\n            while self.data():\n                pass\n            self.__offset = 0\n\n        if self.dispose:\n            self.im.paste(self.dispose, self.dispose_extent)\n\n        from copy import copy\n        self.palette = copy(self.global_palette)\n\n        while True:\n\n            s = self.fp.read(1)\n            if not s or s == b\";\":\n                break\n\n            elif s == b\"!\":\n                #\n                # extensions\n                #\n                s = self.fp.read(1)\n                block = self.data()\n                if i8(s) == 249:\n                    #\n                    # graphic control extension\n                    #\n                    flags = i8(block[0])\n                    if flags & 1:\n                        self.info[\"transparency\"] = i8(block[3])\n                    self.info[\"duration\"] = i16(block[1:3]) * 10\n\n                    # disposal method - find the value of bits 4 - 6\n                    dispose_bits = 0b00011100 & flags\n                    dispose_bits = dispose_bits >> 2\n                    if dispose_bits:\n                        # only set the dispose if it is not\n                        # unspecified. I'm not sure if this is\n                        # correct, but it seems to prevent the last\n                        # frame from looking odd for some animations\n                        self.disposal_method = dispose_bits\n                elif i8(s) == 254:\n                    #\n                    # comment extension\n                    #\n                    self.info[\"comment\"] = block\n                elif i8(s) == 255:\n                    #\n                    # application extension\n                    #\n                    self.info[\"extension\"] = block, self.fp.tell()\n                    if block[:11] == b\"NETSCAPE2.0\":\n                        block = self.data()\n                        if len(block) >= 3 and i8(block[0]) == 1:\n                            self.info[\"loop\"] = i16(block[1:3])\n                while self.data():\n                    pass\n\n            elif s == b\",\":\n                #\n                # local image\n                #\n                s = self.fp.read(9)\n\n                # extent\n                x0, y0 = i16(s[0:]), i16(s[2:])\n                x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:])\n                self.dispose_extent = x0, y0, x1, y1\n                flags = i8(s[8])\n\n                interlace = (flags & 64) != 0\n\n                if flags & 128:\n                    bits = (flags & 7) + 1\n                    self.palette =\\\n                        ImagePalette.raw(\"RGB\", self.fp.read(3 << bits))\n\n                # image data\n                bits = i8(self.fp.read(1))\n                self.__offset = self.fp.tell()\n                self.tile = [(\"gif\",\n                             (x0, y0, x1, y1),\n                             self.__offset,\n                             (bits, interlace))]\n                break\n\n            else:\n                pass\n                # raise IOError, \"illegal GIF tag `%x`\" % i8(s)\n\n        try:\n            if self.disposal_method < 2:\n                # do not dispose or none specified\n                self.dispose = None\n            elif self.disposal_method == 2:\n                # replace with background colour\n                self.dispose = Image.core.fill(\"P\", self.size,\n                                               self.info[\"background\"])\n            else:\n                # replace with previous contents\n                if self.im:\n                    self.dispose = self.im.copy()\n\n            # only dispose the extent in this frame\n            if self.dispose:\n                self.dispose = self._crop(self.dispose, self.dispose_extent)\n        except (AttributeError, KeyError):\n            pass\n\n        if not self.tile:\n            # self.__fp = None\n            raise EOFError\n\n        self.mode = \"L\"\n        if self.palette:\n            self.mode = \"P\"\n\n    def tell(self):\n        return self.__frame\n\n    def load_end(self):\n        ImageFile.ImageFile.load_end(self)\n\n        # if the disposal method is 'do not dispose', transparent\n        # pixels should show the content of the previous frame\n        if self._prev_im and self.disposal_method == 1:\n            # we do this by pasting the updated area onto the previous\n            # frame which we then use as the current image content\n            updated = self._crop(self.im, self.dispose_extent)\n            self._prev_im.paste(updated, self.dispose_extent,\n                                updated.convert('RGBA'))\n            self.im = self._prev_im\n        self._prev_im = self.im.copy()\n\n# --------------------------------------------------------------------\n# Write GIF files\n\n\nRAWMODE = {\n    \"1\": \"L\",\n    \"L\": \"L\",\n    \"P\": \"P\"\n}\n\n\ndef _normalize_mode(im, initial_call=False):\n    \"\"\"\n    Takes an image (or frame), returns an image in a mode that is appropriate\n    for saving in a Gif.\n\n    It may return the original image, or it may return an image converted to\n    palette or 'L' mode.\n\n    UNDONE: What is the point of mucking with the initial call palette, for\n    an image that shouldn't have a palette, or it would be a mode 'P' and\n    get returned in the RAWMODE clause.\n\n    :param im: Image object\n    :param initial_call: Default false, set to true for a single frame.\n    :returns: Image object\n    \"\"\"\n    if im.mode in RAWMODE:\n        im.load()\n        return im\n    if Image.getmodebase(im.mode) == \"RGB\":\n        if initial_call:\n            palette_size = 256\n            if im.palette:\n                palette_size = len(im.palette.getdata()[1]) // 3\n            return im.convert(\"P\", palette=Image.ADAPTIVE, colors=palette_size)\n        else:\n            return im.convert(\"P\")\n    return im.convert(\"L\")\n\n\ndef _normalize_palette(im, palette, info):\n    \"\"\"\n    Normalizes the palette for image.\n      - Sets the palette to the incoming palette, if provided.\n      - Ensures that there's a palette for L mode images\n      - Optimizes the palette if necessary/desired.\n\n    :param im: Image object\n    :param palette: bytes object containing the source palette, or ....\n    :param info: encoderinfo\n    :returns: Image object\n    \"\"\"\n    source_palette = None\n    if palette:\n        # a bytes palette\n        if isinstance(palette, (bytes, bytearray, list)):\n            source_palette = bytearray(palette[:768])\n        if isinstance(palette, ImagePalette.ImagePalette):\n            source_palette = bytearray(itertools.chain.from_iterable(\n                                zip(palette.palette[:256],\n                                    palette.palette[256:512],\n                                    palette.palette[512:768])))\n\n    if im.mode == \"P\":\n        if not source_palette:\n            source_palette = im.im.getpalette(\"RGB\")[:768]\n    else:  # L-mode\n        if not source_palette:\n            source_palette = bytearray(i//3 for i in range(768))\n        im.palette = ImagePalette.ImagePalette(\"RGB\",\n                                               palette=source_palette)\n\n    used_palette_colors = _get_optimize(im, info)\n    if used_palette_colors is not None:\n        return im.remap_palette(used_palette_colors, source_palette)\n\n    im.palette.palette = source_palette\n    return im\n\n\ndef _write_single_frame(im, fp, palette):\n    im_out = _normalize_mode(im, True)\n    im_out = _normalize_palette(im_out, palette, im.encoderinfo)\n\n    for s in _get_global_header(im_out, im.encoderinfo):\n        fp.write(s)\n\n    # local image header\n    flags = 0\n    if get_interlace(im):\n        flags = flags | 64\n    _write_local_header(fp, im, (0, 0), flags)\n\n    im_out.encoderconfig = (8, get_interlace(im))\n    ImageFile._save(im_out, fp, [(\"gif\", (0, 0)+im.size, 0,\n                                  RAWMODE[im_out.mode])])\n\n    fp.write(b\"\\0\")  # end of image data\n\n\ndef _write_multiple_frames(im, fp, palette):\n\n    duration = im.encoderinfo.get(\"duration\", None)\n    disposal = im.encoderinfo.get('disposal', None)\n\n    im_frames = []\n    frame_count = 0\n    for imSequence in itertools.chain([im], im.encoderinfo.get(\"append_images\", [])):\n        for im_frame in ImageSequence.Iterator(imSequence):\n            # a copy is required here since seek can still mutate the image\n            im_frame = _normalize_mode(im_frame.copy())\n            im_frame = _normalize_palette(im_frame, palette, im.encoderinfo)\n\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            frame_count += 1\n\n            if im_frames:\n                # delta frame\n                previous = im_frames[-1]\n                if _get_palette_bytes(im_frame) == _get_palette_bytes(previous['im']):\n                    delta = ImageChops.subtract_modulo(im_frame,\n                                                       previous['im'])\n                else:\n                    delta = ImageChops.subtract_modulo(im_frame.convert('RGB'),\n                                                       previous['im'].convert('RGB'))\n                bbox = delta.getbbox()\n                if not bbox:\n                    # This frame is identical to the previous frame\n                    if duration:\n                        previous['encoderinfo']['duration'] += encoderinfo['duration']\n                    continue\n            else:\n                bbox = None\n            im_frames.append({\n                'im': im_frame,\n                'bbox': bbox,\n                'encoderinfo': encoderinfo\n            })\n\n    if len(im_frames) > 1:\n        for frame_data in im_frames:\n            im_frame = frame_data['im']\n            if not frame_data['bbox']:\n                # global header\n                for s in _get_global_header(im_frame,\n                                            frame_data['encoderinfo']):\n                    fp.write(s)\n                offset = (0, 0)\n            else:\n                # compress difference\n                frame_data['encoderinfo']['include_color_table'] = True\n\n                im_frame = im_frame.crop(frame_data['bbox'])\n                offset = frame_data['bbox'][:2]\n            _write_frame_data(fp, im_frame, offset, frame_data['encoderinfo'])\n        return True\n\n\ndef _save_all(im, fp, filename):\n    _save(im, fp, filename, save_all=True)\n\n\ndef _save(im, fp, filename, save_all=False):\n    for k, v in im.info.items():\n        im.encoderinfo.setdefault(k, v)\n    # header\n    try:\n        palette = im.encoderinfo[\"palette\"]\n    except KeyError:\n        palette = None\n        im.encoderinfo[\"optimize\"] = im.encoderinfo.get(\"optimize\", True)\n\n    if not save_all or not _write_multiple_frames(im, fp, palette):\n        _write_single_frame(im, fp, palette)\n\n    fp.write(b\";\")  # end of file\n\n    if hasattr(fp, \"flush\"):\n        fp.flush()\n\n\ndef get_interlace(im):\n    interlace = im.encoderinfo.get(\"interlace\", 1)\n\n    # workaround for @PIL153\n    if min(im.size) < 16:\n        interlace = 0\n\n    return interlace\n\n\ndef _write_local_header(fp, im, offset, flags):\n    transparent_color_exists = False\n    try:\n        transparency = im.encoderinfo[\"transparency\"]\n    except KeyError:\n        pass\n    else:\n        transparency = int(transparency)\n        # optimize the block away if transparent color is not used\n        transparent_color_exists = True\n\n        used_palette_colors = _get_optimize(im, im.encoderinfo)\n        if used_palette_colors is not None:\n            # adjust the transparency index after optimize\n            try:\n                transparency = used_palette_colors.index(transparency)\n            except ValueError:\n                transparent_color_exists = False\n\n    if \"duration\" in im.encoderinfo:\n        duration = int(im.encoderinfo[\"duration\"] / 10)\n    else:\n        duration = 0\n\n    disposal = int(im.encoderinfo.get('disposal', 0))\n\n    if transparent_color_exists or duration != 0 or disposal:\n        packed_flag = 1 if transparent_color_exists else 0\n        packed_flag |= disposal << 2\n        if not transparent_color_exists:\n            transparency = 0\n\n        fp.write(b\"!\" +\n                 o8(249) +                # extension intro\n                 o8(4) +                  # length\n                 o8(packed_flag) +        # packed fields\n                 o16(duration) +          # duration\n                 o8(transparency) +       # transparency index\n                 o8(0))\n\n    if \"comment\" in im.encoderinfo and 1 <= len(im.encoderinfo[\"comment\"]) <= 255:\n        fp.write(b\"!\" +\n                 o8(254) +                # extension intro\n                 o8(len(im.encoderinfo[\"comment\"])) +\n                 im.encoderinfo[\"comment\"] +\n                 o8(0))\n    if \"loop\" in im.encoderinfo:\n        number_of_loops = im.encoderinfo[\"loop\"]\n        fp.write(b\"!\" +\n                 o8(255) +                # extension intro\n                 o8(11) +\n                 b\"NETSCAPE2.0\" +\n                 o8(3) +\n                 o8(1) +\n                 o16(number_of_loops) +   # number of loops\n                 o8(0))\n    include_color_table = im.encoderinfo.get('include_color_table')\n    if include_color_table:\n        palette = im.encoderinfo.get(\"palette\", None)\n        palette_bytes = _get_palette_bytes(im)\n        color_table_size = _get_color_table_size(palette_bytes)\n        if color_table_size:\n            flags = flags | 128               # local color table flag\n            flags = flags | color_table_size\n\n    fp.write(b\",\" +\n             o16(offset[0]) +             # offset\n             o16(offset[1]) +\n             o16(im.size[0]) +            # size\n             o16(im.size[1]) +\n             o8(flags))                   # flags\n    if include_color_table and color_table_size:\n        fp.write(_get_header_palette(palette_bytes))\n    fp.write(o8(8))                       # bits\n\n\ndef _save_netpbm(im, fp, filename):\n\n    # Unused by default.\n    # To use, uncomment the register_save call at the end of the file.\n    #\n    # If you need real GIF compression and/or RGB quantization, you\n    # can use the external NETPBM/PBMPLUS utilities.  See comments\n    # below for information on how to enable this.\n\n    import os\n    from subprocess import Popen, check_call, PIPE, CalledProcessError\n    file = im._dump()\n\n    with open(filename, 'wb') as f:\n        if im.mode != \"RGB\":\n            with open(os.devnull, 'wb') as devnull:\n                check_call([\"ppmtogif\", file], stdout=f, stderr=devnull)\n        else:\n            # Pipe ppmquant output into ppmtogif\n            # \"ppmquant 256 %s | ppmtogif > %s\" % (file, filename)\n            quant_cmd = [\"ppmquant\", \"256\", file]\n            togif_cmd = [\"ppmtogif\"]\n            with open(os.devnull, 'wb') as devnull:\n                quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=devnull)\n                togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout,\n                                   stdout=f, stderr=devnull)\n\n            # Allow ppmquant to receive SIGPIPE if ppmtogif exits\n            quant_proc.stdout.close()\n\n            retcode = quant_proc.wait()\n            if retcode:\n                raise CalledProcessError(retcode, quant_cmd)\n\n            retcode = togif_proc.wait()\n            if retcode:\n                raise CalledProcessError(retcode, togif_cmd)\n\n    try:\n        os.unlink(file)\n    except OSError:\n        pass\n\n\n# Force optimization so that we can test performance against\n# cases where it took lots of memory and time previously.\n_FORCE_OPTIMIZE = False\n\n\ndef _get_optimize(im, info):\n    \"\"\"\n    Palette optimization is a potentially expensive operation.\n\n    This function determines if the palette should be optimized using\n    some heuristics, then returns the list of palette entries in use.\n\n    :param im: Image object\n    :param info: encoderinfo\n    :returns: list of indexes of palette entries in use, or None\n    \"\"\"\n    if im.mode in (\"P\", \"L\") and info and info.get(\"optimize\", 0):\n        # Potentially expensive operation.\n\n        # The palette saves 3 bytes per color not used, but palette\n        # lengths are restricted to 3*(2**N) bytes. Max saving would\n        # be 768 -> 6 bytes if we went all the way down to 2 colors.\n        # * If we're over 128 colors, we can't save any space.\n        # * If there aren't any holes, it's not worth collapsing.\n        # * If we have a 'large' image, the palette is in the noise.\n\n        # create the new palette if not every color is used\n        optimise = _FORCE_OPTIMIZE or im.mode == 'L'\n        if optimise or im.width * im.height < 512 * 512:\n            # check which colors are used\n            used_palette_colors = []\n            for i, count in enumerate(im.histogram()):\n                if count:\n                    used_palette_colors.append(i)\n\n            if optimise or (len(used_palette_colors) <= 128 and\n               max(used_palette_colors) > len(used_palette_colors)):\n                return used_palette_colors\n\n\ndef _get_color_table_size(palette_bytes):\n    # calculate the palette size for the header\n    import math\n    color_table_size = int(math.ceil(math.log(len(palette_bytes)//3, 2)))-1\n    if color_table_size < 0:\n        color_table_size = 0\n    return color_table_size\n\n\ndef _get_header_palette(palette_bytes):\n    \"\"\"\n    Returns the palette, null padded to the next power of 2 (*3) bytes\n    suitable for direct inclusion in the GIF header\n\n    :param palette_bytes: Unpadded palette bytes, in RGBRGB form\n    :returns: Null padded palette\n    \"\"\"\n    color_table_size = _get_color_table_size(palette_bytes)\n\n    # add the missing amount of bytes\n    # the palette has to be 2<<n in size\n    actual_target_size_diff = (2 << color_table_size) - len(palette_bytes)//3\n    if actual_target_size_diff > 0:\n        palette_bytes += o8(0) * 3 * actual_target_size_diff\n    return palette_bytes\n\n\ndef _get_palette_bytes(im):\n    \"\"\"\n    Gets the palette for inclusion in the gif header\n\n    :param im: Image object\n    :returns: Bytes, len<=768 suitable for inclusion in gif header\n    \"\"\"\n    return im.palette.palette\n\n\ndef _get_global_header(im, info):\n    \"\"\"Return a list of strings representing a GIF header\"\"\"\n\n    # Header Block\n    # http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp\n\n    version = b\"87a\"\n    for extensionKey in [\"transparency\", \"duration\", \"loop\", \"comment\"]:\n        if info and extensionKey in info:\n            if ((extensionKey == \"duration\" and info[extensionKey] == 0) or\n               (extensionKey == \"comment\" and not (1 <= len(info[extensionKey]) <= 255))):\n                continue\n            version = b\"89a\"\n            break\n    else:\n        if im.info.get(\"version\") == b\"89a\":\n            version = b\"89a\"\n\n    palette_bytes = _get_palette_bytes(im)\n    color_table_size = _get_color_table_size(palette_bytes)\n\n    background = info[\"background\"] if \"background\" in info else 0\n\n    return [\n        b\"GIF\"+version +               # signature + version\n        o16(im.size[0]) +              # canvas width\n        o16(im.size[1]),               # canvas height\n\n        # Logical Screen Descriptor\n        # size of global color table + global color table flag\n        o8(color_table_size + 128),   # packed fields\n        # background + reserved/aspect\n        o8(background) + o8(0),\n\n        # Global Color Table\n        _get_header_palette(palette_bytes)\n    ]\n\n\ndef _write_frame_data(fp, im_frame, offset, params):\n    try:\n        im_frame.encoderinfo = params\n\n        # local image header\n        _write_local_header(fp, im_frame, offset, 0)\n\n        ImageFile._save(im_frame, fp, [(\"gif\", (0, 0)+im_frame.size, 0,\n                                        RAWMODE[im_frame.mode])])\n\n        fp.write(b\"\\0\")  # end of image data\n    finally:\n        del im_frame.encoderinfo\n\n# --------------------------------------------------------------------\n# Legacy GIF utilities\n\n\ndef getheader(im, palette=None, info=None):\n    \"\"\"\n    Legacy Method to get Gif data from image.\n\n    Warning:: May modify image data.\n\n    :param im: Image object\n    :param palette: bytes object containing the source palette, or ....\n    :param info: encoderinfo\n    :returns: tuple of(list of header items, optimized palette)\n\n    \"\"\"\n    used_palette_colors = _get_optimize(im, info)\n\n    if info is None:\n        info = {}\n\n    if \"background\" not in info and \"background\" in im.info:\n        info[\"background\"] = im.info[\"background\"]\n\n    im_mod = _normalize_palette(im, palette, info)\n    im.palette = im_mod.palette\n    im.im = im_mod.im\n    header = _get_global_header(im, info)\n\n    return header, used_palette_colors\n\n\n# To specify duration, add the time in milliseconds to getdata(),\n# e.g. getdata(im_frame, duration=1000)\ndef getdata(im, offset=(0, 0), **params):\n    \"\"\"\n    Legacy Method\n\n    Return a list of strings representing this image.\n    The first string is a local image header, the rest contains\n    encoded image data.\n\n    :param im: Image object\n    :param offset: Tuple of (x, y) pixels. Defaults to (0,0)\n    :param \\\\**params: E.g. duration or other encoder info parameters\n    :returns: List of Bytes containing gif encoded frame data\n\n    \"\"\"\n    class Collector(object):\n        data = []\n\n        def write(self, data):\n            self.data.append(data)\n\n    im.load()  # make sure raster data is available\n\n    fp = Collector()\n\n    _write_frame_data(fp, im, offset, params)\n\n    return fp.data\n\n\n# --------------------------------------------------------------------\n# Registry\n\nImage.register_open(GifImageFile.format, GifImageFile, _accept)\nImage.register_save(GifImageFile.format, _save)\nImage.register_save_all(GifImageFile.format, _save_all)\nImage.register_extension(GifImageFile.format, \".gif\")\nImage.register_mime(GifImageFile.format, \"image/gif\")\n\n#\n# Uncomment the following line if you wish to use NETPBM/PBMPLUS\n# instead of the built-in \"uncompressed\" GIF encoder\n\n# Image.register_save(GifImageFile.format, _save_netpbm)\n",
            "header": {
                "file": {
                    "path": "src/PIL/GifImagePlugin.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "c01adff88",
                "new_commit_hash": "1bfbb5ffd",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/src/PIL/GifImagePlugin.py"
            },
            "plus_file": {
                "path": "b/src/PIL/GifImagePlugin.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 457,
                            "length": 7
                        },
                        "new_range": {
                            "start": 457,
                            "length": 8
                        },
                        "section": "def _save_all(im, fp, filename):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "def _save(im, fp, filename, save_all=False):",
                                "type": "context"
                            },
                            {
                                "content": "    im.encoderinfo.update(im.info)",
                                "type": "deleted"
                            },
                            {
                                "content": "    for k, v in im.info.items():",
                                "type": "added"
                            },
                            {
                                "content": "        im.encoderinfo.setdefault(k, v)",
                                "type": "added"
                            },
                            {
                                "content": "    # header",
                                "type": "context"
                            },
                            {
                                "content": "    try:",
                                "type": "context"
                            },
                            {
                                "content": "        palette = im.encoderinfo[\"palette\"]",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "src/PIL/GifImagePlugin.py",
                            "type": "function",
                            "name": "_save",
                            "content": "def _save(im, fp, filename, save_all=False):\n    im.encoderinfo.update(im.info)\n    # header\n    try:\n        palette = im.encoderinfo[\"palette\"]\n    except KeyError:\n        palette = None\n        im.encoderinfo[\"optimize\"] = im.encoderinfo.get(\"optimize\", True)\n\n    if not save_all or not _write_multiple_frames(im, fp, palette):\n        _write_single_frame(im, fp, palette)\n\n    fp.write(b\";\")  # end of file\n\n    if hasattr(fp, \"flush\"):\n        fp.flush()",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 459,
                            "end_lineno": 474,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "05c78a13e73d98befcffdfb351ac4dbad5e60b2e^",
    "new_commit_hash": "05c78a13e73d98befcffdfb351ac4dbad5e60b2e",
    "commit_message": "Changed encoderinfo to have priority over info when saving GIF images",
    "commit_date": "2018-04-11T08:57:31+10:00",
    "metadata": {}
}
{
    "repo_name": "pillow",
    "new_commit_hash": "05c78a13e73d98befcffdfb351ac4dbad5e60b2e",
    "test_file_codes": [
        "from helper import unittest, PillowTestCase, hopper, netpbm_available\n\nfrom PIL import Image, ImagePalette, GifImagePlugin\n\nfrom io import BytesIO\n\ncodecs = dir(Image.core)\n\n# sample gif stream\nTEST_GIF = \"Tests/images/hopper.gif\"\n\nwith open(TEST_GIF, \"rb\") as f:\n    data = f.read()\n\n\nclass TestFileGif(PillowTestCase):\n\n    def setUp(self):\n        if \"gif_encoder\" not in codecs or \"gif_decoder\" not in codecs:\n            self.skipTest(\"gif support not available\")  # can this happen?\n\n    def test_sanity(self):\n        im = Image.open(TEST_GIF)\n        im.load()\n        self.assertEqual(im.mode, \"P\")\n        self.assertEqual(im.size, (128, 128))\n        self.assertEqual(im.format, \"GIF\")\n        self.assertEqual(im.info[\"version\"], b\"GIF89a\")\n\n    def test_invalid_file(self):\n        invalid_file = \"Tests/images/flower.jpg\"\n\n        self.assertRaises(SyntaxError,\n                          GifImagePlugin.GifImageFile, invalid_file)\n\n    def test_optimize(self):\n        def test_grayscale(optimize):\n            im = Image.new(\"L\", (1, 1), 0)\n            filename = BytesIO()\n            im.save(filename, \"GIF\", optimize=optimize)\n            return len(filename.getvalue())\n\n        def test_bilevel(optimize):\n            im = Image.new(\"1\", (1, 1), 0)\n            test_file = BytesIO()\n            im.save(test_file, \"GIF\", optimize=optimize)\n            return len(test_file.getvalue())\n\n        self.assertEqual(test_grayscale(0), 800)\n        self.assertEqual(test_grayscale(1), 38)\n        self.assertEqual(test_bilevel(0), 800)\n        self.assertEqual(test_bilevel(1), 800)\n\n    def test_optimize_correctness(self):\n        # 256 color Palette image, posterize to > 128 and < 128 levels\n        # Size bigger and smaller than 512x512\n        # Check the palette for number of colors allocated.\n        # Check for correctness after conversion back to RGB\n        def check(colors, size, expected_palette_length):\n            # make an image with empty colors in the start of the palette range\n            im = Image.frombytes('P', (colors, colors),\n                                 bytes(bytearray(range(256-colors, 256))*colors))\n            im = im.resize((size, size))\n            outfile = BytesIO()\n            im.save(outfile, 'GIF')\n            outfile.seek(0)\n            reloaded = Image.open(outfile)\n\n            # check palette length\n            palette_length = max(i+1 for i, v in enumerate(reloaded.histogram()) if v)\n            self.assertEqual(expected_palette_length, palette_length)\n\n            self.assert_image_equal(im.convert('RGB'), reloaded.convert('RGB'))\n\n        # These do optimize the palette\n        check(128, 511, 128)\n        check(64, 511, 64)\n        check(4, 511, 4)\n\n        # These don't optimize the palette\n        check(128, 513, 256)\n        check(64, 513, 256)\n        check(4, 513, 256)\n\n        # other limits that don't optimize the palette\n        check(129, 511, 256)\n        check(255, 511, 256)\n        check(256, 511, 256)\n\n    def test_optimize_full_l(self):\n        im = Image.frombytes(\"L\", (16, 16), bytes(bytearray(range(256))))\n        test_file = BytesIO()\n        im.save(test_file, \"GIF\", optimize=True)\n        self.assertEqual(im.mode, \"L\")\n\n    def test_roundtrip(self):\n        out = self.tempfile('temp.gif')\n        im = hopper()\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), im, 50)\n\n    def test_roundtrip2(self):\n        # see https://github.com/python-pillow/Pillow/issues/403\n        out = self.tempfile('temp.gif')\n        im = Image.open(TEST_GIF)\n        im2 = im.copy()\n        im2.save(out)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), hopper(), 50)\n\n    def test_roundtrip_save_all(self):\n        # Single frame image\n        out = self.tempfile('temp.gif')\n        im = hopper()\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        self.assert_image_similar(reread.convert('RGB'), im, 50)\n\n        # Multiframe image\n        im = Image.open(\"Tests/images/dispose_bgnd.gif\")\n\n        out = self.tempfile('temp.gif')\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.n_frames, 5)\n\n    def test_headers_saving_for_animated_gifs(self):\n        important_headers = ['background', 'version', 'duration', 'loop']\n        # Multiframe image\n        im = Image.open(\"Tests/images/dispose_bgnd.gif\")\n\n        out = self.tempfile('temp.gif')\n        im.save(out, save_all=True)\n        reread = Image.open(out)\n\n        for header in important_headers:\n            self.assertEqual(\n                im.info[header],\n                reread.info[header]\n            )\n\n    def test_palette_handling(self):\n        # see https://github.com/python-pillow/Pillow/issues/513\n\n        im = Image.open(TEST_GIF)\n        im = im.convert('RGB')\n\n        im = im.resize((100, 100), Image.LANCZOS)\n        im2 = im.convert('P', palette=Image.ADAPTIVE, colors=256)\n\n        f = self.tempfile('temp.gif')\n        im2.save(f, optimize=True)\n\n        reloaded = Image.open(f)\n\n        self.assert_image_similar(im, reloaded.convert('RGB'), 10)\n\n    def test_palette_434(self):\n        # see https://github.com/python-pillow/Pillow/issues/434\n\n        def roundtrip(im, *args, **kwargs):\n            out = self.tempfile('temp.gif')\n            im.copy().save(out, *args, **kwargs)\n            reloaded = Image.open(out)\n\n            return reloaded\n\n        orig = \"Tests/images/test.colors.gif\"\n        im = Image.open(orig)\n\n        self.assert_image_similar(im, roundtrip(im), 1)\n        self.assert_image_similar(im, roundtrip(im, optimize=True), 1)\n\n        im = im.convert(\"RGB\")\n        # check automatic P conversion\n        reloaded = roundtrip(im).convert('RGB')\n        self.assert_image_equal(im, reloaded)\n\n    @unittest.skipUnless(netpbm_available(), \"netpbm not available\")\n    def test_save_netpbm_bmp_mode(self):\n        img = Image.open(TEST_GIF).convert(\"RGB\")\n\n        tempfile = self.tempfile(\"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        self.assert_image_similar(img, Image.open(tempfile).convert(\"RGB\"), 0)\n\n    @unittest.skipUnless(netpbm_available(), \"netpbm not available\")\n    def test_save_netpbm_l_mode(self):\n        img = Image.open(TEST_GIF).convert(\"L\")\n\n        tempfile = self.tempfile(\"temp.gif\")\n        GifImagePlugin._save_netpbm(img, 0, tempfile)\n        self.assert_image_similar(img, Image.open(tempfile).convert(\"L\"), 0)\n\n    def test_seek(self):\n        img = Image.open(\"Tests/images/dispose_none.gif\")\n        framecount = 0\n        try:\n            while True:\n                framecount += 1\n                img.seek(img.tell() + 1)\n        except EOFError:\n            self.assertEqual(framecount, 5)\n\n    def test_n_frames(self):\n        for path, n_frames in [\n            [TEST_GIF, 1],\n            ['Tests/images/iss634.gif', 42]\n        ]:\n            # Test is_animated before n_frames\n            im = Image.open(path)\n            self.assertEqual(im.is_animated, n_frames != 1)\n\n            # Test is_animated after n_frames\n            im = Image.open(path)\n            self.assertEqual(im.n_frames, n_frames)\n            self.assertEqual(im.is_animated, n_frames != 1)\n\n    def test_eoferror(self):\n        im = Image.open(TEST_GIF)\n        n_frames = im.n_frames\n\n        # Test seeking past the last frame\n        self.assertRaises(EOFError, im.seek, n_frames)\n        self.assertLess(im.tell(), n_frames)\n\n        # Test that seeking to the last frame does not raise an error\n        im.seek(n_frames-1)\n\n    def test_dispose_none(self):\n        img = Image.open(\"Tests/images/dispose_none.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 1)\n        except EOFError:\n            pass\n\n    def test_dispose_background(self):\n        img = Image.open(\"Tests/images/dispose_bgnd.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 2)\n        except EOFError:\n            pass\n\n    def test_dispose_previous(self):\n        img = Image.open(\"Tests/images/dispose_prev.gif\")\n        try:\n            while True:\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, 3)\n        except EOFError:\n            pass\n\n    def test_save_dispose(self):\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111'),\n            Image.new('L', (100, 100), '#222'),\n        ]\n        for method in range(0, 4):\n            im_list[0].save(\n                out,\n                save_all=True,\n                append_images=im_list[1:],\n                disposal=method\n            )\n            img = Image.open(out)\n            for _ in range(2):\n                img.seek(img.tell() + 1)\n                self.assertEqual(img.disposal_method, method)\n\n        # check per frame disposal\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            disposal=tuple(range(len(im_list)))\n            )\n\n        img = Image.open(out)\n\n        for i in range(2):\n            img.seek(img.tell() + 1)\n            self.assertEqual(img.disposal_method, i+1)\n\n    def test_iss634(self):\n        img = Image.open(\"Tests/images/iss634.gif\")\n        # seek to the second frame\n        img.seek(img.tell() + 1)\n        # all transparent pixels should be replaced with the color from the\n        # first frame\n        self.assertEqual(img.histogram()[img.info['transparency']], 0)\n\n    def test_duration(self):\n        duration = 1000\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n\n        # Check that the argument has priority over the info settings\n        im.info['duration'] = 100\n        im.save(out, duration=duration)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.info['duration'], duration)\n\n    def test_multiple_duration(self):\n        duration_list = [1000, 2000, 3000]\n\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111'),\n            Image.new('L', (100, 100), '#222')\n        ]\n\n        # duration as list\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=duration_list\n        )\n        reread = Image.open(out)\n\n        for duration in duration_list:\n            self.assertEqual(reread.info['duration'], duration)\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n        # duration as tuple\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=tuple(duration_list)\n        )\n        reread = Image.open(out)\n\n        for duration in duration_list:\n            self.assertEqual(reread.info['duration'], duration)\n            try:\n                reread.seek(reread.tell() + 1)\n            except EOFError:\n                pass\n\n    def test_identical_frames(self):\n        duration_list = [1000, 1500, 2000, 4000]\n\n        out = self.tempfile('temp.gif')\n        im_list = [\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#000'),\n            Image.new('L', (100, 100), '#111')\n        ]\n\n        # duration as list\n        im_list[0].save(\n            out,\n            save_all=True,\n            append_images=im_list[1:],\n            duration=duration_list\n        )\n        reread = Image.open(out)\n\n        # Assert that the first three frames were combined\n        self.assertEqual(reread.n_frames, 2)\n\n        # Assert that the new duration is the total of the identical frames\n        self.assertEqual(reread.info['duration'], 4500)\n\n    def test_number_of_loops(self):\n        number_of_loops = 2\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.save(out, loop=number_of_loops)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['loop'], number_of_loops)\n\n    def test_background(self):\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.info['background'] = 1\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['background'], im.info['background'])\n\n    def test_comment(self):\n        im = Image.open(TEST_GIF)\n        self.assertEqual(im.info['comment'], b\"File written by Adobe Photoshop\\xa8 4.0\")\n\n        out = self.tempfile('temp.gif')\n        im = Image.new('L', (100, 100), '#000')\n        im.info['comment'] = b\"Test comment text\"\n        im.save(out)\n        reread = Image.open(out)\n\n        self.assertEqual(reread.info['comment'], im.info['comment'])\n\n    def test_version(self):\n        out = self.tempfile('temp.gif')\n\n        def assertVersionAfterSave(im, version):\n            im.save(out)\n            reread = Image.open(out)\n            self.assertEqual(reread.info[\"version\"], version)\n\n        # Test that GIF87a is used by default\n        im = Image.new('L', (100, 100), '#000')\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n        # Test setting the version to 89a\n        im = Image.new('L', (100, 100), '#000')\n        im.info[\"version\"] = b\"89a\"\n        assertVersionAfterSave(im, b\"GIF89a\")\n\n        # Test that adding a GIF89a feature changes the version\n        im.info[\"transparency\"] = 1\n        assertVersionAfterSave(im, b\"GIF89a\")\n\n        # Test that a GIF87a image is also saved in that format\n        im = Image.open(\"Tests/images/test.colors.gif\")\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n        # Test that a GIF89a image is also saved in that format\n        im.info[\"version\"] = b\"GIF89a\"\n        assertVersionAfterSave(im, b\"GIF87a\")\n\n    def test_append_images(self):\n        out = self.tempfile('temp.gif')\n\n        # Test appending single frame images\n        im = Image.new('RGB', (100, 100), '#f00')\n        ims = [Image.new('RGB', (100, 100), color) for color\n               in ['#0f0', '#00f']]\n        im.copy().save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 3)\n\n        # Tests appending using a generator\n        def imGenerator(ims):\n            for im in ims:\n                yield im\n        im.save(out, save_all=True, append_images=imGenerator(ims))\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 3)\n\n        # Tests appending single and multiple frame images\n        im = Image.open(\"Tests/images/dispose_none.gif\")\n        ims = [Image.open(\"Tests/images/dispose_prev.gif\")]\n        im.save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 10)\n\n    def test_transparent_optimize(self):\n        # from issue #2195, if the transparent color is incorrectly\n        # optimized out, gif loses transparency\n        # Need a palette that isn't using the 0 color, and one\n        # that's > 128 items where the transparent color is actually\n        # the top palette entry to trigger the bug.\n\n        data = bytes(bytearray(range(1, 254)))\n        palette = ImagePalette.ImagePalette(\"RGB\", list(range(256))*3)\n\n        im = Image.new('L', (253, 1))\n        im.frombytes(data)\n        im.putpalette(palette)\n\n        out = self.tempfile('temp.gif')\n        im.save(out, transparency=253)\n        reloaded = Image.open(out)\n\n        self.assertEqual(reloaded.info['transparency'], 253)\n\n    def test_bbox(self):\n        out = self.tempfile('temp.gif')\n\n        im = Image.new('RGB', (100, 100), '#fff')\n        ims = [Image.new(\"RGB\", (100, 100), '#000')]\n        im.save(out, save_all=True, append_images=ims)\n\n        reread = Image.open(out)\n        self.assertEqual(reread.n_frames, 2)\n\n    def test_palette_save_L(self):\n        # generate an L mode image with a separate palette\n\n        im = hopper('P')\n        im_l = Image.frombytes('L', im.size, im.tobytes())\n        palette = bytes(bytearray(im.getpalette()))\n\n        out = self.tempfile('temp.gif')\n        im_l.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n\n        self.assert_image_equal(reloaded.convert('RGB'), im.convert('RGB'))\n\n    def test_palette_save_P(self):\n        # pass in a different palette, then construct what the image\n        # would look like.\n        # Forcing a non-straight grayscale palette.\n\n        im = hopper('P')\n        palette = bytes(bytearray([255-i//3 for i in range(768)]))\n\n        out = self.tempfile('temp.gif')\n        im.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n        im.putpalette(palette)\n        self.assert_image_equal(reloaded, im)\n\n    def test_palette_save_ImagePalette(self):\n        # pass in a different palette, as an ImagePalette.ImagePalette\n        # effectively the same as test_palette_save_P\n\n        im = hopper('P')\n        palette = ImagePalette.ImagePalette('RGB', list(range(256))[::-1]*3)\n\n        out = self.tempfile('temp.gif')\n        im.save(out, palette=palette)\n\n        reloaded = Image.open(out)\n        im.putpalette(palette)\n        self.assert_image_equal(reloaded, im)\n\n    def test_save_I(self):\n        # Test saving something that would trigger the auto-convert to 'L'\n\n        im = hopper('I')\n\n        out = self.tempfile('temp.gif')\n        im.save(out)\n\n        reloaded = Image.open(out)\n        self.assert_image_equal(reloaded.convert('L'), im.convert('L'))\n\n    def test_getdata(self):\n        # test getheader/getdata against legacy values\n        # Create a 'P' image with holes in the palette\n        im = Image._wedge().resize((16, 16))\n        im.putpalette(ImagePalette.ImagePalette('RGB'))\n        im.info = {'background': 0}\n\n        passed_palette = bytes(bytearray([255-i//3 for i in range(768)]))\n\n        GifImagePlugin._FORCE_OPTIMIZE = True\n        try:\n            h = GifImagePlugin.getheader(im, passed_palette)\n            d = GifImagePlugin.getdata(im)\n\n            import pickle\n            # Enable to get target values on pre-refactor version\n            # with open('Tests/images/gif_header_data.pkl', 'wb') as f:\n            #    pickle.dump((h, d), f, 1)\n            with open('Tests/images/gif_header_data.pkl', 'rb') as f:\n                (h_target, d_target) = pickle.load(f)\n\n            self.assertEqual(h, h_target)\n            self.assertEqual(d, d_target)\n        finally:\n            GifImagePlugin._FORCE_OPTIMIZE = False\n\n    def test_lzw_bits(self):\n        # see https://github.com/python-pillow/Pillow/issues/2811\n        im = Image.open('Tests/images/issue_2811.gif')\n\n        self.assertEqual(im.tile[0][3][0], 11)  # LZW bits\n        # codec error prepatch\n        im.load()\n\n\nif __name__ == '__main__':\n    unittest.main()\n",
        "#!/usr/bin/env python\nimport unittest\nimport sys\nimport time\nimport traceback\nimport platform\nimport os\n\n\nclass PytestLikeResult(unittest.TestResult):\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.start_time = None\n        # Store (test, outcome, info) for each test:\n        #   outcome ∈ { \"passed\", \"failed\", \"error\", \"skipped\" }\n        #   info can hold the exception or skip reason, etc.\n        self.results = []\n\n    def startTestRun(self):\n        \"\"\"Called once before all tests.\"\"\"\n        self.start_time = time.time()\n        # Print a header similar to pytest\n        print(\"=\" * 29 + \" test session starts \" + \"=\" * 29)\n        # You can customize the platform/versions/paths as you like:\n        print(\n            f\"platform {platform.system().lower()} -- Python {platform.python_version()}\"\n        )\n        print(f\"rootdir: {os.path.abspath(os.curdir)}\")\n\n    def stopTestRun(self):\n        \"\"\"Called once after all tests are done.\"\"\"\n        elapsed = time.time() - self.start_time\n        print()  # blank line after final dot\n\n        # Summarize\n        ntests = len(self.results)\n        print(f\"collected {ntests} items\")\n\n        # Show details of failures/errors\n        failures = [r for r in self.results if r[1] == \"failed\"]\n        errors = [r for r in self.results if r[1] == \"error\"]\n\n        if failures:\n            print(\"\\n\" + \"=\" * 35 + \" FAILURES \" + \"=\" * 35)\n            for test, outcome, err_info in failures:\n                test = test.split(\"::\")\n                if len(test) == 3:\n                    test = f\"{test[1]}.{test[2]}\"\n                else:\n                    test = f\"{test[1]}\"\n                print(f\"__________ {test} __________\")\n                self._print_traceback(err_info)\n        if errors:\n            print(\"\\n\" + \"=\" * 35 + \" ERRORS \" + \"=\" * 37)\n            for test, outcome, err_info in errors:\n                test = test.split(\"::\")\n                if len(test) == 3:\n                    test = f\"{test[1]}.{test[2]}\"\n                else:\n                    test = f\"{test[1]}\"\n                print(f\"__________ {test} __________\")\n                self._print_traceback(err_info)\n\n        # Optional short summary lines for passes/fails/skips\n        print(\"\\n==================== short test summary info ====================\")\n        for test, outcome, info in self.results:\n            if outcome in (\"passed\", \"failed\", \"error\", \"skipped\"):\n                print(f\"{outcome.upper()} {test}\")\n\n        # Final one-line summary: “1 failed, 1 errors, 10 passed in 2.04s”\n        passed_count = sum(1 for r in self.results if r[1] == \"passed\")\n        failed_count = len(failures)\n        error_count = len(errors)\n        skipped_count = sum(1 for r in self.results if r[1] == \"skipped\")\n\n        summary_bits = []\n        if failed_count:\n            summary_bits.append(f\"{failed_count} failed\")\n        if error_count:\n            summary_bits.append(f\"{error_count} error{'s' if error_count != 1 else ''}\")\n        if passed_count:\n            summary_bits.append(f\"{passed_count} passed\")\n        if skipped_count:\n            summary_bits.append(f\"{skipped_count} skipped\")\n\n        summary_str = \", \".join(summary_bits) if summary_bits else \"no tests run\"\n        print(\n            f\"=================== {summary_str} in {elapsed:.2f}s ===================\"\n        )\n\n    def startTest(self, test):\n        \"\"\"Called right before each test method.\"\"\"\n        super().startTest(test)\n        # Print a dot or some indicator\n        print(\".\", end=\"\", flush=True)\n\n    def addSuccess(self, test):\n        super().addSuccess(test)\n        self.results.append((self._test_id(test), \"passed\", None))\n\n    def addFailure(self, test, err):\n        super().addFailure(test, err)\n        self.results.append((self._test_id(test), \"failed\", err))\n\n    def addError(self, test, err):\n        super().addError(test, err)\n        self.results.append((self._test_id(test), \"error\", err))\n\n    def addSkip(self, test, reason):\n        super().addSkip(test, reason)\n        self.results.append((self._test_id(test), \"skipped\", reason))\n\n    def _print_traceback(self, err_info):\n        \"\"\"Helper to print the traceback like pytest does.\"\"\"\n        if isinstance(err_info, tuple) and len(err_info) == 3:\n            exc_type, exc_value, tb = err_info\n            traceback.print_exception(exc_type, exc_value, tb, file=sys.stdout)\n        else:\n            print(str(err_info))\n\n    def _test_id(self, test):\n        try:\n            return f\"{test.__class__.__module__}::{test.__class__.__name__}::{test._testMethodName}\"\n        except AttributeError:\n            # Fallback for _ErrorHolder objects\n            return f\"{test.__class__.__module__}::{test.__class__.__name__}\"\n\n\nclass PytestLikeRunner(unittest.TextTestRunner):\n    \"\"\"A custom TextTestRunner that uses the PytestLikeResult.\"\"\"\n\n    resultclass = PytestLikeResult\n\n    def run(self, test):\n        result = self._makeResult()\n        # Overriding run to ensure we call startTestRun/stopTestRun\n        result.startTestRun()\n        test(result)\n        result.stopTestRun()\n        return result\n\n\ndef main():\n    loader = unittest.TestLoader()\n    suite = loader.discover(\"r2e_tests\")  # discover tests in r2e_tests/\n    runner = PytestLikeRunner(verbosity=0)\n    result = runner.run(suite)\n    # Exit with code 0 if all tests passed/skipped, 1 if there were fails/errors\n    sys.exit(not result.wasSuccessful())\n\n\nif __name__ == \"__main__\":\n    main()\n"
    ],
    "test_file_names": [
        "test_1.py",
        "unittest_custom_runner.py"
    ],
    "setup_res_code": 0,
    "setup_res_stdout": "Starting Pillow installation attempts...\nVerifying Pillow installation...\n✅ Pillow installation successful!\nSuccessfully installed Pillow\n",
    "setup_res_stderr": "Using CPython 3.9.21\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 12 packages in 11ms\nInstalled 12 packages in 218ms\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 835ms\nPrepared 1 package in 16.34s\nInstalled 1 package in 0.93ms\n + pillow==5.1.0 (from file:///home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_05c78a13e73d98befcffdfb351ac4dbad5e60b2e)\n",
    "new_commit_res_code": 0,
    "new_commit_res_stdout": "============================= test session starts =============================\nplatform linux -- Python 3.9.21\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_05c78a13e73d98befcffdfb351ac4dbad5e60b2e\n.....................................\ncollected 37 items\n\n==================== short test summary info ====================\nPASSED test_1::TestFileGif::test_append_images\nPASSED test_1::TestFileGif::test_background\nPASSED test_1::TestFileGif::test_bbox\nPASSED test_1::TestFileGif::test_comment\nPASSED test_1::TestFileGif::test_dispose_background\nPASSED test_1::TestFileGif::test_dispose_none\nPASSED test_1::TestFileGif::test_dispose_previous\nPASSED test_1::TestFileGif::test_duration\nPASSED test_1::TestFileGif::test_eoferror\nPASSED test_1::TestFileGif::test_getdata\nPASSED test_1::TestFileGif::test_headers_saving_for_animated_gifs\nPASSED test_1::TestFileGif::test_identical_frames\nPASSED test_1::TestFileGif::test_invalid_file\nPASSED test_1::TestFileGif::test_iss634\nPASSED test_1::TestFileGif::test_lzw_bits\nPASSED test_1::TestFileGif::test_multiple_duration\nPASSED test_1::TestFileGif::test_n_frames\nPASSED test_1::TestFileGif::test_number_of_loops\nPASSED test_1::TestFileGif::test_optimize\nPASSED test_1::TestFileGif::test_optimize_correctness\nPASSED test_1::TestFileGif::test_optimize_full_l\nPASSED test_1::TestFileGif::test_palette_434\nPASSED test_1::TestFileGif::test_palette_handling\nPASSED test_1::TestFileGif::test_palette_save_ImagePalette\nPASSED test_1::TestFileGif::test_palette_save_L\nPASSED test_1::TestFileGif::test_palette_save_P\nPASSED test_1::TestFileGif::test_roundtrip\nPASSED test_1::TestFileGif::test_roundtrip2\nPASSED test_1::TestFileGif::test_roundtrip_save_all\nPASSED test_1::TestFileGif::test_sanity\nPASSED test_1::TestFileGif::test_save_I\nPASSED test_1::TestFileGif::test_save_dispose\nSKIPPED test_1::TestFileGif::test_save_netpbm_bmp_mode\nSKIPPED test_1::TestFileGif::test_save_netpbm_l_mode\nPASSED test_1::TestFileGif::test_seek\nPASSED test_1::TestFileGif::test_transparent_optimize\nPASSED test_1::TestFileGif::test_version\n=================== 35 passed, 2 skipped in 0.40s ===================\n",
    "new_commit_res_stderr": "",
    "old_commit_res_code": 1,
    "old_commit_res_stdout": "============================= test session starts =============================\nplatform linux -- Python 3.9.21\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_05c78a13e73d98befcffdfb351ac4dbad5e60b2e\n...........=== orphaned temp file: /tmp/tempbu6__t9j.gif\n.=== orphaned temp file: /tmp/tempk7tus24i.gif\n....=== orphaned temp file: /tmp/temp2oxy4p86.gif\n..=== orphaned temp file: /tmp/temp3ftepvc5.gif\n....=== orphaned temp file: /tmp/temp23msrlv1.gif\n=== orphaned temp file: /tmp/temp9tc3did7.gif\n=== orphaned temp file: /tmp/temph35ax0tr.gif\n.=== orphaned temp file: /tmp/tempkvxxsm4_.gif\n.=== orphaned temp file: /tmp/temp8u9tag99.gif\n.=== orphaned temp file: /tmp/temp12zlwlf1.gif\n.=== orphaned temp file: /tmp/temp6xvuem06.gif\n.=== orphaned temp file: /tmp/temprtt6_gui.gif\n.=== orphaned temp file: /tmp/tempoyak1yd1.gif\n.=== orphaned temp file: /tmp/tempp1sr7621.gif\n=== orphaned temp file: /tmp/tempvd6qj486.gif\n..=== orphaned temp file: /tmp/tempuic2d0m7.gif\n.=== orphaned temp file: /tmp/temp97l63__3.gif\n....=== orphaned temp file: /tmp/temp_poru4wz.gif\n.=== orphaned temp file: /tmp/tempjvf1vi3v.gif\n\ncollected 37 items\n\n=================================== FAILURES ===================================\n__________ TestFileGif.test_duration __________\nTraceback (most recent call last):\n  File \"/home/gcpuser/.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/unittest/case.py\", line 59, in testPartExecutor\n    yield\n  File \"/home/gcpuser/.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/unittest/case.py\", line 592, in run\n    self._callTestMethod(testMethod)\n  File \"/home/gcpuser/.local/share/uv/python/cpython-3.9.21-linux-x86_64-gnu/lib/python3.9/unittest/case.py\", line 550, in _callTestMethod\n    method()\n  File \"/home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_05c78a13e73d98befcffdfb351ac4dbad5e60b2e/r2e_tests/test_1.py\", line 314, in test_duration\n    self.assertEqual(reread.info['duration'], duration)\nAssertionError: 100 != 1000\n\n==================== short test summary info ====================\nPASSED test_1::TestFileGif::test_append_images\nPASSED test_1::TestFileGif::test_background\nPASSED test_1::TestFileGif::test_bbox\nPASSED test_1::TestFileGif::test_comment\nPASSED test_1::TestFileGif::test_dispose_background\nPASSED test_1::TestFileGif::test_dispose_none\nPASSED test_1::TestFileGif::test_dispose_previous\nFAILED test_1::TestFileGif::test_duration\nPASSED test_1::TestFileGif::test_eoferror\nPASSED test_1::TestFileGif::test_getdata\nPASSED test_1::TestFileGif::test_headers_saving_for_animated_gifs\nPASSED test_1::TestFileGif::test_identical_frames\nPASSED test_1::TestFileGif::test_invalid_file\nPASSED test_1::TestFileGif::test_iss634\nPASSED test_1::TestFileGif::test_lzw_bits\nPASSED test_1::TestFileGif::test_multiple_duration\nPASSED test_1::TestFileGif::test_n_frames\nPASSED test_1::TestFileGif::test_number_of_loops\nPASSED test_1::TestFileGif::test_optimize\nPASSED test_1::TestFileGif::test_optimize_correctness\nPASSED test_1::TestFileGif::test_optimize_full_l\nPASSED test_1::TestFileGif::test_palette_434\nPASSED test_1::TestFileGif::test_palette_handling\nPASSED test_1::TestFileGif::test_palette_save_ImagePalette\nPASSED test_1::TestFileGif::test_palette_save_L\nPASSED test_1::TestFileGif::test_palette_save_P\nPASSED test_1::TestFileGif::test_roundtrip\nPASSED test_1::TestFileGif::test_roundtrip2\nPASSED test_1::TestFileGif::test_roundtrip_save_all\nPASSED test_1::TestFileGif::test_sanity\nPASSED test_1::TestFileGif::test_save_I\nPASSED test_1::TestFileGif::test_save_dispose\nSKIPPED test_1::TestFileGif::test_save_netpbm_bmp_mode\nSKIPPED test_1::TestFileGif::test_save_netpbm_l_mode\nPASSED test_1::TestFileGif::test_seek\nPASSED test_1::TestFileGif::test_transparent_optimize\nPASSED test_1::TestFileGif::test_version\n=================== 1 failed, 34 passed, 2 skipped in 0.52s ===================\n",
    "old_commit_res_stderr": ""
}
Tests/test_file_gif.py src/PIL/GifImagePlugin.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** GIF Saving: Duration Parameter Overridden by Image Info

**Description:**
When saving GIF images using PIL, setting the `duration` in the image's `info` dictionary overrides the `duration` parameter provided to the `save` method. This results in the saved GIF using an unintended frame duration.

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

im = Image.new('L', (100, 100), '#000')
im.info['duration'] = 100  # Intended duration
im.save('temp.gif', duration=1000)  # Desired duration parameter
```

**Expected Behavior:**
The saved GIF (`temp.gif`) should have a frame duration of **1000 milliseconds** as specified by the `duration` parameter in the `save` method.

**Actual Behavior:**
The saved GIF (`temp.gif`) has a frame duration of **100 milliseconds**, ignoring the `duration` parameter and using the value from `im.info`.

[/ISSUE]
--- END ISSUE ---

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

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

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

import itertools

__version__ = "0.9"


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

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


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

class GifImageFile(ImageFile.ImageFile):

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

    global_palette = None

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

    def _open(self):

        # Screen
        s = self.fp.read(13)
        if s[:6] not in [b"GIF87a", b"GIF89a"]:
            raise SyntaxError("not a GIF file")

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

        if flags & 128:
            # get global palette
            self.info["background"] = i8(s[11])
            # check if palette contains colour indices
            p = self.fp.read(3 << bits)
            for i in range(0, len(p), 3):
                if not (i//3 == i8(p[i]) == i8(p[i+1]) == i8(p[i+2])):
                    p = ImagePalette.raw("RGB", p)
                    self.global_palette = self.palette = p
                    break

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

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

    @property
    def is_animated(self):
        if self._is_animated is None:
            if self._n_frames is not None:
                self._is_animated = self._n_frames != 1
            else:
                current = self.tell()

                try:
                    self.seek(1)
                    self._is_animated = True
                except EOFError:
                    self._is_animated = False

                self.seek(current)
        return self._is_animated

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

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

    def _seek(self, frame):

        if frame == 0:
            # rewind
            self.__offset = 0
            self.dispose = None
            self.dispose_extent = [0, 0, 0, 0]  # x0, y0, x1, y1
            self.__frame = -1
            self.__fp.seek(self.__rewind)
            self._prev_im = None
            self.disposal_method = 0
        else:
            # ensure that the previous frame was loaded
            if not self.im:
                self.load()

        if frame != self.__frame + 1:
            raise ValueError("cannot seek to frame %d" % frame)
        self.__frame = frame

        self.tile = []

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

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

        from copy import copy
        self.palette = copy(self.global_palette)

        while True:

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

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

                    # disposal method - find the value of bits 4 - 6
                    dispose_bits = 0b00011100 & flags
                    dispose_bits = dispose_bits >> 2
                    if dispose_bits:
                        # only set the dispose if it is not
                        # unspecified. I'm not sure if this is
                        # correct, but it seems to prevent the last
                        # frame from looking odd for some animations
                        self.disposal_method = dispose_bits
                elif i8(s) == 254:
                    #
                    # comment extension
                    #
                    self.info["comment"] = block
                elif i8(s) == 255:
                    #
                    # application extension
                    #
                    self.info["extension"] = block, self.fp.tell()
                    if block[:11] == b"NETSCAPE2.0":
                        block = self.data()
                        if len(block) >= 3 and i8(block[0]) == 1:
                            self.info["loop"] = i16(block[1:3])
                while self.data():
                    pass

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

                # extent
                x0, y0 = i16(s[0:]), i16(s[2:])
                x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:])
                self.dispose_extent = x0, y0, x1, y1
                flags = i8(s[8])

                interlace = (flags & 64) != 0

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

                # image data
                bits = i8(self.fp.read(1))
                self.__offset = self.fp.tell()
                self.tile = [("gif",
                             (x0, y0, x1, y1),
                             self.__offset,
                             (bits, interlace))]
                break

            else:
                pass
                # raise IOError, "illegal GIF tag `%x`" % i8(s)

        try:
            if self.disposal_method < 2:
                # do not dispose or none specified
                self.dispose = None
            elif self.disposal_method == 2:
                # replace with background colour
                self.dispose = Image.core.fill("P", self.size,
                                               self.info["background"])
            else:
                # replace with previous contents
                if self.im:
                    self.dispose = self.im.copy()

            # only dispose the extent in this frame
            if self.dispose:
                self.dispose = self._crop(self.dispose, self.dispose_extent)
        except (AttributeError, KeyError):
            pass

        if not self.tile:
            # self.__fp = None
            raise EOFError

        self.mode = "L"
        if self.palette:
            self.mode = "P"

    def tell(self):
        return self.__frame

    def load_end(self):
        ImageFile.ImageFile.load_end(self)

        # if the disposal method is 'do not dispose', transparent
        # pixels should show the content of the previous frame
        if self._prev_im and self.disposal_method == 1:
            # we do this by pasting the updated area onto the previous
            # frame which we then use as the current image content
            updated = self._crop(self.im, self.dispose_extent)
            self._prev_im.paste(updated, self.dispose_extent,
                                updated.convert('RGBA'))
            self.im = self._prev_im
        self._prev_im = self.im.copy()

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


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


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

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

    UNDONE: What is the point of mucking with the initial call palette, for
    an image that shouldn't have a palette, or it would be a mode 'P' and
    get returned in the RAWMODE clause.

    :param im: Image object
    :param initial_call: Default false, set to true for a single frame.
    :returns: Image object
    """
    if im.mode in RAWMODE:
        im.load()
        return im
    if Image.getmodebase(im.mode) == "RGB":
        if initial_call:
            palette_size = 256
            if im.palette:
                palette_size = len(im.palette.getdata()[1]) // 3
            return im.convert("P", palette=Image.ADAPTIVE, colors=palette_size)
        else:
            return im.convert("P")
    return im.convert("L")


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

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

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

    used_palette_colors = _get_optimize(im, info)
    if used_palette_colors is not None:
        return im.remap_palette(used_palette_colors, source_palette)

    im.palette.palette = source_palette
    return im


def _write_single_frame(im, fp, palette):
    im_out = _normalize_mode(im, True)
    im_out = _normalize_palette(im_out, palette, im.encoderinfo)

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

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

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

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


def _write_multiple_frames(im, fp, palette):

    duration = im.encoderinfo.get("duration", None)
    disposal = im.encoderinfo.get('disposal', None)

    im_frames = []
    frame_count = 0
    for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
        for im_frame in ImageSequence.Iterator(imSequence):
            # a copy is required here since seek can still mutate the image
            im_frame = _normalize_mode(im_frame.copy())
            im_frame = _normalize_palette(im_frame, palette, im.encoderinfo)

            encoderinfo = im.encoderinfo.copy()
            if isinstance(duration, (list, tuple)):
                encoderinfo['duration'] = duration[frame_count]
            if isinstance(disposal, (list, tuple)):
                encoderinfo["disposal"] = disposal[frame_count]
            frame_count += 1

            if im_frames:
                # delta frame
                previous = im_frames[-1]
                if _get_palette_bytes(im_frame) == _get_palette_bytes(previous['im']):
                    delta = ImageChops.subtract_modulo(im_frame,
                                                       previous['im'])
                else:
                    delta = ImageChops.subtract_modulo(im_frame.convert('RGB'),
                                                       previous['im'].convert('RGB'))
                bbox = delta.getbbox()
                if not bbox:
                    # This frame is identical to the previous frame
                    if duration:
                        previous['encoderinfo']['duration'] += encoderinfo['duration']
                    continue
            else:
                bbox = None
            im_frames.append({
                'im': im_frame,
                'bbox': bbox,
                'encoderinfo': encoderinfo
            })

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

                im_frame = im_frame.crop(frame_data['bbox'])
                offset = frame_data['bbox'][:2]
            _write_frame_data(fp, im_frame, offset, frame_data['encoderinfo'])
        return True


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


def _save(im, fp, filename, save_all=False):
    im.encoderinfo.update(im.info)
    # header
    try:
        palette = im.encoderinfo["palette"]
    except KeyError:
        palette = None
        im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)

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

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

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


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

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

    return interlace


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

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

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

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

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

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

    if "comment" in im.encoderinfo and 1 <= len(im.encoderinfo["comment"]) <= 255:
        fp.write(b"!" +
                 o8(254) +                # extension intro
                 o8(len(im.encoderinfo["comment"])) +
                 im.encoderinfo["comment"] +
                 o8(0))
    if "loop" in im.encoderinfo:
        number_of_loops = im.encoderinfo["loop"]
        fp.write(b"!" +
                 o8(255) +                # extension intro
                 o8(11) +
                 b"NETSCAPE2.0" +
                 o8(3) +
                 o8(1) +
                 o16(number_of_loops) +   # number of loops
                 o8(0))
    include_color_table = im.encoderinfo.get('include_color_table')
    if include_color_table:
        palette = im.encoderinfo.get("palette", None)
        palette_bytes = _get_palette_bytes(im)
        color_table_size = _get_color_table_size(palette_bytes)
        if color_table_size:
            flags = flags | 128               # local color table flag
            flags = flags | color_table_size

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


def _save_netpbm(im, fp, filename):

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

    import os
    from subprocess import Popen, check_call, PIPE, CalledProcessError
    file = im._dump()

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

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

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

            retcode = togif_proc.wait()
            if retcode:
                raise CalledProcessError(retcode, togif_cmd)

    try:
        os.unlink(file)
    except OSError:
        pass


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


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

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

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

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

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

            if optimise or (len(used_palette_colors) <= 128 and
               max(used_palette_colors) > len(used_palette_colors)):
                return used_palette_colors


def _get_color_table_size(palette_bytes):
    # calculate the palette size for the header
    import math
    color_table_size = int(math.ceil(math.log(len(palette_bytes)//3, 2)))-1
    if color_table_size < 0:
        color_table_size = 0
    return color_table_size


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

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

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


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

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


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

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

    version = b"87a"
    for extensionKey in ["transparency", "duration", "loop", "comment"]:
        if info and extensionKey in info:
            if ((extensionKey == "duration" and info[extensionKey] == 0) or
               (extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255))):
                continue
            version = b"89a"
            break
    else:
        if im.info.get("version") == b"89a":
            version = b"89a"

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

    background = info["background"] if "background" in info else 0

    return [
        b"GIF"+version +               # signature + version
        o16(im.size[0]) +              # canvas width
        o16(im.size[1]),               # canvas height

        # Logical Screen Descriptor
        # size of global color table + global color table flag
        o8(color_table_size + 128),   # packed fields
        # background + reserved/aspect
        o8(background) + o8(0),

        # Global Color Table
        _get_header_palette(palette_bytes)
    ]


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

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

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

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

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


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

    Warning:: May modify image data.

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

    """
    used_palette_colors = _get_optimize(im, info)

    if info is None:
        info = {}

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

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

    return header, used_palette_colors


# To specify duration, add the time in milliseconds to getdata(),
# e.g. getdata(im_frame, duration=1000)
def getdata(im, offset=(0, 0), **params):
    """
    Legacy Method

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

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

    """
    class Collector(object):
        data = []

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

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

    fp = Collector()

    _write_frame_data(fp, im, offset, params)

    return fp.data


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

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

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

# Image.register_save(GifImageFile.format, _save_netpbm)

```
--- END FILE ---

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

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

Here is an example:

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

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

**Description:**
When saving GIF images using PIL, setting the `duration` in the image's `info` dictionary overrides the `duration` parameter provided to the `save` method. This results in the saved GIF using an unintended frame duration.

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

im = Image.new('L', (100, 100), '#000')
im.info['duration'] = 100  # Intended duration
im.save('temp.gif', duration=1000)  # Desired duration parameter
```

**Expected Behavior:**
The saved GIF (`temp.gif`) should have a frame duration of **1000 milliseconds** as specified by the `duration` parameter in the `save` method.

**Actual Behavior:**
The saved GIF (`temp.gif`) has a frame duration of **100 milliseconds**, ignoring the `duration` parameter and using the value from `im.info`.

[/ISSUE]
{
    "TestFileGif.test_append_images": "PASSED",
    "TestFileGif.test_background": "PASSED",
    "TestFileGif.test_bbox": "PASSED",
    "TestFileGif.test_comment": "PASSED",
    "TestFileGif.test_dispose_background": "PASSED",
    "TestFileGif.test_dispose_none": "PASSED",
    "TestFileGif.test_dispose_previous": "PASSED",
    "TestFileGif.test_duration": "PASSED",
    "TestFileGif.test_eoferror": "PASSED",
    "TestFileGif.test_getdata": "PASSED",
    "TestFileGif.test_headers_saving_for_animated_gifs": "PASSED",
    "TestFileGif.test_identical_frames": "PASSED",
    "TestFileGif.test_invalid_file": "PASSED",
    "TestFileGif.test_iss634": "PASSED",
    "TestFileGif.test_lzw_bits": "PASSED",
    "TestFileGif.test_multiple_duration": "PASSED",
    "TestFileGif.test_n_frames": "PASSED",
    "TestFileGif.test_number_of_loops": "PASSED",
    "TestFileGif.test_optimize": "PASSED",
    "TestFileGif.test_optimize_correctness": "PASSED",
    "TestFileGif.test_optimize_full_l": "PASSED",
    "TestFileGif.test_palette_434": "PASSED",
    "TestFileGif.test_palette_handling": "PASSED",
    "TestFileGif.test_palette_save_ImagePalette": "PASSED",
    "TestFileGif.test_palette_save_L": "PASSED",
    "TestFileGif.test_palette_save_P": "PASSED",
    "TestFileGif.test_roundtrip": "PASSED",
    "TestFileGif.test_roundtrip2": "PASSED",
    "TestFileGif.test_roundtrip_save_all": "PASSED",
    "TestFileGif.test_sanity": "PASSED",
    "TestFileGif.test_save_I": "PASSED",
    "TestFileGif.test_save_dispose": "PASSED",
    "TestFileGif.test_seek": "PASSED",
    "TestFileGif.test_transparent_optimize": "PASSED",
    "TestFileGif.test_version": "PASSED"
}
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index c01adff88..1bfbb5ffd 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -457,7 +457,8 @@ def _save_all(im, fp, filename):
 
 
 def _save(im, fp, filename, save_all=False):
-    im.encoderinfo.update(im.info)
+    for k, v in im.info.items():
+        im.encoderinfo.setdefault(k, v)
     # header
     try:
         palette = im.encoderinfo["palette"]
 

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

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

**Description:**
When saving GIF images using PIL, setting the `duration` in the image's `info` dictionary overrides the `duration` parameter provided to the `save` method. This results in the saved GIF using an unintended frame duration.

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

im = Image.new('L', (100, 100), '#000')
im.info['duration'] = 100  # Intended duration
im.save('temp.gif', duration=1000)  # Desired duration parameter
```

**Expected Behavior:**
The saved GIF (`temp.gif`) should have a frame duration of **1000 milliseconds** as specified by the `duration` parameter in the `save` method.

**Actual Behavior:**
The saved GIF (`temp.gif`) has a frame duration of **100 milliseconds**, ignoring the `duration` parameter and using the value from `im.info`.

[/ISSUE]
--- END ISSUE ---

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

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

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

import itertools

__version__ = "0.9"


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

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


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

class GifImageFile(ImageFile.ImageFile):

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

    global_palette = None

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

    def _open(self):

        # Screen
        s = self.fp.read(13)
        if s[:6] not in [b"GIF87a", b"GIF89a"]:
            raise SyntaxError("not a GIF file")

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

        if flags & 128:
            # get global palette
            self.info["background"] = i8(s[11])
            # check if palette contains colour indices
            p = self.fp.read(3 << bits)
            for i in range(0, len(p), 3):
                if not (i//3 == i8(p[i]) == i8(p[i+1]) == i8(p[i+2])):
                    p = ImagePalette.raw("RGB", p)
                    self.global_palette = self.palette = p
                    break

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

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

    @property
    def is_animated(self):
        if self._is_animated is None:
            if self._n_frames is not None:
                self._is_animated = self._n_frames != 1
            else:
                current = self.tell()

                try:
                    self.seek(1)
                    self._is_animated = True
                except EOFError:
                    self._is_animated = False

                self.seek(current)
        return self._is_animated

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

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

    def _seek(self, frame):

        if frame == 0:
            # rewind
            self.__offset = 0
            self.dispose = None
            self.dispose_extent = [0, 0, 0, 0]  # x0, y0, x1, y1
            self.__frame = -1
            self.__fp.seek(self.__rewind)
            self._prev_im = None
            self.disposal_method = 0
        else:
            # ensure that the previous frame was loaded
            if not self.im:
                self.load()

        if frame != self.__frame + 1:
            raise ValueError("cannot seek to frame %d" % frame)
        self.__frame = frame

        self.tile = []

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

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

        from copy import copy
        self.palette = copy(self.global_palette)

        while True:

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

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

                    # disposal method - find the value of bits 4 - 6
                    dispose_bits = 0b00011100 & flags
                    dispose_bits = dispose_bits >> 2
                    if dispose_bits:
                        # only set the dispose if it is not
                        # unspecified. I'm not sure if this is
                        # correct, but it seems to prevent the last
                        # frame from looking odd for some animations
                        self.disposal_method = dispose_bits
                elif i8(s) == 254:
                    #
                    # comment extension
                    #
                    self.info["comment"] = block
                elif i8(s) == 255:
                    #
                    # application extension
                    #
                    self.info["extension"] = block, self.fp.tell()
                    if block[:11] == b"NETSCAPE2.0":
                        block = self.data()
                        if len(block) >= 3 and i8(block[0]) == 1:
                            self.info["loop"] = i16(block[1:3])
                while self.data():
                    pass

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

                # extent
                x0, y0 = i16(s[0:]), i16(s[2:])
                x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:])
                self.dispose_extent = x0, y0, x1, y1
                flags = i8(s[8])

                interlace = (flags & 64) != 0

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

                # image data
                bits = i8(self.fp.read(1))
                self.__offset = self.fp.tell()
                self.tile = [("gif",
                             (x0, y0, x1, y1),
                             self.__offset,
                             (bits, interlace))]
                break

            else:
                pass
                # raise IOError, "illegal GIF tag `%x`" % i8(s)

        try:
            if self.disposal_method < 2:
                # do not dispose or none specified
                self.dispose = None
            elif self.disposal_method == 2:
                # replace with background colour
                self.dispose = Image.core.fill("P", self.size,
                                               self.info["background"])
            else:
                # replace with previous contents
                if self.im:
                    self.dispose = self.im.copy()

            # only dispose the extent in this frame
            if self.dispose:
                self.dispose = self._crop(self.dispose, self.dispose_extent)
        except (AttributeError, KeyError):
            pass

        if not self.tile:
            # self.__fp = None
            raise EOFError

        self.mode = "L"
        if self.palette:
            self.mode = "P"

    def tell(self):
        return self.__frame

    def load_end(self):
        ImageFile.ImageFile.load_end(self)

        # if the disposal method is 'do not dispose', transparent
        # pixels should show the content of the previous frame
        if self._prev_im and self.disposal_method == 1:
            # we do this by pasting the updated area onto the previous
            # frame which we then use as the current image content
            updated = self._crop(self.im, self.dispose_extent)
            self._prev_im.paste(updated, self.dispose_extent,
                                updated.convert('RGBA'))
            self.im = self._prev_im
        self._prev_im = self.im.copy()

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


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


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

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

    UNDONE: What is the point of mucking with the initial call palette, for
    an image that shouldn't have a palette, or it would be a mode 'P' and
    get returned in the RAWMODE clause.

    :param im: Image object
    :param initial_call: Default false, set to true for a single frame.
    :returns: Image object
    """
    if im.mode in RAWMODE:
        im.load()
        return im
    if Image.getmodebase(im.mode) == "RGB":
        if initial_call:
            palette_size = 256
            if im.palette:
                palette_size = len(im.palette.getdata()[1]) // 3
            return im.convert("P", palette=Image.ADAPTIVE, colors=palette_size)
        else:
            return im.convert("P")
    return im.convert("L")


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

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

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

    used_palette_colors = _get_optimize(im, info)
    if used_palette_colors is not None:
        return im.remap_palette(used_palette_colors, source_palette)

    im.palette.palette = source_palette
    return im


def _write_single_frame(im, fp, palette):
    im_out = _normalize_mode(im, True)
    im_out = _normalize_palette(im_out, palette, im.encoderinfo)

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

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

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

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


def _write_multiple_frames(im, fp, palette):

    duration = im.encoderinfo.get("duration", None)
    disposal = im.encoderinfo.get('disposal', None)

    im_frames = []
    frame_count = 0
    for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
        for im_frame in ImageSequence.Iterator(imSequence):
            # a copy is required here since seek can still mutate the image
            im_frame = _normalize_mode(im_frame.copy())
            im_frame = _normalize_palette(im_frame, palette, im.encoderinfo)

            encoderinfo = im.encoderinfo.copy()
            if isinstance(duration, (list, tuple)):
                encoderinfo['duration'] = duration[frame_count]
            if isinstance(disposal, (list, tuple)):
                encoderinfo["disposal"] = disposal[frame_count]
            frame_count += 1

            if im_frames:
                # delta frame
                previous = im_frames[-1]
                if _get_palette_bytes(im_frame) == _get_palette_bytes(previous['im']):
                    delta = ImageChops.subtract_modulo(im_frame,
                                                       previous['im'])
                else:
                    delta = ImageChops.subtract_modulo(im_frame.convert('RGB'),
                                                       previous['im'].convert('RGB'))
                bbox = delta.getbbox()
                if not bbox:
                    # This frame is identical to the previous frame
                    if duration:
                        previous['encoderinfo']['duration'] += encoderinfo['duration']
                    continue
            else:
                bbox = None
            im_frames.append({
                'im': im_frame,
                'bbox': bbox,
                'encoderinfo': encoderinfo
            })

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

                im_frame = im_frame.crop(frame_data['bbox'])
                offset = frame_data['bbox'][:2]
            _write_frame_data(fp, im_frame, offset, frame_data['encoderinfo'])
        return True


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


def _save(im, fp, filename, save_all=False):
    im.encoderinfo.update(im.info)
    # header
    try:
        palette = im.encoderinfo["palette"]
    except KeyError:
        palette = None
        im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True)

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

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

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


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

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

    return interlace


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

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

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

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

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

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

    if "comment" in im.encoderinfo and 1 <= len(im.encoderinfo["comment"]) <= 255:
        fp.write(b"!" +
                 o8(254) +                # extension intro
                 o8(len(im.encoderinfo["comment"])) +
                 im.encoderinfo["comment"] +
                 o8(0))
    if "loop" in im.encoderinfo:
        number_of_loops = im.encoderinfo["loop"]
        fp.write(b"!" +
                 o8(255) +                # extension intro
                 o8(11) +
                 b"NETSCAPE2.0" +
                 o8(3) +
                 o8(1) +
                 o16(number_of_loops) +   # number of loops
                 o8(0))
    include_color_table = im.encoderinfo.get('include_color_table')
    if include_color_table:
        palette = im.encoderinfo.get("palette", None)
        palette_bytes = _get_palette_bytes(im)
        color_table_size = _get_color_table_size(palette_bytes)
        if color_table_size:
            flags = flags | 128               # local color table flag
            flags = flags | color_table_size

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


def _save_netpbm(im, fp, filename):

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

    import os
    from subprocess import Popen, check_call, PIPE, CalledProcessError
    file = im._dump()

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

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

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

            retcode = togif_proc.wait()
            if retcode:
                raise CalledProcessError(retcode, togif_cmd)

    try:
        os.unlink(file)
    except OSError:
        pass


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


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

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

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

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

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

            if optimise or (len(used_palette_colors) <= 128 and
               max(used_palette_colors) > len(used_palette_colors)):
                return used_palette_colors


def _get_color_table_size(palette_bytes):
    # calculate the palette size for the header
    import math
    color_table_size = int(math.ceil(math.log(len(palette_bytes)//3, 2)))-1
    if color_table_size < 0:
        color_table_size = 0
    return color_table_size


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

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

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


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

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


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

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

    version = b"87a"
    for extensionKey in ["transparency", "duration", "loop", "comment"]:
        if info and extensionKey in info:
            if ((extensionKey == "duration" and info[extensionKey] == 0) or
               (extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255))):
                continue
            version = b"89a"
            break
    else:
        if im.info.get("version") == b"89a":
            version = b"89a"

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

    background = info["background"] if "background" in info else 0

    return [
        b"GIF"+version +               # signature + version
        o16(im.size[0]) +              # canvas width
        o16(im.size[1]),               # canvas height

        # Logical Screen Descriptor
        # size of global color table + global color table flag
        o8(color_table_size + 128),   # packed fields
        # background + reserved/aspect
        o8(background) + o8(0),

        # Global Color Table
        _get_header_palette(palette_bytes)
    ]


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

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

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

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

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


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

    Warning:: May modify image data.

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

    """
    used_palette_colors = _get_optimize(im, info)

    if info is None:
        info = {}

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

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

    return header, used_palette_colors


# To specify duration, add the time in milliseconds to getdata(),
# e.g. getdata(im_frame, duration=1000)
def getdata(im, offset=(0, 0), **params):
    """
    Legacy Method

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

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

    """
    class Collector(object):
        data = []

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

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

    fp = Collector()

    _write_frame_data(fp, im, offset, params)

    return fp.data


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

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

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

# Image.register_save(GifImageFile.format, _save_netpbm)

```
--- END FILE ---

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

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

Here is an example:

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

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