Search code examples
pythonimage-processinggifpython-imaging-library

Pillow - Resizing a GIF


I have a gif that I would like to resize with pillow so that its size decreases. The current size of the gif is 2MB.

I am trying to

  1. resize it so its height / width is smaller

  2. decrease its quality.

With JPEG, the following piece of code is usually enough so that large image drastically decrease in size.

from PIL import Image

im = Image.open("my_picture.jpg")
im = im.resize((im.size[0] // 2, im.size[1] // 2), Image.ANTIALIAS)  # decreases width and height of the image
im.save("out.jpg", optimize=True, quality=85)  # decreases its quality

With a GIF, though, it does not seem to work. The following piece of code even makes the out.gif bigger than the initial gif:

im = Image.open("my_gif.gif")
im.seek(im.tell() + 1)  # loads all frames
im.save("out.gif", save_all=True, optimize=True, quality=10)  # should decrease its quality

print(os.stat("my_gif.gif").st_size)  # 2096558 bytes / roughly 2MB
print(os.stat("out.gif").st_size)  # 7536404 bytes / roughly 7.5MB

If I add the following line, then only the first frame of the GIF is saved, instead of all of its frame.

im = im.resize((im.size[0] // 2, im.size[1] // 2), Image.ANTIALIAS)  # should decrease its size

I've been thinking about calling resize() on im.seek() or im.tell() but neither of these methods return an Image object, and therefore I cannot call resize() on their output.

Would you know how I can use Pillow to decrease the size of my GIF while keeping all of its frames?

[edit] Partial solution:

Following Old Bear's response, I have done the following changes:

  • I am using BigglesZX's script to extract all frames. It is useful to note that this is a Python 2 script, and my project is written in Python 3 (I did mention that detail initially, but it was edited out by the Stack Overflow Community). Running 2to3 -w gifextract.py makes that script compatible with Python 3.

  • I have been resicing each frame individually: frame.resize((frame.size[0] // 2, frame.size[1] // 2), Image.ANTIALIAS)

  • I've been saving all the frames together: img.save("out.gif", save_all=True, optimize=True).

The new gif is now saved and works, but there is 2 main problems :

  • I am not sure that the resize method works, as out.gif is still 7.5MB. The initial gif was 2MB.

  • The gif speed is increased and the gif does not loop. It stops after its first run.

Example:

original gif my_gif.gif:

Original gif

Gif after processing (out.gif) https://i.imgur.com/zDO4cE4.mp4 (I could not add it to Stack Overflow ). Imgur made it slower (and converted it to mp4). When I open the gif file from my computer, the entire gif lasts about 1.5 seconds.


Solution

  • Using BigglesZX's script, I have created a new script which resizes a GIF using Pillow.

    Original GIF (2.1 MB):

    Original gif

    Output GIF after resizing (1.7 MB):

    Output gif

    I have saved the script here. It is using the thumbnail method of Pillow rather than the resize method as I found the resize method did not work.

    The is not perfect so feel free to fork and improve it. Here are a few unresolved issues:

    • While the GIF displays just fine when hosted by imgur, there is a speed issue when I open it from my computer where the entire GIF only take 1.5 seconds.
    • Likewise, while imgur seems to make up for the speed problem, the GIF wouldn't display correctly when I tried to upload it to stack.imgur. Only the first frame was displayed (you can see it here).

    Full code (should the above gist be deleted):

    def resize_gif(path, save_as=None, resize_to=None):
        """
        Resizes the GIF to a given length:
    
        Args:
            path: the path to the GIF file
            save_as (optional): Path of the resized gif. If not set, the original gif will be overwritten.
            resize_to (optional): new size of the gif. Format: (int, int). If not set, the original GIF will be resized to
                                  half of its size.
        """
        all_frames = extract_and_resize_frames(path, resize_to)
    
        if not save_as:
            save_as = path
    
        if len(all_frames) == 1:
            print("Warning: only 1 frame found")
            all_frames[0].save(save_as, optimize=True)
        else:
            all_frames[0].save(save_as, optimize=True, save_all=True, append_images=all_frames[1:], loop=1000)
    
    
    def analyseImage(path):
        """
        Pre-process pass over the image to determine the mode (full or additive).
        Necessary as assessing single frames isn't reliable. Need to know the mode
        before processing all frames.
        """
        im = Image.open(path)
        results = {
            'size': im.size,
            'mode': 'full',
        }
        try:
            while True:
                if im.tile:
                    tile = im.tile[0]
                    update_region = tile[1]
                    update_region_dimensions = update_region[2:]
                    if update_region_dimensions != im.size:
                        results['mode'] = 'partial'
                        break
                im.seek(im.tell() + 1)
        except EOFError:
            pass
        return results
    
    
    def extract_and_resize_frames(path, resize_to=None):
        """
        Iterate the GIF, extracting each frame and resizing them
    
        Returns:
            An array of all frames
        """
        mode = analyseImage(path)['mode']
    
        im = Image.open(path)
    
        if not resize_to:
            resize_to = (im.size[0] // 2, im.size[1] // 2)
    
        i = 0
        p = im.getpalette()
        last_frame = im.convert('RGBA')
    
        all_frames = []
    
        try:
            while True:
                # print("saving %s (%s) frame %d, %s %s" % (path, mode, i, im.size, im.tile))
    
                '''
                If the GIF uses local colour tables, each frame will have its own palette.
                If not, we need to apply the global palette to the new frame.
                '''
                if not im.getpalette():
                    im.putpalette(p)
    
                new_frame = Image.new('RGBA', im.size)
    
                '''
                Is this file a "partial"-mode GIF where frames update a region of a different size to the entire image?
                If so, we need to construct the new frame by pasting it on top of the preceding frames.
                '''
                if mode == 'partial':
                    new_frame.paste(last_frame)
    
                new_frame.paste(im, (0, 0), im.convert('RGBA'))
    
                new_frame.thumbnail(resize_to, Image.ANTIALIAS)
                all_frames.append(new_frame)
    
                i += 1
                last_frame = new_frame
                im.seek(im.tell() + 1)
        except EOFError:
            pass
    
        return all_frames