mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-10-30 23:47:27 +03:00 
			
		
		
		
	Merge pull request #1384 from radarhere/gifparams
GIF 89a and animation parameters
This commit is contained in:
		
						commit
						a38fb2d0c5
					
				|  | @ -24,7 +24,8 @@ | |||
| # See the README file for information on usage and redistribution. | ||||
| # | ||||
| 
 | ||||
| from PIL import Image, ImageFile, ImagePalette, ImageChops, ImageSequence, _binary | ||||
| from PIL import Image, ImageFile, ImagePalette, \ | ||||
|                 ImageChops, ImageSequence, _binary | ||||
| 
 | ||||
| __version__ = "0.9" | ||||
| 
 | ||||
|  | @ -300,14 +301,17 @@ RAWMODE = { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| def _convert_mode(im): | ||||
| def _convert_mode(im, initial_call=False): | ||||
|     # convert on the fly (EXPERIMENTAL -- I'm not sure PIL | ||||
|     # should automatically convert images on save...) | ||||
|     if Image.getmodebase(im.mode) == "RGB": | ||||
|         palette_size = 256 | ||||
|         if im.palette: | ||||
|             palette_size = len(im.palette.getdata()[1]) // 3 | ||||
|         return im.convert("P", palette=1, colors=palette_size) | ||||
|         if initial_call: | ||||
|             palette_size = 256 | ||||
|             if im.palette: | ||||
|                 palette_size = len(im.palette.getdata()[1]) // 3 | ||||
|             return im.convert("P", palette=1, colors=palette_size) | ||||
|         else: | ||||
|             return im.convert("P") | ||||
|     return im.convert("L") | ||||
| 
 | ||||
| 
 | ||||
|  | @ -317,6 +321,7 @@ def _save_all(im, fp, filename): | |||
| 
 | ||||
| def _save(im, fp, filename, save_all=False): | ||||
| 
 | ||||
|     im.encoderinfo.update(im.info) | ||||
|     if _imaging_gif: | ||||
|         # call external driver | ||||
|         try: | ||||
|  | @ -328,7 +333,7 @@ def _save(im, fp, filename, save_all=False): | |||
|     if im.mode in RAWMODE: | ||||
|         im_out = im.copy() | ||||
|     else: | ||||
|         im_out = _convert_mode(im) | ||||
|         im_out = _convert_mode(im, True) | ||||
| 
 | ||||
|     # header | ||||
|     try: | ||||
|  | @ -340,6 +345,7 @@ def _save(im, fp, filename, save_all=False): | |||
|     if save_all: | ||||
|         previous = None | ||||
| 
 | ||||
|         first_frame = None | ||||
|         for im_frame in ImageSequence.Iterator(im): | ||||
|             im_frame = _convert_mode(im_frame) | ||||
| 
 | ||||
|  | @ -347,22 +353,30 @@ def _save(im, fp, filename, save_all=False): | |||
|             # e.g. getdata(im_frame, duration=1000) | ||||
|             if not previous: | ||||
|                 # global header | ||||
|                 for s in getheader(im_frame, palette, im.encoderinfo)[0] + getdata(im_frame): | ||||
|                     fp.write(s) | ||||
|                 first_frame = getheader(im_frame, palette, im.encoderinfo)[0] | ||||
|                 first_frame += getdata(im_frame, (0, 0), **im.encoderinfo) | ||||
|             else: | ||||
|                 if first_frame: | ||||
|                     for s in first_frame: | ||||
|                         fp.write(s) | ||||
|                     first_frame = None | ||||
| 
 | ||||
|                 # delta frame | ||||
|                 delta = ImageChops.subtract_modulo(im_frame, previous) | ||||
|                 delta = ImageChops.subtract_modulo(im_frame, previous.copy()) | ||||
|                 bbox = delta.getbbox() | ||||
| 
 | ||||
|                 if bbox: | ||||
|                     # compress difference | ||||
|                     for s in getdata(im_frame.crop(bbox), offset=bbox[:2]): | ||||
|                     for s in getdata(im_frame.crop(bbox), | ||||
|                                      bbox[:2], **im.encoderinfo): | ||||
|                         fp.write(s) | ||||
|                 else: | ||||
|                     # FIXME: what should we do in this case? | ||||
|                     pass | ||||
|             previous = im_frame.copy() | ||||
|     else: | ||||
|             previous = im_frame | ||||
|         if first_frame: | ||||
|             save_all = False | ||||
|     if not save_all: | ||||
|         header = getheader(im_out, palette, im.encoderinfo)[0] | ||||
|         for s in header: | ||||
|             fp.write(s) | ||||
|  | @ -533,8 +547,19 @@ def getheader(im, palette=None, info=None): | |||
| 
 | ||||
|     # Header Block | ||||
|     # http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp | ||||
| 
 | ||||
|     version = b"87a" | ||||
|     for extensionKey in ["transparency", "duration", "loop"]: | ||||
|         if info and extensionKey in info and \ | ||||
|                 not (extensionKey == "duration" and info[extensionKey] == 0): | ||||
|             version = b"89a" | ||||
|             break | ||||
|     else: | ||||
|         if im.info.get("version") == "89a": | ||||
|             version = b"89a" | ||||
| 
 | ||||
|     header = [ | ||||
|         b"GIF87a" +             # signature + version | ||||
|         b"GIF"+version +        # signature + version | ||||
|         o16(im.size[0]) +       # canvas width | ||||
|         o16(im.size[1])         # canvas height | ||||
|     ] | ||||
|  | @ -591,7 +616,16 @@ def getheader(im, palette=None, info=None): | |||
|     # size of global color table + global color table flag | ||||
|     header.append(o8(color_table_size + 128)) | ||||
|     # background + reserved/aspect | ||||
|     background = im.info["background"] if "background" in im.info else 0 | ||||
|     if info and "background" in info: | ||||
|         background = info["background"] | ||||
|     elif "background" in im.info: | ||||
|         # This elif is redundant within GifImagePlugin | ||||
|         # since im.info parameters are bundled into the info dictionary | ||||
|         # However, external scripts may call getheader directly | ||||
|         # So this maintains earlier behaviour | ||||
|         background = im.info["background"] | ||||
|     else: | ||||
|         background = 0 | ||||
|     header.append(o8(background) + o8(0)) | ||||
|     # end of Logical Screen Descriptor | ||||
| 
 | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ class TestFileGif(PillowTestCase): | |||
|         self.assertEqual(im.mode, "P") | ||||
|         self.assertEqual(im.size, (128, 128)) | ||||
|         self.assertEqual(im.format, "GIF") | ||||
|         self.assertEqual(im.info["version"], b"GIF89a") | ||||
| 
 | ||||
|     def test_invalid_file(self): | ||||
|         invalid_file = "Tests/images/flower.jpg" | ||||
|  | @ -95,6 +96,21 @@ class TestFileGif(PillowTestCase): | |||
| 
 | ||||
|         self.assertEqual(reread.n_frames, 5) | ||||
| 
 | ||||
|     def test_headers_saving_for_animated_gifs(self): | ||||
|         important_headers = ['background', 'version', 'duration', 'loop'] | ||||
|         # Multiframe image | ||||
|         im = Image.open("Tests/images/dispose_bgnd.gif") | ||||
| 
 | ||||
|         out = self.tempfile('temp.gif') | ||||
|         im.save(out, save_all=True) | ||||
|         reread = Image.open(out) | ||||
| 
 | ||||
|         for header in important_headers: | ||||
|             self.assertEqual( | ||||
|                 im.info[header], | ||||
|                 reread.info[header] | ||||
|             ) | ||||
| 
 | ||||
|     def test_palette_handling(self): | ||||
|         # see https://github.com/python-pillow/Pillow/issues/513 | ||||
| 
 | ||||
|  | @ -251,6 +267,34 @@ class TestFileGif(PillowTestCase): | |||
| 
 | ||||
|         self.assertEqual(reread.info['background'], im.info['background']) | ||||
| 
 | ||||
|     def test_version(self): | ||||
|         out = self.tempfile('temp.gif') | ||||
| 
 | ||||
|         # Test that GIF87a is used by default | ||||
|         im = Image.new('L', (100, 100), '#000') | ||||
|         im.save(out) | ||||
|         reread = Image.open(out) | ||||
|         self.assertEqual(reread.info["version"], b"GIF87a") | ||||
| 
 | ||||
|         # Test that adding a GIF89a feature changes the version | ||||
|         im.info["transparency"] = 1 | ||||
|         im.save(out) | ||||
|         reread = Image.open(out) | ||||
|         self.assertEqual(reread.info["version"], b"GIF89a") | ||||
| 
 | ||||
|         # Test that a GIF87a image is also saved in that format | ||||
|         im = Image.open(TEST_GIF) | ||||
|         im.save(out) | ||||
|         reread = Image.open(out) | ||||
|         self.assertEqual(reread.info["version"], b"GIF87a") | ||||
| 
 | ||||
|         # Test that a GIF89a image is also saved in that format | ||||
|         im.info["version"] = "GIF89a" | ||||
|         im.save(out) | ||||
|         reread = Image.open(out) | ||||
|         self.assertEqual(reread.info["version"], b"GIF87a") | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     unittest.main() | ||||
| 
 | ||||
|  |  | |||
|  | @ -54,8 +54,11 @@ GIF | |||
| ^^^ | ||||
| 
 | ||||
| PIL reads GIF87a and GIF89a versions of the GIF file format. The library writes | ||||
| run-length encoded GIF87a files. Note that GIF files are always read as | ||||
| grayscale (``L``) or palette mode (``P``) images. | ||||
| run-length encoded files in GIF87a by default, unless GIF89a features | ||||
| are used or GIF89a is already in use. | ||||
| 
 | ||||
| Note that GIF files are always read as grayscale (``L``) | ||||
| or palette mode (``P``) images. | ||||
| 
 | ||||
| The :py:meth:`~PIL.Image.Image.open` method sets the following | ||||
| :py:attr:`~PIL.Image.Image.info` properties: | ||||
|  | @ -73,12 +76,32 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following | |||
| **version** | ||||
|     Version (either ``GIF87a`` or ``GIF89a``). | ||||
| 
 | ||||
| **duration** | ||||
|     May not be present. The time to display each frame of the GIF, in | ||||
|     milliseconds. | ||||
| 
 | ||||
| **loop** | ||||
|     May not be present. The number of times the GIF should loop. | ||||
| 
 | ||||
| Reading sequences | ||||
| ~~~~~~~~~~~~~~~~~ | ||||
| 
 | ||||
| The GIF loader supports the :py:meth:`~file.seek` and :py:meth:`~file.tell` | ||||
| methods. You can seek to the next frame (``im.seek(im.tell() + 1)``), or rewind | ||||
| the file by seeking to the first frame. Random access is not supported. ``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame. | ||||
| the file by seeking to the first frame. Random access is not supported. | ||||
| 
 | ||||
| ``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame. | ||||
| 
 | ||||
| Saving sequences | ||||
| ~~~~~~~~~~~~~~~~ | ||||
| 
 | ||||
| When calling :py:meth:`~PIL.Image.Image.save`, if a multiframe image is used, | ||||
| by default only the first frame will be saved. To save all frames, the | ||||
| ``save_all`` parameter must be present and set to ``True``. | ||||
| 
 | ||||
| If present, the ``loop`` parameter can be used to set the number of times | ||||
| the GIF should loop, and the ``duration`` parameter can set the number of | ||||
| milliseconds between each frame. | ||||
| 
 | ||||
| Reading local images | ||||
| ~~~~~~~~~~~~~~~~~~~~ | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user