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

GifImageFile and animated gif with mismatched color palettes #2893

Closed
tomaszhlawiczka opened this issue Dec 18, 2017 · 8 comments · Fixed by #5857
Closed

GifImageFile and animated gif with mismatched color palettes #2893

tomaszhlawiczka opened this issue Dec 18, 2017 · 8 comments · Fixed by #5857
Labels
Bug Any unexpected behavior, until confirmed feature. GIF
Projects
Milestone

Comments

@tomaszhlawiczka
Copy link

I'm trying to resize an animated gif file with a different color palette for each frame.

Each frame should be composed with own colors, but because of buildin frames merge (see GifImageFile.load_end) colors are messed up.

Python: 3.6
PIL: 4.4.0.dev0

from PIL import Image
source = Image.open("src.gif")
w, h = source.size
resized = []
try:
	for i in range(0, 0xffffffff):
		source.seek(i)
		resized.append(source.convert('RGBA').resize((w//2, h//2), Image.ANTIALIAS))
except EOFError:
	pass
resized[0].save("result.gif", "GIF", save_all=True, append_images=resized[1:], loop=1, duration=50)

I see following solutions here:

  1. Merge color palettes as well and reindex frames to new colors (hard one, losing an original data, might be impossible due to the limit of colors in the palette)
  2. First convert each frame from mode PRGBA and then make a merge (losing an original data)
  3. Move merging to another level of abstraction allowing users to open GIF files exactly how there were composed (opening GIF would require additional actions)

So far I've made a workaround as follow (might be helpful for someone):

# Clear up the GifImageFile.load_end()
from PIL import Image, ImageFile
from PIL.GifImagePlugin import GifImageFile, _accept, _save, _save_all

class AnimatedGifImageFile(GifImageFile):
	def load_end(self):
		ImageFile.ImageFile.load_end(self)

Image.register_open(AnimatedGifImageFile.format, AnimatedGifImageFile, _accept)
Image.register_save(AnimatedGifImageFile.format, _save)
Image.register_save_all(AnimatedGifImageFile.format, _save_all)
Image.register_extension(AnimatedGifImageFile.format, ".gif")
Image.register_mime(AnimatedGifImageFile.format, "image/gif")


# Make merge by my own (losing an original palette 
# doesn't matter for this particular case - having RGBA is good enough):
im = Image.open("src.gif")
last_frame = None
all_frames = []
try:
	for i in range(0, 0xffffffff):
		im.seek(i)
		new_frame = im.convert('RGBA')
		if last_frame is not None and im.disposal_method == 1:
			updated = new_frame.crop(im.dispose_extent)
			last_frame.paste(updated, im.dispose_extent, updated)
			new_frame = last_frame
		else:
			last_frame = new_frame

		# do resizing on new_frame here...

		all_frames.append(new_frame)
except EOFError:
	pass

src
result

@wiredfool
Copy link
Member

To be clear -- is the image incorrect when reading it in, or when writing back out?

@tomaszhlawiczka
Copy link
Author

The reading part is incorrect (GifImageFile.load_end).

@aclark4life aclark4life added the Bug Any unexpected behavior, until confirmed feature. label Apr 1, 2018
@aclark4life aclark4life added this to the Future milestone Apr 1, 2018
@aclark4life aclark4life added this to Backlog in Pillow May 11, 2019
@aclark4life aclark4life moved this from Backlog to Icebox in Pillow May 11, 2019
@JankesJanco
Copy link

@tomaszhlawiczka thanks for the workaround! it saved my day!

@JankesJanco
Copy link

JankesJanco commented Jun 17, 2019

This workaround helps with some GIFs, but I found some GIFs which are still incorrectly opened or saved even after applying the workaround. I simply just open the gif and then save it, the code looks like this:

# workaround initialization code
class AnimatedGifImageFile(GifImageFile):

    def load_end(self):
        ImageFile.ImageFile.load_end(self)


Image.register_open(AnimatedGifImageFile.format, AnimatedGifImageFile, _accept)
Image.register_save(AnimatedGifImageFile.format, _save)
Image.register_save_all(AnimatedGifImageFile.format, _save_all)
Image.register_extension(AnimatedGifImageFile.format, ".gif")
Image.register_mime(AnimatedGifImageFile.format, "image/gif")
# end of workaround initialization code

def get_frames(gif: Image.Image) -> List[Image.Image]:
    """
    Extract all frames from gif. 

    This function is just slight adjustment of the for-cycle from the workaround.
    """ 
    last_frame = None
    all_frames = []
    i = 0
    try:
        while True:
            gif.seek(i)
            new_frame = gif.convert('RGBA')
            if last_frame is not None and gif.disposal_method == 1:
                updated = new_frame.crop(gif.dispose_extent)
                last_frame.paste(updated, gif.dispose_extent, updated)
                new_frame = last_frame
            else:
                last_frame = new_frame

            # do resizing on new_frame here...

            all_frames.append(new_frame.copy())
            i += 1
    except EOFError:
        gif.seek(0)

    return all_frames


gif: Image.Image = Image.open('example.gif')
frames = get_frames(gif)
frames[0].save('example.saved.gif', save_all=True, append_images=frames[1:])

here is an example:
bowie
bowie saved_2

@radarhere
Copy link
Member

#5333 doesn't completely fix, but does significantly improve the car gif.

@doublex
Copy link

doublex commented Oct 22, 2021

We use this code to save AnimGifs (taken from https://github.com/akirbaes/giftools.git):

import PIL.Image, numpy

# https://github.com/akirbaes/giftools.git
def create_gif_frame( frame, transparency=255):
    # 'only RGB or L mode images can be quantized to a palette' says PIL
    quantized = (frame.convert('RGB') if frame.mode != 'RGB' else frame).quantize(colors=255, method=2, kmeans=0, dither=0)
    # Save the transparency areas beforehand because quantizing doesn't always respect it
    mask = numpy.array( frame.convert('RGBA') if frame.mode != 'RGBA' else frame.copy() )[:,:,3].copy() // 255
    # Puts back the transparency areas on the image after quantization
    data = numpy.array( quantized )
    data = data * mask  # Nullifies transparent areas
    mask = -(mask-1) * transparency
    data = data + mask  # Makes transparent area the transparency color
    result = PIL.Image.fromarray( data,'P' )
    result.putpalette( quantized.getpalette() )
    result.info['transparency'] = transparency
    return result

with PIL.Image.open('/tmp/example.gif', 'r') as image:

    frames, durations = [], []
    for frame in PIL.ImageSequence.Iterator(image):
        frames.append( create_gif_frame(frame) )
        # crucial to get duration+timestamp
        image.copy()
        duration = image.info.get( 'duration' )
        assert duration is None or type(duration) is int    # 'image/mpo' has no duration
        if duration is None:
            assert not durations
            break
        durations.append( duration )
    frames[0].save( '/tmp/test.gif', format='GIF', version='GIF89a', save_all=True, optimize=False, disposal=2,
                    append_images=frames[1:], duration=durations, loop=0 )

@radarhere
Copy link
Member

I've created PR #5857, which fixes both images listed here, resolving this.

Pillow automation moved this from Icebox to Closed Dec 6, 2021
@bongdang
Copy link

bongdang commented Dec 7, 2021

I've just upload my solution on github. https://github.com/bongdang/gifraw
testing above two animated gifs is ok.
please check it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Any unexpected behavior, until confirmed feature. GIF
Projects
Pillow
  
Closed
Development

Successfully merging a pull request may close this issue.

7 participants