Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Find GIF disposal method #8563

Closed
pablopla opened this issue Nov 23, 2024 · 8 comments
Closed

Find GIF disposal method #8563

pablopla opened this issue Nov 23, 2024 · 8 comments

Comments

@pablopla
Copy link

pablopla commented Nov 23, 2024

I'm trying to modify all frames in an animated GIF and save to a new GIF.
Is there a way to get the original GIF disposal method to preserve it in the new GIF?
The image info dict has the loop value but missing the disposal value.

I'm currently using hard coded value disposal=2:

with Image.open('test1.gif') as im:
  for frame in ImageSequence.Iterator(im):
    frame = frame.rotate(90)
    frames.append(frame)
  frames[0].save('test2.gif', save_all=True, append_images=frames[1:], loop=im.info['loop'], disposal=2)

What are your OS, Python and Pillow versions?

  • OS: Debian 12
  • Python: 3.11.2
  • Pillow: Pillow 9.4.0
--------------------------------------------------------------------
Pillow 9.4.0
Python 3.11.2 (main, Sep 14 2024, 03:00:30) [GCC 12.2.0]
--------------------------------------------------------------------
Python modules loaded from /usr/lib/python3/dist-packages/PIL
Binary modules loaded from /usr/lib/python3/dist-packages/PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 9.4.0
*** TKINTER support not installed
--- FREETYPE2 support ok, loaded 2.12.1
--- LITTLECMS2 support ok, loaded 2.14
--- WEBP support ok, loaded 1.4.0
--- WEBP Transparency support ok
--- WEBPMUX support ok
--- WEBP Animation support ok
--- JPEG support ok, compiled for libjpeg-turbo 2.1.5
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.0
--- ZLIB (PNG/ZIP) support ok, loaded 1.2.13
--- LIBTIFF support ok, loaded 4.5.0
--- RAQM (Bidirectional Text) support ok, loaded 0.7.0
--- LIBIMAGEQUANT (Quantization method) support ok, loaded 2.16.0
--- XCB (X protocol) support ok
--------------------------------------------------------------------
@radarhere radarhere added the GIF label Nov 23, 2024
@radarhere
Copy link
Member

radarhere commented Nov 23, 2024

frame.disposal_method will give you the disposal method.

However, be aware that it may be different for different frames.

from PIL import Image, ImageSequence
with Image.open("Tests/images/dispose_bgnd.gif") as im:
    for frame in ImageSequence.Iterator(im):
        print(frame.disposal_method)

gives

1
2
2
2
2

That's ok though, you can use disposal_method=(1, 2, 2, 2, 2) when saving.

More importantly though, why are you interested in this? GIFs do not necessarily consist of separate whole images - the second frame might only contain the pixels that are different. To give a usual result, Pillow will combine the GIF frames as it loads them from the file - meaning that the disposal that worked for the raw frames will not have the same effect for you when you resave it with the images you are receiving. If you'd like to get the original, uncombined frames, it is possible, but I expect it to generate more problems for you later.

@pablopla
Copy link
Author

pablopla commented Nov 23, 2024

Thanks. So what is the correct way to rotate frames in a GIF while preserving all other attributes like duration, loop and disposal?

@radarhere
Copy link
Member

radarhere commented Nov 23, 2024

If all you are doing is rotating images by 90, 180 or 270 degrees, that is an operation that doesn't change the pixel values. So that should work. The following code should preserve the duration, loop, disposal and transparency (yes, by default, it should stay the same) of each frame.

from PIL import Image, ImageSequence, GifImagePlugin

# The following line patches Pillow so that the images aren't loaded loaded as combined RGB images
# The individual images will look broken,
# but if the GIF is saved with the same settings as the original, the combined result will look ok
GifImagePlugin.GifImageFile.load_end = lambda self: None

frames = []
disposal_method = []
with Image.open("input.gif") as im:
    for frame in ImageSequence.Iterator(im):
        disposal_method.append(frame.disposal_method)
        rotated_frame = frame.rotate(90)
        rotated_frame.info["transparency"] = frame._frame_transparency
        frames.append(rotated_frame)
