Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 399
Pauline Avatar asked Jan 18 '17 12:01

Pauline


People also ask

Can a GIF be resized?

In the Resize GIF section, enter its new dimensions in the Width and Height fields. To change the GIF proportion, unselect the Lock aspect ratio option. Click the Save button to download the resized GIF.

Does pillow support GIFs?

Pillow reads GIF87a and GIF89a versions of the GIF file format. The library writes files in GIF87a by default, unless GIF89a features are used or GIF89a is already in use. Files are written with LZW encoding.


1 Answers

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
like image 194
Pauline Avatar answered Sep 17 '22 10:09

Pauline