{
    "file_diffs": [
        {
            "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\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\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            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    if transparent_color_exists or duration != 0:\n        transparency_flag = 1 if transparent_color_exists else 0\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(transparency_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\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",
            "header": {
                "file": {
                    "path": "PIL/GifImagePlugin.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "f227211a2",
                "new_commit_hash": "71d8ce59a",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/PIL/GifImagePlugin.py"
            },
            "plus_file": {
                "path": "b/PIL/GifImagePlugin.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 392,
                            "length": 6
                        },
                        "new_range": {
                            "start": 392,
                            "length": 7
                        },
                        "section": "def _write_single_frame(im, fp, palette):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "def _write_multiple_frames(im, fp, palette):",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    duration = im.encoderinfo.get(\"duration\", None)",
                                "type": "context"
                            },
                            {
                                "content": "    disposal = im.encoderinfo.get('disposal', None)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    im_frames = []",
                                "type": "context"
                            },
                            {
                                "content": "    frame_count = 0",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "PIL/GifImagePlugin.py",
                            "type": "function",
                            "name": "_write_multiple_frames",
                            "content": "def _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",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 392,
                            "end_lineno": 451,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 404,
                            "length": 6
                        },
                        "new_range": {
                            "start": 405,
                            "length": 8
                        },
                        "section": "def _write_multiple_frames(im, fp, palette):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "            encoderinfo = im.encoderinfo.copy()",
                                "type": "context"
                            },
                            {
                                "content": "            if isinstance(duration, (list, tuple)):",
                                "type": "context"
                            },
                            {
                                "content": "                encoderinfo['duration'] = duration[frame_count]",
                                "type": "context"
                            },
                            {
                                "content": "            if isinstance(disposal, (list, tuple)):",
                                "type": "added"
                            },
                            {
                                "content": "                encoderinfo[\"disposal\"] = disposal[frame_count]",
                                "type": "added"
                            },
                            {
                                "content": "            frame_count += 1",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "            if im_frames:",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "PIL/GifImagePlugin.py",
                            "type": "function",
                            "name": "_write_multiple_frames",
                            "content": "def _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",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 392,
                            "end_lineno": 451,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 503,
                            "length": 15
                        },
                        "new_range": {
                            "start": 506,
                            "length": 19
                        },
                        "section": "def _write_local_header(fp, im, offset, flags):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        duration = int(im.encoderinfo[\"duration\"] / 10)",
                                "type": "context"
                            },
                            {
                                "content": "    else:",
                                "type": "context"
                            },
                            {
                                "content": "        duration = 0",
                                "type": "context"
                            },
                            {
                                "content": "    if transparent_color_exists or duration != 0:",
                                "type": "deleted"
                            },
                            {
                                "content": "        transparency_flag = 1 if transparent_color_exists else 0",
                                "type": "deleted"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    disposal = int(im.encoderinfo.get('disposal', 0))",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    if transparent_color_exists or duration != 0 or disposal:",
                                "type": "added"
                            },
                            {
                                "content": "        packed_flag = 1 if transparent_color_exists else 0",
                                "type": "added"
                            },
                            {
                                "content": "        packed_flag |= disposal << 2",
                                "type": "added"
                            },
                            {
                                "content": "        if not transparent_color_exists:",
                                "type": "context"
                            },
                            {
                                "content": "            transparency = 0",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        fp.write(b\"!\" +",
                                "type": "context"
                            },
                            {
                                "content": "                 o8(249) +                # extension intro",
                                "type": "context"
                            },
                            {
                                "content": "                 o8(4) +                  # length",
                                "type": "context"
                            },
                            {
                                "content": "                 o8(transparency_flag) +  # packed fields",
                                "type": "deleted"
                            },
                            {
                                "content": "                 o8(packed_flag) +        # packed fields",
                                "type": "added"
                            },
                            {
                                "content": "                 o16(duration) +          # duration",
                                "type": "context"
                            },
                            {
                                "content": "                 o8(transparency) +       # transparency index",
                                "type": "context"
                            },
                            {
                                "content": "                 o8(0))",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "PIL/GifImagePlugin.py",
                            "type": "function",
                            "name": "_write_local_header",
                            "content": "def _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    if transparent_color_exists or duration != 0:\n        transparency_flag = 1 if transparent_color_exists else 0\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(transparency_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))",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 483,
                            "end_lineno": 552,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "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_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        from PIL import ImagePalette\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\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\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        from PIL import ImagePalette\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\nif __name__ == '__main__':\n    unittest.main()\n",
            "header": {
                "file": {
                    "path": "Tests/test_file_gif.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "2ca234173",
                "new_commit_hash": "22a2c0072",
                "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": 259,
                            "length": 6
                        },
                        "new_range": {
                            "start": 259,
                            "length": 40
                        },
                        "section": "class TestFileGif(PillowTestCase):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        except EOFError:",
                                "type": "context"
                            },
                            {
                                "content": "            pass",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def test_save_dispose(self):",
                                "type": "added"
                            },
                            {
                                "content": "        out = self.tempfile('temp.gif')",
                                "type": "added"
                            },
                            {
                                "content": "        im_list = [",
                                "type": "added"
                            },
                            {
                                "content": "            Image.new('L', (100, 100), '#000'),",
                                "type": "added"
                            },
                            {
                                "content": "            Image.new('L', (100, 100), '#111'),",
                                "type": "added"
                            },
                            {
                                "content": "            Image.new('L', (100, 100), '#222'),",
                                "type": "added"
                            },
                            {
                                "content": "        ]",
                                "type": "added"
                            },
                            {
                                "content": "        for method in range(0,4):",
                                "type": "added"
                            },
                            {
                                "content": "            im_list[0].save(",
                                "type": "added"
                            },
                            {
                                "content": "                out,",
                                "type": "added"
                            },
                            {
                                "content": "                save_all=True,",
                                "type": "added"
                            },
                            {
                                "content": "                append_images=im_list[1:],",
                                "type": "added"
                            },
                            {
                                "content": "                disposal=method",
                                "type": "added"
                            },
                            {
                                "content": "            )",
                                "type": "added"
                            },
                            {
                                "content": "            img = Image.open(out)",
                                "type": "added"
                            },
                            {
                                "content": "            for _ in range(2):",
                                "type": "added"
                            },
                            {
                                "content": "                img.seek(img.tell() + 1)",
                                "type": "added"
                            },
                            {
                                "content": "                self.assertEqual(img.disposal_method, method)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        # check per frame disposal",
                                "type": "added"
                            },
                            {
                                "content": "        im_list[0].save(",
                                "type": "added"
                            },
                            {
                                "content": "            out,",
                                "type": "added"
                            },
                            {
                                "content": "            save_all=True,",
                                "type": "added"
                            },
                            {
                                "content": "            append_images=im_list[1:],",
                                "type": "added"
                            },
                            {
                                "content": "            disposal=tuple(range(len(im_list)))",
                                "type": "added"
                            },
                            {
                                "content": "            )",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        img = Image.open(out)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        for i in range(2):",
                                "type": "added"
                            },
                            {
                                "content": "            img.seek(img.tell() + 1)",
                                "type": "added"
                            },
                            {
                                "content": "            self.assertEqual(img.disposal_method, i+1)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    def test_iss634(self):",
                                "type": "context"
                            },
                            {
                                "content": "        img = Image.open(\"Tests/images/iss634.gif\")",
                                "type": "context"
                            },
                            {
                                "content": "        # seek to the second frame",
                                "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\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        from PIL import ImagePalette\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": 589,
                            "parent": null
                        }
                    ],
                    "added_entities": [
                        {
                            "file_name": "Tests/test_file_gif.py",
                            "type": "method",
                            "name": "TestFileGif.test_save_dispose",
                            "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\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        from PIL import ImagePalette\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": 262,
                            "end_lineno": 294,
                            "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\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        from PIL import ImagePalette\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": 589,
                                "parent": null
                            }
                        }
                    ],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": ".. _image-file-formats:\n\nImage file formats\n==================\n\nThe Python Imaging Library supports a wide variety of raster file formats.\nOver 30 different file formats can be identified and read by the library.\nWrite support is less extensive, but most common interchange and presentation\nformats are supported.\n\nThe :py:meth:`~PIL.Image.Image.open` function identifies files from their\ncontents, not their names, but the :py:meth:`~PIL.Image.Image.save` method\nlooks at the name to determine which format to use, unless the format is given\nexplicitly.\n\nFully supported formats\n-----------------------\n\n.. contents::\n\nBMP\n^^^\n\nPIL reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``,\nor ``RGB`` data. 16-colour images are read as ``P`` images. Run-length encoding\nis not supported.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**compression**\n    Set to ``bmp_rle`` if the file is run-length encoded.\n\nEPS\n^^^\n\nPIL identifies EPS files containing image data, and can read files that contain\nembedded raster images (ImageData descriptors). If Ghostscript is available,\nother EPS files can be read as well. The EPS driver can also write EPS\nimages. The EPS driver can read EPS images in ``L``, ``LAB``, ``RGB`` and\n``CMYK`` mode, but Ghostscript may convert the images to ``RGB`` mode rather\nthan leaving them in the original color space. The EPS driver can write images\nin ``L``, ``RGB`` and ``CMYK`` modes.\n\nIf Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`\nmethod with the following parameter to affect how Ghostscript renders the EPS\n\n**scale**\n    Affects the scale of the resultant rasterized image. If the EPS suggests\n    that the image be rendered at 100px x 100px, setting this parameter to\n    2 will make the Ghostscript render a 200px x 200px image instead. The\n    relative position of the bounding box is maintained::\n\n        im = Image.open(...)\n        im.size #(100,100)\n        im.load(scale=2)\n        im.size #(200,200)\n\nGIF\n^^^\n\nPIL reads GIF87a and GIF89a versions of the GIF file format. The library writes\nrun-length encoded files in GIF87a by default, unless GIF89a features\nare used or GIF89a is already in use.\n\nNote that GIF files are always read as grayscale (``L``)\nor palette mode (``P``) images.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**background**\n    Default background color (a palette color index).\n\n**transparency**\n    Transparency color index. This key is omitted if the image is not\n    transparent.\n\n**version**\n    Version (either ``GIF87a`` or ``GIF89a``).\n\n**duration**\n    May not be present. The time to display the current frame\n    of the GIF, in milliseconds.\n\n**loop**\n    May not be present. The number of times the GIF should loop.\n\nReading sequences\n~~~~~~~~~~~~~~~~~\n\nThe GIF loader supports the :py:meth:`~file.seek` and :py:meth:`~file.tell`\nmethods. You can seek to the next frame (``im.seek(im.tell() + 1)``), or rewind\nthe file by seeking to the first frame. Random access is not supported.\n\n``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame.\n\nSaving\n~~~~~~\n\nWhen calling :py:meth:`~PIL.Image.Image.save`, the following options\nare available::\n\n    im.save(out, save_all=True, append_images=[im1, im2, ...])\n\n**save_all**\n    If present and true, all frames of the image will be saved. If\n    not, then only the first frame of a multiframe image will be saved.\n\n**append_images**\n    A list of images to append as additional frames. Each of the\n    images in the list can be single or multiframe images.\n    This is currently only supported for GIF, PDF, TIFF, and WebP.\n\n**duration**\n    The display duration of each frame of the multiframe gif, in\n    milliseconds. Pass a single integer for a constant duration, or a\n    list or tuple to set the duration for each frame separately.\n\n**loop**\n    Integer number of times the GIF should loop.\n\n**optimize**\n    If present and true, attempt to compress the palette by\n    eliminating unused colors. This is only useful if the palette can\n    be compressed to the next smaller power of 2 elements.\n\n**palette**\n    Use the specified palette for the saved image. The palette should\n    be a bytes or bytearray object containing the palette entries in\n    RGBRGB... form. It should be no more than 768 bytes. Alternately,\n    the palette can be passed in as an\n    :py:class:`PIL.ImagePalette.ImagePalette` object.\n\nReading local images\n~~~~~~~~~~~~~~~~~~~~\n\nThe GIF loader creates an image memory the same size as the GIF file’s *logical\nscreen size*, and pastes the actual pixel data (the *local image*) into this\nimage. If you only want the actual pixel rectangle, you can manipulate the\n:py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.Image.Image.tile`\nattributes before loading the file::\n\n    im = Image.open(...)\n\n    if im.tile[0][0] == \"gif\":\n        # only read the first \"local image\" from this GIF file\n        tag, (x0, y0, x1, y1), offset, extra = im.tile[0]\n        im.size = (x1 - x0, y1 - y0)\n        im.tile = [(tag, (0, 0) + im.size, offset, extra)]\n\nICNS\n^^^^\n\nPIL reads and (macOS only) writes macOS ``.icns`` files.  By default, the\nlargest available icon is read, though you can override this by setting the\n:py:attr:`~PIL.Image.Image.size` property before calling\n:py:meth:`~PIL.Image.Image.load`.  The :py:meth:`~PIL.Image.Image.open` method\nsets the following :py:attr:`~PIL.Image.Image.info` property:\n\n**sizes**\n    A list of supported sizes found in this icon file; these are a\n    3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina\n    icon and 1 for a standard icon.  You *are* permitted to use this 3-tuple\n    format for the :py:attr:`~PIL.Image.Image.size` property if you set it\n    before calling :py:meth:`~PIL.Image.Image.load`; after loading, the size\n    will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you\n    ask for ``(512, 512, 2)``, the final value of\n    :py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``).\n\nICO\n^^^\n\nICO is used to store icons on Windows. The largest available icon is read.\n\nThe :py:meth:`~PIL.Image.Image.save` method supports the following options:\n\n**sizes**\n    A list of sizes including in this ico file; these are a 2-tuple,\n    ``(width, height)``; Default to ``[(16, 16), (24, 24), (32, 32), (48, 48),\n    (64, 64), (128, 128), (256, 256)]``. Any sizes bigger than the original\n    size or 256 will be ignored.\n\nIM\n^^\n\nIM is a format used by LabEye and other applications based on the IFUNC image\nprocessing library. The library reads and writes most uncompressed interchange\nversions of this format.\n\nIM is the only format that can store all internal PIL formats.\n\nJPEG\n^^^^\n\nPIL reads JPEG, JFIF, and Adobe JPEG files containing ``L``, ``RGB``, or\n``CMYK`` data. It writes standard and progressive JFIF files.\n\nUsing the :py:meth:`~PIL.Image.Image.draft` method, you can speed things up by\nconverting ``RGB`` images to ``L``, and resize images to 1/2, 1/4 or 1/8 of\ntheir original size while loading them.\n\nThe :py:meth:`~PIL.Image.Image.open` method may set the following\n:py:attr:`~PIL.Image.Image.info` properties if available:\n\n**jfif**\n    JFIF application marker found. If the file is not a JFIF file, this key is\n    not present.\n\n**jfif_version**\n    A tuple representing the jfif version, (major version, minor version).\n\n**jfif_density**\n    A tuple representing the pixel density of the image, in units specified\n    by jfif_unit.\n\n**jfif_unit**\n    Units for the jfif_density:\n\n    * 0 - No Units\n    * 1 - Pixels per Inch\n    * 2 - Pixels per Centimeter\n\n**dpi**\n    A tuple representing the reported pixel density in pixels per inch, if\n    the file is a jfif file and the units are in inches.\n\n**adobe**\n    Adobe application marker found. If the file is not an Adobe JPEG file, this\n    key is not present.\n\n**adobe_transform**\n    Vendor Specific Tag.\n\n**progression**\n    Indicates that this is a progressive JPEG file.\n\n**icc_profile**\n    The ICC color profile for the image.\n\n**exif**\n    Raw EXIF data from the image.\n\n\nThe :py:meth:`~PIL.Image.Image.save` method supports the following options:\n\n**quality**\n    The image quality, on a scale from 1 (worst) to 95 (best). The default is\n    75. Values above 95 should be avoided; 100 disables portions of the JPEG\n    compression algorithm, and results in large files with hardly any gain in\n    image quality.\n\n**optimize**\n    If present and true, indicates that the encoder should make an extra pass\n    over the image in order to select optimal encoder settings.\n\n**progressive**\n    If present and true, indicates that this image should be stored as a\n    progressive JPEG file.\n\n**dpi**\n    A tuple of integers representing the pixel density, ``(x,y)``.\n\n**icc_profile**\n    If present and true, the image is stored with the provided ICC profile.\n    If this parameter is not provided, the image will be saved with no profile\n    attached. To preserve the existing profile::\n\n        im.save(filename, 'jpeg', icc_profile=im.info.get('icc_profile'))\n\n**exif**\n    If present, the image will be stored with the provided raw EXIF data.\n\n**subsampling**\n    If present, sets the subsampling for the encoder.\n\n    * ``keep``: Only valid for JPEG files, will retain the original image setting.\n    * ``4:4:4``, ``4:2:2``, ``4:2:0``: Specific sampling values\n    * ``-1``: equivalent to ``keep``\n    * ``0``: equivalent to ``4:4:4``\n    * ``1``: equivalent to ``4:2:2``\n    * ``2``: equivalent to ``4:2:0``\n\n**qtables**\n    If present, sets the qtables for the encoder. This is listed as an\n    advanced option for wizards in the JPEG documentation. Use with\n    caution. ``qtables`` can be one of several types of values:\n\n    *  a string, naming a preset, e.g. ``keep``, ``web_low``, or ``web_high``\n    *  a list, tuple, or dictionary (with integer keys =\n       range(len(keys))) of lists of 64 integers. There must be\n       between 2 and 4 tables.\n\n    .. versionadded:: 2.5.0\n\n\n.. note::\n\n    To enable JPEG support, you need to build and install the IJG JPEG library\n    before building the Python Imaging Library. See the distribution README for\n    details.\n\nJPEG 2000\n^^^^^^^^^\n\n.. versionadded:: 2.4.0\n\nPIL reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB`` or\n``RGBA`` data.  It can also read files containing ``YCbCr`` data, which it\nconverts on read into ``RGB`` or ``RGBA`` depending on whether or not there is\nan alpha channel.  PIL supports JPEG 2000 raw codestreams (``.j2k`` files), as\nwell as boxed JPEG 2000 files (``.j2p`` or ``.jpx`` files).  PIL does *not*\nsupport files whose components have different sampling frequencies.\n\nWhen loading, if you set the ``mode`` on the image prior to the\n:py:meth:`~PIL.Image.Image.load` method being invoked, you can ask PIL to\nconvert the image to either ``RGB`` or ``RGBA`` rather than choosing for\nitself.  It is also possible to set ``reduce`` to the number of resolutions to\ndiscard (each one reduces the size of the resulting image by a factor of 2),\nand ``layers`` to specify the number of quality layers to load.\n\nThe :py:meth:`~PIL.Image.Image.save` method supports the following options:\n\n**offset**\n    The image offset, as a tuple of integers, e.g. (16, 16)\n\n**tile_offset**\n    The tile offset, again as a 2-tuple of integers.\n\n**tile_size**\n    The tile size as a 2-tuple.  If not specified, or if set to None, the\n    image will be saved without tiling.\n\n**quality_mode**\n    Either `\"rates\"` or `\"dB\"` depending on the units you want to use to\n    specify image quality.\n\n**quality_layers**\n    A sequence of numbers, each of which represents either an approximate size\n    reduction (if quality mode is `\"rates\"`) or a signal to noise ratio value\n    in decibels.  If not specified, defaults to a single layer of full quality.\n\n**num_resolutions**\n    The number of different image resolutions to be stored (which corresponds\n    to the number of Discrete Wavelet Transform decompositions plus one).\n\n**codeblock_size**\n    The code-block size as a 2-tuple.  Minimum size is 4 x 4, maximum is 1024 x\n    1024, with the additional restriction that no code-block may have more\n    than 4096 coefficients (i.e. the product of the two numbers must be no\n    greater than 4096).\n\n**precinct_size**\n    The precinct size as a 2-tuple.  Must be a power of two along both axes,\n    and must be greater than the code-block size.\n\n**irreversible**\n    If ``True``, use the lossy Irreversible Color Transformation\n    followed by DWT 9-7.  Defaults to ``False``, which means to use the\n    Reversible Color Transformation with DWT 5-3.\n\n**progression**\n    Controls the progression order; must be one of ``\"LRCP\"``, ``\"RLCP\"``,\n    ``\"RPCL\"``, ``\"PCRL\"``, ``\"CPRL\"``.  The letters stand for Component,\n    Position, Resolution and Layer respectively and control the order of\n    encoding, the idea being that e.g. an image encoded using LRCP mode can\n    have its quality layers decoded as they arrive at the decoder, while one\n    encoded using RLCP mode will have increasing resolutions decoded as they\n    arrive, and so on.\n\n**cinema_mode**\n    Set the encoder to produce output compliant with the digital cinema\n    specifications.  The options here are ``\"no\"`` (the default),\n    ``\"cinema2k-24\"`` for 24fps 2K, ``\"cinema2k-48\"`` for 48fps 2K, and\n    ``\"cinema4k-24\"`` for 24fps 4K.  Note that for compliant 2K files,\n    *at least one* of your image dimensions must match 2048 x 1080, while\n    for compliant 4K files, *at least one* of the dimensions must match\n    4096 x 2160.\n\n.. note::\n\n   To enable JPEG 2000 support, you need to build and install the OpenJPEG\n   library, version 2.0.0 or higher, before building the Python Imaging\n   Library.\n\n   Windows users can install the OpenJPEG binaries available on the\n   OpenJPEG website, but must add them to their PATH in order to use PIL (if\n   you fail to do this, you will get errors about not being able to load the\n   ``_imaging`` DLL).\n\nMSP\n^^^\n\nPIL identifies and reads MSP files from Windows 1 and 2. The library writes\nuncompressed (Windows 1) versions of this format.\n\nPCX\n^^^\n\nPIL reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data.\n\nPNG\n^^^\n\nPIL identifies, reads, and writes PNG files containing ``1``, ``L``, ``P``,\n``RGB``, or ``RGBA`` data. Interlaced files are supported as of v1.1.7.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties, when appropriate:\n\n**chromaticity**\n    The chromaticity points, as an 8 tuple of floats. (``White Point\n    X``, ``White Point Y``, ``Red X``, ``Red Y``, ``Green X``, ``Green\n    Y``, ``Blue X``, ``Blue Y``)\n\n**gamma**\n    Gamma, given as a floating point number.\n\n**srgb**\n    The sRGB rendering intent as an integer.\n\n      * 0 Perceptual\n      * 1 Relative Colorimetric\n      * 2 Saturation\n      * 3 Absolute Colorimetric\n\n**transparency**\n    For ``P`` images: Either the palette index for full transparent pixels,\n    or a byte string with alpha values for each palette entry.\n\n    For ``L`` and ``RGB`` images, the color that represents full transparent\n    pixels in this image.\n\n    This key is omitted if the image is not a transparent palette image.\n\n``Open`` also sets ``Image.text`` to a list of the values of the\n``tEXt``, ``zTXt``, and ``iTXt`` chunks of the PNG image. Individual\ncompressed chunks are limited to a decompressed size of\n``PngImagePlugin.MAX_TEXT_CHUNK``, by default 1MB, to prevent\ndecompression bombs. Additionally, the total size of all of the text\nchunks is limited to ``PngImagePlugin.MAX_TEXT_MEMORY``, defaulting to\n64MB.\n\nThe :py:meth:`~PIL.Image.Image.save` method supports the following options:\n\n**optimize**\n    If present and true, instructs the PNG writer to make the output file as\n    small as possible. This includes extra processing in order to find optimal\n    encoder settings.\n\n**transparency**\n    For ``P``, ``L``, and ``RGB`` images, this option controls what\n    color image to mark as transparent.\n\n    For ``P`` images, this can be a either the palette index,\n    or a byte string with alpha values for each palette entry.\n\n**dpi**\n    A tuple of two numbers corresponding to the desired dpi in each direction.\n\n**pnginfo**\n    A :py:class:`PIL.PngImagePlugin.PngInfo` instance containing text tags.\n\n**compress_level**\n    ZLIB compression level, a number between 0 and 9: 1 gives best speed,\n    9 gives best compression, 0 gives no compression at all. Default is 6.\n    When ``optimize`` option is True ``compress_level`` has no effect\n    (it is set to 9 regardless of a value passed).\n\n**icc_profile**\n    The ICC Profile to include in the saved file.\n\n**bits (experimental)**\n    For ``P`` images, this option controls how many bits to store. If omitted,\n    the PNG writer uses 8 bits (256 colors).\n\n**dictionary (experimental)**\n    Set the ZLIB encoder dictionary.\n\n.. note::\n\n    To enable PNG support, you need to build and install the ZLIB compression\n    library before building the Python Imaging Library. See the installation\n    documentation for details.\n\nPPM\n^^^\n\nPIL reads and writes PBM, PGM and PPM files containing ``1``, ``L`` or ``RGB``\ndata.\n\nSGI\n^^^\n\nPillow reads and writes uncompressed ``L``, ``RGB``, and ``RGBA`` files.\n\n\nSPIDER\n^^^^^^\n\nPIL reads and writes SPIDER image files of 32-bit floating point data\n(\"F;32F\").\n\nPIL also reads SPIDER stack files containing sequences of SPIDER images. The\n:py:meth:`~file.seek` and :py:meth:`~file.tell` methods are supported, and\nrandom access is allowed.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following attributes:\n\n**format**\n    Set to ``SPIDER``\n\n**istack**\n    Set to 1 if the file is an image stack, else 0.\n\n**nimages**\n    Set to the number of images in the stack.\n\nA convenience method, :py:meth:`~PIL.Image.Image.convert2byte`, is provided for\nconverting floating point data to byte data (mode ``L``)::\n\n    im = Image.open('image001.spi').convert2byte()\n\nWriting files in SPIDER format\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe extension of SPIDER files may be any 3 alphanumeric characters. Therefore\nthe output format must be specified explicitly::\n\n    im.save('newimage.spi', format='SPIDER')\n\nFor more information about the SPIDER image processing package, see the\n`SPIDER homepage`_ at `Wadsworth Center`_.\n\n.. _SPIDER homepage: https://spider.wadsworth.org/spider_doc/spider/docs/spider.html\n.. _Wadsworth Center: https://www.wadsworth.org/\n\nTIFF\n^^^^\n\nPIL reads and writes TIFF files. It can read both striped and tiled images,\npixel and plane interleaved multi-band images, and either uncompressed, or\nPackbits, LZW, or JPEG compressed images.\n\nIf you have libtiff and its headers installed, PIL can read and write many more\nkinds of compressed TIFF files. If not, PIL will always write uncompressed\nfiles.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**compression**\n    Compression mode.\n\n    .. versionadded:: 2.0.0\n\n**dpi**\n    Image resolution as an ``(xdpi, ydpi)`` tuple, where applicable. You can use\n    the :py:attr:`~PIL.Image.Image.tag` attribute to get more detailed\n    information about the image resolution.\n\n    .. versionadded:: 1.1.5\n\n**resolution**\n    Image resolution as an ``(xres, yres)`` tuple, where applicable. This is a\n    measurement in whichever unit is specified by the file.\n\n    .. versionadded:: 1.1.5\n\n\nThe :py:attr:`~PIL.Image.Image.tag_v2` attribute contains a dictionary\nof TIFF metadata. The keys are numerical indexes from\n:py:attr:`~PIL.TiffTags.TAGS_V2`.  Values are strings or numbers for single\nitems, multiple values are returned in a tuple of values. Rational\nnumbers are returned as a :py:class:`~PIL.TiffImagePlugin.IFDRational`\nobject.\n\n    .. versionadded:: 3.0.0\n\nFor compatibility with legacy code, the\n:py:attr:`~PIL.Image.Image.tag` attribute contains a dictionary of\ndecoded TIFF fields as returned prior to version 3.0.0.  Values are\nreturned as either strings or tuples of numeric values. Rational\nnumbers are returned as a tuple of ``(numerator, denominator)``.\n\n    .. deprecated:: 3.0.0\n\n\nSaving Tiff Images\n~~~~~~~~~~~~~~~~~~\n\nThe :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:\n\n**save_all**\n    If true, Pillow will save all frames of the image to a multiframe tiff document.\n\n    .. versionadded:: 3.4.0\n\n**tiffinfo**\n    A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` object or dict\n    object containing tiff tags and values. The TIFF field type is\n    autodetected for Numeric and string values, any other types\n    require using an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`\n    object and setting the type in\n    :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` with\n    the appropriate numerical value from\n    ``TiffTags.TYPES``.\n\n    .. versionadded:: 2.3.0\n\n    Metadata values that are of the rational type should be passed in\n    using a :py:class:`~PIL.TiffImagePlugin.IFDRational` object.\n\n    .. versionadded:: 3.1.0\n\n    For compatibility with legacy code, a\n    :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may\n    be passed in this field. However, this is deprecated.\n\n    .. versionadded:: 3.0.0\n\n .. note::\n\n    Only some tags are currently supported when writing using\n    libtiff. The supported list is found in\n    :py:attr:`~PIL:TiffTags.LIBTIFF_CORE`.\n\n**compression**\n    A string containing the desired compression method for the\n    file. (valid only with libtiff installed) Valid compression\n    methods are: ``None``, ``\"tiff_ccitt\"``, ``\"group3\"``,\n    ``\"group4\"``, ``\"tiff_jpeg\"``, ``\"tiff_adobe_deflate\"``,\n    ``\"tiff_thunderscan\"``, ``\"tiff_deflate\"``, ``\"tiff_sgilog\"``,\n    ``\"tiff_sgilog24\"``, ``\"tiff_raw_16\"``\n\nThese arguments to set the tiff header fields are an alternative to\nusing the general tags available through tiffinfo.\n\n**description**\n\n**software**\n\n**date_time**\n\n**artist**\n\n**copyright**\n    Strings\n\n**resolution_unit**\n    A string of \"inch\", \"centimeter\" or \"cm\"\n\n**resolution**\n\n**x_resolution**\n\n**y_resolution**\n\n**dpi**\n    Either a Float, 2 tuple of (numerator, denominator) or a\n    :py:class:`~PIL.TiffImagePlugin.IFDRational`. Resolution implies\n    an equal x and y resolution, dpi also implies a unit of inches.\n\n\nWebP\n^^^^\n\nPIL reads and writes WebP files. The specifics of PIL's capabilities with this\nformat are currently undocumented.\n\nThe :py:meth:`~PIL.Image.Image.save` method supports the following options:\n\n**lossless**\n    If present and true, instructs the WebP writer to use lossless compression.\n\n**quality**\n    Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest\n    size and 100 the largest. For lossless, this parameter is the amount\n    of effort put into the compression: 0 is the fastest, but gives larger\n    files compared to the slowest, but best, 100.\n\n**method**\n    Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 0.\n\n**icc_procfile**\n    The ICC Profile to include in the saved file. Only supported if\n    the system WebP library was built with webpmux support.\n\n**exif**\n    The exif data to include in the saved file. Only supported if\n    the system WebP library was built with webpmux support.\n\nSaving sequences\n~~~~~~~~~~~~~~~~~\n\n.. note::\n\n    Support for animated WebP files will only be enabled if the system WebP\n    library is v0.5.0 or later. You can check webp animation support at\n    runtime by calling `features.check(\"webp_anim\")`.\n\nWhen calling :py:meth:`~PIL.Image.Image.save`, the following options\nare available when the `save_all` argument is present and true.\n\n**append_images**\n    A list of images to append as additional frames. Each of the\n    images in the list can be single or multiframe images.\n\n**duration**\n    The display duration of each frame, in milliseconds. Pass a single\n    integer for a constant duration, or a list or tuple to set the\n    duration for each frame separately.\n\n**loop**\n    Number of times to repeat the animation. Defaults to [0 = infinite].\n\n**background**\n    Background color of the canvas, as an RGBA tuple with values in\n    the range of (0-255).\n\n**minimize_size**\n    If true, minimize the output size (slow). Implicitly disables\n    key-frame insertion.\n\n**kmin, kmax**\n    Minimum and maximum distance between consecutive key frames in\n    the output. The library may insert some key frames as needed\n    to satisfy this criteria. Note that these conditions should\n    hold: kmax > kmin and kmin >= kmax / 2 + 1. Also, if kmax <= 0,\n    then key-frame insertion is disabled; and if kmax == 1, then all\n    frames will be key-frames (kmin value does not matter for these\n    special cases).\n\n**allow_mixed**\n    If true, use mixed compression mode; the encoder heuristically\n    chooses between lossy and lossless for each frame.\n\nXBM\n^^^\n\nPIL reads and writes X bitmap files (mode ``1``).\n\nRead-only formats\n-----------------\n\nCUR\n^^^\n\nCUR is used to store cursors on Windows. The CUR decoder reads the largest\navailable cursor. Animated cursors are not supported.\n\nDCX\n^^^\n\nDCX is a container file format for PCX files, defined by Intel. The DCX format\nis commonly used in fax applications. The DCX decoder can read files containing\n``1``, ``L``, ``P``, or ``RGB`` data.\n\nWhen the file is opened, only the first image is read. You can use\n:py:meth:`~file.seek` or :py:mod:`~PIL.ImageSequence` to read other images.\n\n\nDDS\n^^^\n\nDDS is a popular container texture format used in video games and natively\nsupported by DirectX.\nCurrently, DXT1, DXT3, and DXT5 pixel formats are supported and only in ``RGBA``\nmode.\n\n.. versionadded:: 3.4.0 DXT3\n\nFLI, FLC\n^^^^^^^^\n\nPIL reads Autodesk FLI and FLC animations.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**duration**\n    The delay (in milliseconds) between each frame.\n\nFPX\n^^^\n\nPIL reads Kodak FlashPix files. In the current version, only the highest\nresolution image is read from the file, and the viewing transform is not taken\ninto account.\n\n.. note::\n\n    To enable full FlashPix support, you need to build and install the IJG JPEG\n    library before building the Python Imaging Library. See the distribution\n    README for details.\n\nFTEX\n^^^^\n\n.. versionadded:: 3.2.0\n\nThe FTEX decoder reads textures used for 3D objects in\nIndependence War 2: Edge Of Chaos. The plugin reads a single texture\nper file, in the compressed and uncompressed formats.\n\nGBR\n^^^\n\nThe GBR decoder reads GIMP brush files, version 1 and 2.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**comment**\n    The brush name.\n\n**spacing**\n    The spacing between the brushes, in pixels. Version 2 only.\n\nGD\n^^\n\nPIL reads uncompressed GD files. Note that this file format cannot be\nautomatically identified, so you must use :py:func:`PIL.GdImageFile.open` to\nread such a file.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**transparency**\n    Transparency color index. This key is omitted if the image is not\n    transparent.\n\nIMT\n^^^\n\nPIL reads Image Tools images containing ``L`` data.\n\nIPTC/NAA\n^^^^^^^^\n\nPIL provides limited read support for IPTC/NAA newsphoto files.\n\nMCIDAS\n^^^^^^\n\nPIL identifies and reads 8-bit McIdas area files.\n\nMIC\n^^^\n\nPIL identifies and reads Microsoft Image Composer (MIC) files. When opened, the\nfirst sprite in the file is loaded. You can use :py:meth:`~file.seek` and\n:py:meth:`~file.tell` to read other sprites from the file.\n\nNote that there may be an embedded gamma of 2.2 in MIC files.\n\nMPO\n^^^\n\nPillow identifies and reads Multi Picture Object (MPO) files, loading the primary\nimage when first opened. The :py:meth:`~file.seek` and :py:meth:`~file.tell`\nmethods may be used to read other pictures from the file. The pictures are\nzero-indexed and random access is supported.\n\nPCD\n^^^\n\nPIL reads PhotoCD files containing ``RGB`` data. This only reads the 768x512\nresolution image from the file. Higher resolutions are encoded in a proprietary\nencoding.\n\nPIXAR\n^^^^^\n\nPIL provides limited support for PIXAR raster files. The library can identify\nand read “dumped” RGB files.\n\nThe format code is ``PIXAR``.\n\nPSD\n^^^\n\nPIL identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.\n\n\nTGA\n^^^\n\nPIL reads 24- and 32-bit uncompressed and run-length encoded TGA files.\n\nWAL\n^^^\n\n.. versionadded:: 1.1.4\n\nPIL reads Quake2 WAL texture files.\n\nNote that this file format cannot be automatically identified, so you must use\nthe open function in the :py:mod:`~PIL.WalImageFile` module to read files in\nthis format.\n\nBy default, a Quake2 standard palette is attached to the texture. To override\nthe palette, use the putpalette method.\n\nXPM\n^^^\n\nPIL reads X pixmap files (mode ``P``) with 256 colors or less.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**transparency**\n    Transparency color index. This key is omitted if the image is not\n    transparent.\n\nWrite-only formats\n------------------\n\nPALM\n^^^^\n\nPIL provides write-only support for PALM pixmap files.\n\nThe format code is ``Palm``, the extension is ``.palm``.\n\nPDF\n^^^\n\nPIL can write PDF (Acrobat) images. Such images are written as binary PDF 1.1\nfiles, using either JPEG or HEX encoding depending on the image mode (and\nwhether JPEG support is available or not).\n\nWhen calling :py:meth:`~PIL.Image.Image.save`, if a multiframe image is used,\nby default, only the first image will be saved. To save all frames, each frame\nto a separate page of the PDF, the ``save_all`` parameter must be present and\nset to ``True``.\n\nXV Thumbnails\n^^^^^^^^^^^^^\n\nPIL can read XV thumbnail files.\n\nIdentify-only formats\n---------------------\n\nBUFR\n^^^^\n\n.. versionadded:: 1.1.3\n\nPIL provides a stub driver for BUFR files.\n\nTo add read or write support to your application, use\n:py:func:`PIL.BufrStubImagePlugin.register_handler`.\n\nFITS\n^^^^\n\n.. versionadded:: 1.1.5\n\nPIL provides a stub driver for FITS files.\n\nTo add read or write support to your application, use\n:py:func:`PIL.FitsStubImagePlugin.register_handler`.\n\nGRIB\n^^^^\n\n.. versionadded:: 1.1.5\n\nPIL provides a stub driver for GRIB files.\n\nThe driver requires the file to start with a GRIB header. If you have files\nwith embedded GRIB data, or files with multiple GRIB fields, your application\nhas to seek to the header before passing the file handle to PIL.\n\nTo add read or write support to your application, use\n:py:func:`PIL.GribStubImagePlugin.register_handler`.\n\nHDF5\n^^^^\n\n.. versionadded:: 1.1.5\n\nPIL provides a stub driver for HDF5 files.\n\nTo add read or write support to your application, use\n:py:func:`PIL.Hdf5StubImagePlugin.register_handler`.\n\nMPEG\n^^^^\n\nPIL identifies MPEG files.\n\nWMF\n^^^\n\nPIL can identify playable WMF files.\n\nIn PIL 1.1.4 and earlier, the WMF driver provides some limited rendering\nsupport, but not enough to be useful for any real application.\n\nIn PIL 1.1.5 and later, the WMF driver is a stub driver. To add WMF read or\nwrite support to your application, use\n:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF handler.\n\n::\n\n    from PIL import Image\n    from PIL import WmfImagePlugin\n\n    class WmfHandler:\n        def open(self, im):\n            ...\n        def load(self, im):\n            ...\n            return image\n        def save(self, im, fp, filename):\n            ...\n\n    wmf_handler = WmfHandler()\n\n    WmfImagePlugin.register_handler(wmf_handler)\n\n    im = Image.open(\"sample.wmf\")\n",
            "new_file_content": ".. _image-file-formats:\n\nImage file formats\n==================\n\nThe Python Imaging Library supports a wide variety of raster file formats.\nOver 30 different file formats can be identified and read by the library.\nWrite support is less extensive, but most common interchange and presentation\nformats are supported.\n\nThe :py:meth:`~PIL.Image.Image.open` function identifies files from their\ncontents, not their names, but the :py:meth:`~PIL.Image.Image.save` method\nlooks at the name to determine which format to use, unless the format is given\nexplicitly.\n\nFully supported formats\n-----------------------\n\n.. contents::\n\nBMP\n^^^\n\nPIL reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``,\nor ``RGB`` data. 16-colour images are read as ``P`` images. Run-length encoding\nis not supported.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**compression**\n    Set to ``bmp_rle`` if the file is run-length encoded.\n\nEPS\n^^^\n\nPIL identifies EPS files containing image data, and can read files that contain\nembedded raster images (ImageData descriptors). If Ghostscript is available,\nother EPS files can be read as well. The EPS driver can also write EPS\nimages. The EPS driver can read EPS images in ``L``, ``LAB``, ``RGB`` and\n``CMYK`` mode, but Ghostscript may convert the images to ``RGB`` mode rather\nthan leaving them in the original color space. The EPS driver can write images\nin ``L``, ``RGB`` and ``CMYK`` modes.\n\nIf Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`\nmethod with the following parameter to affect how Ghostscript renders the EPS\n\n**scale**\n    Affects the scale of the resultant rasterized image. If the EPS suggests\n    that the image be rendered at 100px x 100px, setting this parameter to\n    2 will make the Ghostscript render a 200px x 200px image instead. The\n    relative position of the bounding box is maintained::\n\n        im = Image.open(...)\n        im.size #(100,100)\n        im.load(scale=2)\n        im.size #(200,200)\n\nGIF\n^^^\n\nPIL reads GIF87a and GIF89a versions of the GIF file format. The library writes\nrun-length encoded files in GIF87a by default, unless GIF89a features\nare used or GIF89a is already in use.\n\nNote that GIF files are always read as grayscale (``L``)\nor palette mode (``P``) images.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**background**\n    Default background color (a palette color index).\n\n**transparency**\n    Transparency color index. This key is omitted if the image is not\n    transparent.\n\n**version**\n    Version (either ``GIF87a`` or ``GIF89a``).\n\n**duration**\n    May not be present. The time to display the current frame\n    of the GIF, in milliseconds.\n\n**loop**\n    May not be present. The number of times the GIF should loop.\n\nReading sequences\n~~~~~~~~~~~~~~~~~\n\nThe GIF loader supports the :py:meth:`~file.seek` and :py:meth:`~file.tell`\nmethods. You can seek to the next frame (``im.seek(im.tell() + 1)``), or rewind\nthe file by seeking to the first frame. Random access is not supported.\n\n``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame.\n\nSaving\n~~~~~~\n\nWhen calling :py:meth:`~PIL.Image.Image.save`, the following options\nare available::\n\n    im.save(out, save_all=True, append_images=[im1, im2, ...])\n\n**save_all**\n    If present and true, all frames of the image will be saved. If\n    not, then only the first frame of a multiframe image will be saved.\n\n**append_images**\n    A list of images to append as additional frames. Each of the\n    images in the list can be single or multiframe images.\n    This is currently only supported for GIF, PDF, TIFF, and WebP.\n\n**duration**\n    The display duration of each frame of the multiframe gif, in\n    milliseconds. Pass a single integer for a constant duration, or a\n    list or tuple to set the duration for each frame separately.\n\n**loop**\n    Integer number of times the GIF should loop.\n\n**optimize**\n    If present and true, attempt to compress the palette by\n    eliminating unused colors. This is only useful if the palette can\n    be compressed to the next smaller power of 2 elements.\n\n**palette**\n    Use the specified palette for the saved image. The palette should\n    be a bytes or bytearray object containing the palette entries in\n    RGBRGB... form. It should be no more than 768 bytes. Alternately,\n    the palette can be passed in as an\n    :py:class:`PIL.ImagePalette.ImagePalette` object.\n\n**disposal**\n    Indicates the way in which the graphic is to be treated after being displayed.\n\n    * 0 - No disposal specified.\n    * 1 - Do not dispose.\n    * 2 - Restore to background color.\n    * 3 - Restore to previous content.\n\n     Pass a single integer for a constant disposal, or a list or tuple\n     to set the disposal for each frame separately. \n\nReading local images\n~~~~~~~~~~~~~~~~~~~~\n\nThe GIF loader creates an image memory the same size as the GIF file’s *logical\nscreen size*, and pastes the actual pixel data (the *local image*) into this\nimage. If you only want the actual pixel rectangle, you can manipulate the\n:py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.Image.Image.tile`\nattributes before loading the file::\n\n    im = Image.open(...)\n\n    if im.tile[0][0] == \"gif\":\n        # only read the first \"local image\" from this GIF file\n        tag, (x0, y0, x1, y1), offset, extra = im.tile[0]\n        im.size = (x1 - x0, y1 - y0)\n        im.tile = [(tag, (0, 0) + im.size, offset, extra)]\n\nICNS\n^^^^\n\nPIL reads and (macOS only) writes macOS ``.icns`` files.  By default, the\nlargest available icon is read, though you can override this by setting the\n:py:attr:`~PIL.Image.Image.size` property before calling\n:py:meth:`~PIL.Image.Image.load`.  The :py:meth:`~PIL.Image.Image.open` method\nsets the following :py:attr:`~PIL.Image.Image.info` property:\n\n**sizes**\n    A list of supported sizes found in this icon file; these are a\n    3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina\n    icon and 1 for a standard icon.  You *are* permitted to use this 3-tuple\n    format for the :py:attr:`~PIL.Image.Image.size` property if you set it\n    before calling :py:meth:`~PIL.Image.Image.load`; after loading, the size\n    will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you\n    ask for ``(512, 512, 2)``, the final value of\n    :py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``).\n\nICO\n^^^\n\nICO is used to store icons on Windows. The largest available icon is read.\n\nThe :py:meth:`~PIL.Image.Image.save` method supports the following options:\n\n**sizes**\n    A list of sizes including in this ico file; these are a 2-tuple,\n    ``(width, height)``; Default to ``[(16, 16), (24, 24), (32, 32), (48, 48),\n    (64, 64), (128, 128), (256, 256)]``. Any sizes bigger than the original\n    size or 256 will be ignored.\n\nIM\n^^\n\nIM is a format used by LabEye and other applications based on the IFUNC image\nprocessing library. The library reads and writes most uncompressed interchange\nversions of this format.\n\nIM is the only format that can store all internal PIL formats.\n\nJPEG\n^^^^\n\nPIL reads JPEG, JFIF, and Adobe JPEG files containing ``L``, ``RGB``, or\n``CMYK`` data. It writes standard and progressive JFIF files.\n\nUsing the :py:meth:`~PIL.Image.Image.draft` method, you can speed things up by\nconverting ``RGB`` images to ``L``, and resize images to 1/2, 1/4 or 1/8 of\ntheir original size while loading them.\n\nThe :py:meth:`~PIL.Image.Image.open` method may set the following\n:py:attr:`~PIL.Image.Image.info` properties if available:\n\n**jfif**\n    JFIF application marker found. If the file is not a JFIF file, this key is\n    not present.\n\n**jfif_version**\n    A tuple representing the jfif version, (major version, minor version).\n\n**jfif_density**\n    A tuple representing the pixel density of the image, in units specified\n    by jfif_unit.\n\n**jfif_unit**\n    Units for the jfif_density:\n\n    * 0 - No Units\n    * 1 - Pixels per Inch\n    * 2 - Pixels per Centimeter\n\n**dpi**\n    A tuple representing the reported pixel density in pixels per inch, if\n    the file is a jfif file and the units are in inches.\n\n**adobe**\n    Adobe application marker found. If the file is not an Adobe JPEG file, this\n    key is not present.\n\n**adobe_transform**\n    Vendor Specific Tag.\n\n**progression**\n    Indicates that this is a progressive JPEG file.\n\n**icc_profile**\n    The ICC color profile for the image.\n\n**exif**\n    Raw EXIF data from the image.\n\n\nThe :py:meth:`~PIL.Image.Image.save` method supports the following options:\n\n**quality**\n    The image quality, on a scale from 1 (worst) to 95 (best). The default is\n    75. Values above 95 should be avoided; 100 disables portions of the JPEG\n    compression algorithm, and results in large files with hardly any gain in\n    image quality.\n\n**optimize**\n    If present and true, indicates that the encoder should make an extra pass\n    over the image in order to select optimal encoder settings.\n\n**progressive**\n    If present and true, indicates that this image should be stored as a\n    progressive JPEG file.\n\n**dpi**\n    A tuple of integers representing the pixel density, ``(x,y)``.\n\n**icc_profile**\n    If present and true, the image is stored with the provided ICC profile.\n    If this parameter is not provided, the image will be saved with no profile\n    attached. To preserve the existing profile::\n\n        im.save(filename, 'jpeg', icc_profile=im.info.get('icc_profile'))\n\n**exif**\n    If present, the image will be stored with the provided raw EXIF data.\n\n**subsampling**\n    If present, sets the subsampling for the encoder.\n\n    * ``keep``: Only valid for JPEG files, will retain the original image setting.\n    * ``4:4:4``, ``4:2:2``, ``4:2:0``: Specific sampling values\n    * ``-1``: equivalent to ``keep``\n    * ``0``: equivalent to ``4:4:4``\n    * ``1``: equivalent to ``4:2:2``\n    * ``2``: equivalent to ``4:2:0``\n\n**qtables**\n    If present, sets the qtables for the encoder. This is listed as an\n    advanced option for wizards in the JPEG documentation. Use with\n    caution. ``qtables`` can be one of several types of values:\n\n    *  a string, naming a preset, e.g. ``keep``, ``web_low``, or ``web_high``\n    *  a list, tuple, or dictionary (with integer keys =\n       range(len(keys))) of lists of 64 integers. There must be\n       between 2 and 4 tables.\n\n    .. versionadded:: 2.5.0\n\n\n.. note::\n\n    To enable JPEG support, you need to build and install the IJG JPEG library\n    before building the Python Imaging Library. See the distribution README for\n    details.\n\nJPEG 2000\n^^^^^^^^^\n\n.. versionadded:: 2.4.0\n\nPIL reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB`` or\n``RGBA`` data.  It can also read files containing ``YCbCr`` data, which it\nconverts on read into ``RGB`` or ``RGBA`` depending on whether or not there is\nan alpha channel.  PIL supports JPEG 2000 raw codestreams (``.j2k`` files), as\nwell as boxed JPEG 2000 files (``.j2p`` or ``.jpx`` files).  PIL does *not*\nsupport files whose components have different sampling frequencies.\n\nWhen loading, if you set the ``mode`` on the image prior to the\n:py:meth:`~PIL.Image.Image.load` method being invoked, you can ask PIL to\nconvert the image to either ``RGB`` or ``RGBA`` rather than choosing for\nitself.  It is also possible to set ``reduce`` to the number of resolutions to\ndiscard (each one reduces the size of the resulting image by a factor of 2),\nand ``layers`` to specify the number of quality layers to load.\n\nThe :py:meth:`~PIL.Image.Image.save` method supports the following options:\n\n**offset**\n    The image offset, as a tuple of integers, e.g. (16, 16)\n\n**tile_offset**\n    The tile offset, again as a 2-tuple of integers.\n\n**tile_size**\n    The tile size as a 2-tuple.  If not specified, or if set to None, the\n    image will be saved without tiling.\n\n**quality_mode**\n    Either `\"rates\"` or `\"dB\"` depending on the units you want to use to\n    specify image quality.\n\n**quality_layers**\n    A sequence of numbers, each of which represents either an approximate size\n    reduction (if quality mode is `\"rates\"`) or a signal to noise ratio value\n    in decibels.  If not specified, defaults to a single layer of full quality.\n\n**num_resolutions**\n    The number of different image resolutions to be stored (which corresponds\n    to the number of Discrete Wavelet Transform decompositions plus one).\n\n**codeblock_size**\n    The code-block size as a 2-tuple.  Minimum size is 4 x 4, maximum is 1024 x\n    1024, with the additional restriction that no code-block may have more\n    than 4096 coefficients (i.e. the product of the two numbers must be no\n    greater than 4096).\n\n**precinct_size**\n    The precinct size as a 2-tuple.  Must be a power of two along both axes,\n    and must be greater than the code-block size.\n\n**irreversible**\n    If ``True``, use the lossy Irreversible Color Transformation\n    followed by DWT 9-7.  Defaults to ``False``, which means to use the\n    Reversible Color Transformation with DWT 5-3.\n\n**progression**\n    Controls the progression order; must be one of ``\"LRCP\"``, ``\"RLCP\"``,\n    ``\"RPCL\"``, ``\"PCRL\"``, ``\"CPRL\"``.  The letters stand for Component,\n    Position, Resolution and Layer respectively and control the order of\n    encoding, the idea being that e.g. an image encoded using LRCP mode can\n    have its quality layers decoded as they arrive at the decoder, while one\n    encoded using RLCP mode will have increasing resolutions decoded as they\n    arrive, and so on.\n\n**cinema_mode**\n    Set the encoder to produce output compliant with the digital cinema\n    specifications.  The options here are ``\"no\"`` (the default),\n    ``\"cinema2k-24\"`` for 24fps 2K, ``\"cinema2k-48\"`` for 48fps 2K, and\n    ``\"cinema4k-24\"`` for 24fps 4K.  Note that for compliant 2K files,\n    *at least one* of your image dimensions must match 2048 x 1080, while\n    for compliant 4K files, *at least one* of the dimensions must match\n    4096 x 2160.\n\n.. note::\n\n   To enable JPEG 2000 support, you need to build and install the OpenJPEG\n   library, version 2.0.0 or higher, before building the Python Imaging\n   Library.\n\n   Windows users can install the OpenJPEG binaries available on the\n   OpenJPEG website, but must add them to their PATH in order to use PIL (if\n   you fail to do this, you will get errors about not being able to load the\n   ``_imaging`` DLL).\n\nMSP\n^^^\n\nPIL identifies and reads MSP files from Windows 1 and 2. The library writes\nuncompressed (Windows 1) versions of this format.\n\nPCX\n^^^\n\nPIL reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data.\n\nPNG\n^^^\n\nPIL identifies, reads, and writes PNG files containing ``1``, ``L``, ``P``,\n``RGB``, or ``RGBA`` data. Interlaced files are supported as of v1.1.7.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties, when appropriate:\n\n**chromaticity**\n    The chromaticity points, as an 8 tuple of floats. (``White Point\n    X``, ``White Point Y``, ``Red X``, ``Red Y``, ``Green X``, ``Green\n    Y``, ``Blue X``, ``Blue Y``)\n\n**gamma**\n    Gamma, given as a floating point number.\n\n**srgb**\n    The sRGB rendering intent as an integer.\n\n      * 0 Perceptual\n      * 1 Relative Colorimetric\n      * 2 Saturation\n      * 3 Absolute Colorimetric\n\n**transparency**\n    For ``P`` images: Either the palette index for full transparent pixels,\n    or a byte string with alpha values for each palette entry.\n\n    For ``L`` and ``RGB`` images, the color that represents full transparent\n    pixels in this image.\n\n    This key is omitted if the image is not a transparent palette image.\n\n``Open`` also sets ``Image.text`` to a list of the values of the\n``tEXt``, ``zTXt``, and ``iTXt`` chunks of the PNG image. Individual\ncompressed chunks are limited to a decompressed size of\n``PngImagePlugin.MAX_TEXT_CHUNK``, by default 1MB, to prevent\ndecompression bombs. Additionally, the total size of all of the text\nchunks is limited to ``PngImagePlugin.MAX_TEXT_MEMORY``, defaulting to\n64MB.\n\nThe :py:meth:`~PIL.Image.Image.save` method supports the following options:\n\n**optimize**\n    If present and true, instructs the PNG writer to make the output file as\n    small as possible. This includes extra processing in order to find optimal\n    encoder settings.\n\n**transparency**\n    For ``P``, ``L``, and ``RGB`` images, this option controls what\n    color image to mark as transparent.\n\n    For ``P`` images, this can be a either the palette index,\n    or a byte string with alpha values for each palette entry.\n\n**dpi**\n    A tuple of two numbers corresponding to the desired dpi in each direction.\n\n**pnginfo**\n    A :py:class:`PIL.PngImagePlugin.PngInfo` instance containing text tags.\n\n**compress_level**\n    ZLIB compression level, a number between 0 and 9: 1 gives best speed,\n    9 gives best compression, 0 gives no compression at all. Default is 6.\n    When ``optimize`` option is True ``compress_level`` has no effect\n    (it is set to 9 regardless of a value passed).\n\n**icc_profile**\n    The ICC Profile to include in the saved file.\n\n**bits (experimental)**\n    For ``P`` images, this option controls how many bits to store. If omitted,\n    the PNG writer uses 8 bits (256 colors).\n\n**dictionary (experimental)**\n    Set the ZLIB encoder dictionary.\n\n.. note::\n\n    To enable PNG support, you need to build and install the ZLIB compression\n    library before building the Python Imaging Library. See the installation\n    documentation for details.\n\nPPM\n^^^\n\nPIL reads and writes PBM, PGM and PPM files containing ``1``, ``L`` or ``RGB``\ndata.\n\nSGI\n^^^\n\nPillow reads and writes uncompressed ``L``, ``RGB``, and ``RGBA`` files.\n\n\nSPIDER\n^^^^^^\n\nPIL reads and writes SPIDER image files of 32-bit floating point data\n(\"F;32F\").\n\nPIL also reads SPIDER stack files containing sequences of SPIDER images. The\n:py:meth:`~file.seek` and :py:meth:`~file.tell` methods are supported, and\nrandom access is allowed.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following attributes:\n\n**format**\n    Set to ``SPIDER``\n\n**istack**\n    Set to 1 if the file is an image stack, else 0.\n\n**nimages**\n    Set to the number of images in the stack.\n\nA convenience method, :py:meth:`~PIL.Image.Image.convert2byte`, is provided for\nconverting floating point data to byte data (mode ``L``)::\n\n    im = Image.open('image001.spi').convert2byte()\n\nWriting files in SPIDER format\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe extension of SPIDER files may be any 3 alphanumeric characters. Therefore\nthe output format must be specified explicitly::\n\n    im.save('newimage.spi', format='SPIDER')\n\nFor more information about the SPIDER image processing package, see the\n`SPIDER homepage`_ at `Wadsworth Center`_.\n\n.. _SPIDER homepage: https://spider.wadsworth.org/spider_doc/spider/docs/spider.html\n.. _Wadsworth Center: https://www.wadsworth.org/\n\nTIFF\n^^^^\n\nPIL reads and writes TIFF files. It can read both striped and tiled images,\npixel and plane interleaved multi-band images, and either uncompressed, or\nPackbits, LZW, or JPEG compressed images.\n\nIf you have libtiff and its headers installed, PIL can read and write many more\nkinds of compressed TIFF files. If not, PIL will always write uncompressed\nfiles.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**compression**\n    Compression mode.\n\n    .. versionadded:: 2.0.0\n\n**dpi**\n    Image resolution as an ``(xdpi, ydpi)`` tuple, where applicable. You can use\n    the :py:attr:`~PIL.Image.Image.tag` attribute to get more detailed\n    information about the image resolution.\n\n    .. versionadded:: 1.1.5\n\n**resolution**\n    Image resolution as an ``(xres, yres)`` tuple, where applicable. This is a\n    measurement in whichever unit is specified by the file.\n\n    .. versionadded:: 1.1.5\n\n\nThe :py:attr:`~PIL.Image.Image.tag_v2` attribute contains a dictionary\nof TIFF metadata. The keys are numerical indexes from\n:py:attr:`~PIL.TiffTags.TAGS_V2`.  Values are strings or numbers for single\nitems, multiple values are returned in a tuple of values. Rational\nnumbers are returned as a :py:class:`~PIL.TiffImagePlugin.IFDRational`\nobject.\n\n    .. versionadded:: 3.0.0\n\nFor compatibility with legacy code, the\n:py:attr:`~PIL.Image.Image.tag` attribute contains a dictionary of\ndecoded TIFF fields as returned prior to version 3.0.0.  Values are\nreturned as either strings or tuples of numeric values. Rational\nnumbers are returned as a tuple of ``(numerator, denominator)``.\n\n    .. deprecated:: 3.0.0\n\n\nSaving Tiff Images\n~~~~~~~~~~~~~~~~~~\n\nThe :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:\n\n**save_all**\n    If true, Pillow will save all frames of the image to a multiframe tiff document.\n\n    .. versionadded:: 3.4.0\n\n**tiffinfo**\n    A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` object or dict\n    object containing tiff tags and values. The TIFF field type is\n    autodetected for Numeric and string values, any other types\n    require using an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`\n    object and setting the type in\n    :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` with\n    the appropriate numerical value from\n    ``TiffTags.TYPES``.\n\n    .. versionadded:: 2.3.0\n\n    Metadata values that are of the rational type should be passed in\n    using a :py:class:`~PIL.TiffImagePlugin.IFDRational` object.\n\n    .. versionadded:: 3.1.0\n\n    For compatibility with legacy code, a\n    :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may\n    be passed in this field. However, this is deprecated.\n\n    .. versionadded:: 3.0.0\n\n .. note::\n\n    Only some tags are currently supported when writing using\n    libtiff. The supported list is found in\n    :py:attr:`~PIL:TiffTags.LIBTIFF_CORE`.\n\n**compression**\n    A string containing the desired compression method for the\n    file. (valid only with libtiff installed) Valid compression\n    methods are: ``None``, ``\"tiff_ccitt\"``, ``\"group3\"``,\n    ``\"group4\"``, ``\"tiff_jpeg\"``, ``\"tiff_adobe_deflate\"``,\n    ``\"tiff_thunderscan\"``, ``\"tiff_deflate\"``, ``\"tiff_sgilog\"``,\n    ``\"tiff_sgilog24\"``, ``\"tiff_raw_16\"``\n\nThese arguments to set the tiff header fields are an alternative to\nusing the general tags available through tiffinfo.\n\n**description**\n\n**software**\n\n**date_time**\n\n**artist**\n\n**copyright**\n    Strings\n\n**resolution_unit**\n    A string of \"inch\", \"centimeter\" or \"cm\"\n\n**resolution**\n\n**x_resolution**\n\n**y_resolution**\n\n**dpi**\n    Either a Float, 2 tuple of (numerator, denominator) or a\n    :py:class:`~PIL.TiffImagePlugin.IFDRational`. Resolution implies\n    an equal x and y resolution, dpi also implies a unit of inches.\n\n\nWebP\n^^^^\n\nPIL reads and writes WebP files. The specifics of PIL's capabilities with this\nformat are currently undocumented.\n\nThe :py:meth:`~PIL.Image.Image.save` method supports the following options:\n\n**lossless**\n    If present and true, instructs the WebP writer to use lossless compression.\n\n**quality**\n    Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest\n    size and 100 the largest. For lossless, this parameter is the amount\n    of effort put into the compression: 0 is the fastest, but gives larger\n    files compared to the slowest, but best, 100.\n\n**method**\n    Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 0.\n\n**icc_procfile**\n    The ICC Profile to include in the saved file. Only supported if\n    the system WebP library was built with webpmux support.\n\n**exif**\n    The exif data to include in the saved file. Only supported if\n    the system WebP library was built with webpmux support.\n\nSaving sequences\n~~~~~~~~~~~~~~~~~\n\n.. note::\n\n    Support for animated WebP files will only be enabled if the system WebP\n    library is v0.5.0 or later. You can check webp animation support at\n    runtime by calling `features.check(\"webp_anim\")`.\n\nWhen calling :py:meth:`~PIL.Image.Image.save`, the following options\nare available when the `save_all` argument is present and true.\n\n**append_images**\n    A list of images to append as additional frames. Each of the\n    images in the list can be single or multiframe images.\n\n**duration**\n    The display duration of each frame, in milliseconds. Pass a single\n    integer for a constant duration, or a list or tuple to set the\n    duration for each frame separately.\n\n**loop**\n    Number of times to repeat the animation. Defaults to [0 = infinite].\n\n**background**\n    Background color of the canvas, as an RGBA tuple with values in\n    the range of (0-255).\n\n**minimize_size**\n    If true, minimize the output size (slow). Implicitly disables\n    key-frame insertion.\n\n**kmin, kmax**\n    Minimum and maximum distance between consecutive key frames in\n    the output. The library may insert some key frames as needed\n    to satisfy this criteria. Note that these conditions should\n    hold: kmax > kmin and kmin >= kmax / 2 + 1. Also, if kmax <= 0,\n    then key-frame insertion is disabled; and if kmax == 1, then all\n    frames will be key-frames (kmin value does not matter for these\n    special cases).\n\n**allow_mixed**\n    If true, use mixed compression mode; the encoder heuristically\n    chooses between lossy and lossless for each frame.\n\nXBM\n^^^\n\nPIL reads and writes X bitmap files (mode ``1``).\n\nRead-only formats\n-----------------\n\nCUR\n^^^\n\nCUR is used to store cursors on Windows. The CUR decoder reads the largest\navailable cursor. Animated cursors are not supported.\n\nDCX\n^^^\n\nDCX is a container file format for PCX files, defined by Intel. The DCX format\nis commonly used in fax applications. The DCX decoder can read files containing\n``1``, ``L``, ``P``, or ``RGB`` data.\n\nWhen the file is opened, only the first image is read. You can use\n:py:meth:`~file.seek` or :py:mod:`~PIL.ImageSequence` to read other images.\n\n\nDDS\n^^^\n\nDDS is a popular container texture format used in video games and natively\nsupported by DirectX.\nCurrently, DXT1, DXT3, and DXT5 pixel formats are supported and only in ``RGBA``\nmode.\n\n.. versionadded:: 3.4.0 DXT3\n\nFLI, FLC\n^^^^^^^^\n\nPIL reads Autodesk FLI and FLC animations.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**duration**\n    The delay (in milliseconds) between each frame.\n\nFPX\n^^^\n\nPIL reads Kodak FlashPix files. In the current version, only the highest\nresolution image is read from the file, and the viewing transform is not taken\ninto account.\n\n.. note::\n\n    To enable full FlashPix support, you need to build and install the IJG JPEG\n    library before building the Python Imaging Library. See the distribution\n    README for details.\n\nFTEX\n^^^^\n\n.. versionadded:: 3.2.0\n\nThe FTEX decoder reads textures used for 3D objects in\nIndependence War 2: Edge Of Chaos. The plugin reads a single texture\nper file, in the compressed and uncompressed formats.\n\nGBR\n^^^\n\nThe GBR decoder reads GIMP brush files, version 1 and 2.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**comment**\n    The brush name.\n\n**spacing**\n    The spacing between the brushes, in pixels. Version 2 only.\n\nGD\n^^\n\nPIL reads uncompressed GD files. Note that this file format cannot be\nautomatically identified, so you must use :py:func:`PIL.GdImageFile.open` to\nread such a file.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**transparency**\n    Transparency color index. This key is omitted if the image is not\n    transparent.\n\nIMT\n^^^\n\nPIL reads Image Tools images containing ``L`` data.\n\nIPTC/NAA\n^^^^^^^^\n\nPIL provides limited read support for IPTC/NAA newsphoto files.\n\nMCIDAS\n^^^^^^\n\nPIL identifies and reads 8-bit McIdas area files.\n\nMIC\n^^^\n\nPIL identifies and reads Microsoft Image Composer (MIC) files. When opened, the\nfirst sprite in the file is loaded. You can use :py:meth:`~file.seek` and\n:py:meth:`~file.tell` to read other sprites from the file.\n\nNote that there may be an embedded gamma of 2.2 in MIC files.\n\nMPO\n^^^\n\nPillow identifies and reads Multi Picture Object (MPO) files, loading the primary\nimage when first opened. The :py:meth:`~file.seek` and :py:meth:`~file.tell`\nmethods may be used to read other pictures from the file. The pictures are\nzero-indexed and random access is supported.\n\nPCD\n^^^\n\nPIL reads PhotoCD files containing ``RGB`` data. This only reads the 768x512\nresolution image from the file. Higher resolutions are encoded in a proprietary\nencoding.\n\nPIXAR\n^^^^^\n\nPIL provides limited support for PIXAR raster files. The library can identify\nand read “dumped” RGB files.\n\nThe format code is ``PIXAR``.\n\nPSD\n^^^\n\nPIL identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.\n\n\nTGA\n^^^\n\nPIL reads 24- and 32-bit uncompressed and run-length encoded TGA files.\n\nWAL\n^^^\n\n.. versionadded:: 1.1.4\n\nPIL reads Quake2 WAL texture files.\n\nNote that this file format cannot be automatically identified, so you must use\nthe open function in the :py:mod:`~PIL.WalImageFile` module to read files in\nthis format.\n\nBy default, a Quake2 standard palette is attached to the texture. To override\nthe palette, use the putpalette method.\n\nXPM\n^^^\n\nPIL reads X pixmap files (mode ``P``) with 256 colors or less.\n\nThe :py:meth:`~PIL.Image.Image.open` method sets the following\n:py:attr:`~PIL.Image.Image.info` properties:\n\n**transparency**\n    Transparency color index. This key is omitted if the image is not\n    transparent.\n\nWrite-only formats\n------------------\n\nPALM\n^^^^\n\nPIL provides write-only support for PALM pixmap files.\n\nThe format code is ``Palm``, the extension is ``.palm``.\n\nPDF\n^^^\n\nPIL can write PDF (Acrobat) images. Such images are written as binary PDF 1.1\nfiles, using either JPEG or HEX encoding depending on the image mode (and\nwhether JPEG support is available or not).\n\nWhen calling :py:meth:`~PIL.Image.Image.save`, if a multiframe image is used,\nby default, only the first image will be saved. To save all frames, each frame\nto a separate page of the PDF, the ``save_all`` parameter must be present and\nset to ``True``.\n\nXV Thumbnails\n^^^^^^^^^^^^^\n\nPIL can read XV thumbnail files.\n\nIdentify-only formats\n---------------------\n\nBUFR\n^^^^\n\n.. versionadded:: 1.1.3\n\nPIL provides a stub driver for BUFR files.\n\nTo add read or write support to your application, use\n:py:func:`PIL.BufrStubImagePlugin.register_handler`.\n\nFITS\n^^^^\n\n.. versionadded:: 1.1.5\n\nPIL provides a stub driver for FITS files.\n\nTo add read or write support to your application, use\n:py:func:`PIL.FitsStubImagePlugin.register_handler`.\n\nGRIB\n^^^^\n\n.. versionadded:: 1.1.5\n\nPIL provides a stub driver for GRIB files.\n\nThe driver requires the file to start with a GRIB header. If you have files\nwith embedded GRIB data, or files with multiple GRIB fields, your application\nhas to seek to the header before passing the file handle to PIL.\n\nTo add read or write support to your application, use\n:py:func:`PIL.GribStubImagePlugin.register_handler`.\n\nHDF5\n^^^^\n\n.. versionadded:: 1.1.5\n\nPIL provides a stub driver for HDF5 files.\n\nTo add read or write support to your application, use\n:py:func:`PIL.Hdf5StubImagePlugin.register_handler`.\n\nMPEG\n^^^^\n\nPIL identifies MPEG files.\n\nWMF\n^^^\n\nPIL can identify playable WMF files.\n\nIn PIL 1.1.4 and earlier, the WMF driver provides some limited rendering\nsupport, but not enough to be useful for any real application.\n\nIn PIL 1.1.5 and later, the WMF driver is a stub driver. To add WMF read or\nwrite support to your application, use\n:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF handler.\n\n::\n\n    from PIL import Image\n    from PIL import WmfImagePlugin\n\n    class WmfHandler:\n        def open(self, im):\n            ...\n        def load(self, im):\n            ...\n            return image\n        def save(self, im, fp, filename):\n            ...\n\n    wmf_handler = WmfHandler()\n\n    WmfImagePlugin.register_handler(wmf_handler)\n\n    im = Image.open(\"sample.wmf\")\n",
            "header": {
                "file": {
                    "path": "docs/handbook/image-file-formats.rst"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "eba13d398",
                "new_commit_hash": "d88c089ba",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/docs/handbook/image-file-formats.rst"
            },
            "plus_file": {
                "path": "b/docs/handbook/image-file-formats.rst"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 132,
                            "length": 6
                        },
                        "new_range": {
                            "start": 132,
                            "length": 17
                        },
                        "section": "are available::"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "    the palette can be passed in as an",
                                "type": "context"
                            },
                            {
                                "content": "    :py:class:`PIL.ImagePalette.ImagePalette` object.",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "**disposal**",
                                "type": "added"
                            },
                            {
                                "content": "    Indicates the way in which the graphic is to be treated after being displayed.",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    * 0 - No disposal specified.",
                                "type": "added"
                            },
                            {
                                "content": "    * 1 - Do not dispose.",
                                "type": "added"
                            },
                            {
                                "content": "    * 2 - Restore to background color.",
                                "type": "added"
                            },
                            {
                                "content": "    * 3 - Restore to previous content.",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "     Pass a single integer for a constant disposal, or a list or tuple",
                                "type": "added"
                            },
                            {
                                "content": "     to set the disposal for each frame separately. ",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "Reading local images",
                                "type": "context"
                            },
                            {
                                "content": "~~~~~~~~~~~~~~~~~~~~",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "ff13d184d83c519752c9916740669e1d0968417c^",
    "new_commit_hash": "ff13d184d83c519752c9916740669e1d0968417c",
    "commit_message": "Merge pull request #2902 from wiredfool/pr_2388\n\nAdd disposal option when saving GIFs",
    "commit_date": "2017-12-27T12:15:00Z",
    "metadata": {}
}
{
    "repo_name": "pillow",
    "new_commit_hash": "ff13d184d83c519752c9916740669e1d0968417c",
    "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\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        from PIL import ImagePalette\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\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 18ms\nInstalled 12 packages in 174ms\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 795ms\nPrepared 1 package in 16.64s\nInstalled 1 package in 2ms\n + pillow==4.4.0.dev0 (from file:///home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_ff13d184d83c519752c9916740669e1d0968417c)\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_ff13d184d83c519752c9916740669e1d0968417c\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.59s ===================\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_ff13d184d83c519752c9916740669e1d0968417c\n....................................=== orphaned temp file: /tmp/tempg0kogalx.gif\n.=== orphaned temp file: /tmp/tempahkcbgou.gif\n\ncollected 37 items\n\n=================================== FAILURES ===================================\n__________ TestFileGif.test_save_dispose __________\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_ff13d184d83c519752c9916740669e1d0968417c/r2e_tests/test_1.py\", line 279, in test_save_dispose\n    self.assertEqual(img.disposal_method, method)\nAssertionError: 0 != 1\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\nFAILED 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.42s ===================\n",
    "old_commit_res_stderr": ""
}
PIL/GifImagePlugin.py Tests/test_file_gif.py docs/handbook/image-file-formats.rst
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** GIF Frames Saved with Incorrect Disposal Method

**Description:**
When saving animated GIFs with a specified disposal method, the resulting frames do not retain the correct disposal settings. This leads to unexpected rendering behavior when the GIF is played.

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

# Create a list of frames
im_list = [
    Image.new('L', (100, 100), '#000'),
    Image.new('L', (100, 100), '#111'),
    Image.new('L', (100, 100), '#222'),
]

# Save GIF with disposal method set to 1
im_list[0].save(
    'temp.gif',
    save_all=True,
    append_images=im_list[1:],
    disposal=1
)

# Open the saved GIF and check disposal method of the second frame
img = Image.open('temp.gif')
img.seek(1)
print(img.disposal_method)  # Expected output: 1
```

**Expected Behavior:**
Each frame in the saved GIF should have the specified disposal method applied. In the example above, the second frame should have a `disposal_method` value of `1`, ensuring that the previous frame is restored before rendering the current one.

**Actual Behavior:**
The `disposal_method` of the second frame is incorrectly set to `0`, ignoring the specified disposal method. This causes the GIF to not handle frame disposal as intended, potentially leading to visual artifacts during playback.
[/ISSUE]
--- END ISSUE ---

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

--- BEGIN FILE ---
```
### 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)

    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]
            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
    if transparent_color_exists or duration != 0:
        transparency_flag = 1 if transparent_color_exists else 0
        if not transparent_color_exists:
            transparency = 0

        fp.write(b"!" +
                 o8(249) +                # extension intro
                 o8(4) +                  # length
                 o8(transparency_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 Frames Saved with Incorrect Disposal Method

**Description:**
When saving animated GIFs with a specified disposal method, the resulting frames do not retain the correct disposal settings. This leads to unexpected rendering behavior when the GIF is played.

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

# Create a list of frames
im_list = [
    Image.new('L', (100, 100), '#000'),
    Image.new('L', (100, 100), '#111'),
    Image.new('L', (100, 100), '#222'),
]

# Save GIF with disposal method set to 1
im_list[0].save(
    'temp.gif',
    save_all=True,
    append_images=im_list[1:],
    disposal=1
)

# Open the saved GIF and check disposal method of the second frame
img = Image.open('temp.gif')
img.seek(1)
print(img.disposal_method)  # Expected output: 1
```

**Expected Behavior:**
Each frame in the saved GIF should have the specified disposal method applied. In the example above, the second frame should have a `disposal_method` value of `1`, ensuring that the previous frame is restored before rendering the current one.

**Actual Behavior:**
The `disposal_method` of the second frame is incorrectly set to `0`, ignoring the specified disposal method. This causes the GIF to not handle frame disposal as intended, potentially leading to visual artifacts during playback.
[/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/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py
index f227211a2..71d8ce59a 100644
--- a/PIL/GifImagePlugin.py
+++ b/PIL/GifImagePlugin.py
@@ -392,6 +392,7 @@ def _write_single_frame(im, fp, palette):
 def _write_multiple_frames(im, fp, palette):
 
     duration = im.encoderinfo.get("duration", None)
+    disposal = im.encoderinfo.get('disposal', None)
 
     im_frames = []
     frame_count = 0
@@ -404,6 +405,8 @@ def _write_multiple_frames(im, fp, palette):
             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:
@@ -503,15 +506,19 @@ def _write_local_header(fp, im, offset, flags):
         duration = int(im.encoderinfo["duration"] / 10)
     else:
         duration = 0
-    if transparent_color_exists or duration != 0:
-        transparency_flag = 1 if transparent_color_exists else 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(transparency_flag) +  # packed fields
+                 o8(packed_flag) +        # packed fields
                  o16(duration) +          # duration
                  o8(transparency) +       # transparency index
                  o8(0))

{"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\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\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            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    if transparent_color_exists or duration != 0:\n        transparency_flag = 1 if transparent_color_exists else 0\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(transparency_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 Frames Saved with Incorrect Disposal Method

**Description:**
When saving animated GIFs with a specified disposal method, the resulting frames do not retain the correct disposal settings. This leads to unexpected rendering behavior when the GIF is played.

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

# Create a list of frames
im_list = [
    Image.new('L', (100, 100), '#000'),
    Image.new('L', (100, 100), '#111'),
    Image.new('L', (100, 100), '#222'),
]

# Save GIF with disposal method set to 1
im_list[0].save(
    'temp.gif',
    save_all=True,
    append_images=im_list[1:],
    disposal=1
)

# Open the saved GIF and check disposal method of the second frame
img = Image.open('temp.gif')
img.seek(1)
print(img.disposal_method)  # Expected output: 1
```

**Expected Behavior:**
Each frame in the saved GIF should have the specified disposal method applied. In the example above, the second frame should have a `disposal_method` value of `1`, ensuring that the previous frame is restored before rendering the current one.

**Actual Behavior:**
The `disposal_method` of the second frame is incorrectly set to `0`, ignoring the specified disposal method. This causes the GIF to not handle frame disposal as intended, potentially leading to visual artifacts during playback.
[/ISSUE]
--- END ISSUE ---

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

--- BEGIN FILE ---
```
### 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)

    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]
            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
    if transparent_color_exists or duration != 0:
        transparency_flag = 1 if transparent_color_exists else 0
        if not transparent_color_exists:
            transparency = 0

        fp.write(b"!" +
                 o8(249) +                # extension intro
                 o8(4) +                  # length
                 o8(transparency_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