frames[0].save(
    "output.gif",
    save_all=True,
    append_images=frames[1:],
    loop=im.info["loop"]
)

However, if you wish to perform any operation that does change pixel values, I'm still not clear on why you feel the need to resave the disposal method. If you think that you're unable to get a visually correct result, sure, let's discuss that, but preserving the exact way that image data was originally saved is not really a goal that image encoders have.

@pablopla
Copy link
Author

pablopla commented Nov 23, 2024

Thank you for the detailed answer.

I'm trying to rotate animated GIF (test1.gif) with arbitrary angle, not just multiples of 90 degrees.
The following code result with a GIF (test2.gif) that is missing disposal. Only with adding disposal=2 I'm getting the expected result. But a different GIF might need a different disposal_method?
Shouldn't the extracted frames hold the desired disposal without needing to use raw frames? Similar to how frame duration just work.

from PIL import Image, ImageSequence

frames = []
with Image.open('test1.gif') as im:
  for frame in ImageSequence.Iterator(im):
    frame = frame.rotate(35)
    frames.append(frame)
  frames[0].save('test2.gif', save_all=True, append_images=frames[1:], loop=im.info['loop'])

test1

test2

@radarhere
Copy link
Member

To rewind a bit, here are what the different disposal values mean, as seen at https://www.w3.org/Graphics/GIF/spec-gif89a.txt

0 - No disposal specified. The decoder is not required to take any action.
1 - Do not dispose. The graphic is to be left in place.
2 - Restore to background color. The area used by the graphic must be restored to the background color.
3 - Restore to previous. The decoder is required to restore the area overwritten by the graphic with what was there prior to rendering the graphic.

For your image, the disposal method is mostly 2, so it isn't a great example of what I'm trying to explain. Let's look at the image from #8251 instead.

This image uses just the disposal method of 1.

The first two raw frames in the image are

The second frame is saving space by only containing the pixels that have changed. The first frame is set to 'Do not dispose', so those new pixels will be pasted on top of the old ones, and the image will look good.

Now, when Pillow opens the GIF and seeks to the second frame,

from PIL import Image

with Image.open("input.gif") as im:
    im.seek(1)
    im.save("second.png")

the average user would not expected to see the mostly black raw image. They would come here and report that something is broken. Instead, we give them the combined image.

So when it comes time to resave the image, you are not resaving the first two raw frames, you are resaving

There is no need to keep the original disposal. These aren't really the same images that were in the original GIF at all. If there was transparency in each frame of this GIF, I think some disposal settings might even produce incorrect results. Furthermore, you might not know that each frame of a GIF can only introduce 256 new colours to the image. If I opened a GIF, and added 300 new colours to each frame, I couldn't save those exact colours to a GIF.

Pillow isn't only here to just open an image and resave it. Users might look at the images in between those two actions. Users might make changes like you are doing, or something more drastic. Disposal methods are a space saving mechanism that involves deliberately adding transparency and cropping to later images, so that when they are layered on top of each other, like layering one sheet on top of another for an overhead projector, the result still looks good.

@pablopla
Copy link
Author

pablopla commented Nov 24, 2024

Thanks. I now understand the difference between accessing raw frames and getting something useful to work with.
I'll use this code with disposal=2.

Do I also need to preserve each frame transparency like in your code or will it work automatically? Is this necessary?

rotated_frame.info["transparency"] = frame._frame_transparency

Final code:

from PIL import Image, ImageSequence

frames = []
with Image.open('test1.gif') as im:
  for frame in ImageSequence.Iterator(im):
    rotated_frame = frame.rotate(35)
    #rotated_frame.info["transparency"] = frame._frame_transparency
    frames.append(rotated_frame)
  frames[0].save('test2.gif', save_all=True, append_images=frames[1:], loop=im.info['loop'], disposal=2)

@radarhere
Copy link
Member

You shouldn't need to set it, no. info["transparency"] will already be set the transparency from the first frame. Since the frames afterwards are combined on top of that, their transparencies should not be of consequence.

@pablopla
Copy link
Author

Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants