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

GIF frames: background, transparency, combining #4640

Closed
Dragorn421 opened this issue May 23, 2020 · 13 comments
Closed

GIF frames: background, transparency, combining #4640

Dragorn421 opened this issue May 23, 2020 · 13 comments
Labels

Comments

@Dragorn421
Copy link

  • OS: had the issue on Debian, moved to Windows for easier testing (I would say OS doesn't matter here)
  • Python: 3.7.3 on Debian, 3.6.9 on Windows
  • Pillow: 7.1.2

This was originally a question on StackOverflow, but I since think it's more of a Pillow bug/limitation, I will put all relevant information here, no need to read the SO post.

Resources:
PepePls.gif
PepePls

PeepoCreepo.gif
PeepoCreepo

from PIL import Image

image = Image.open('PepePls.gif')

frames = []
try:
    while True:
        frames.append(image.copy())
        image.seek(image.tell() + 1)
except EOFError:
    pass

for i in range(len(frames)):
    frames[i].save(f'gif_f{i}.gif')
    frames[i].save(f'png_f{i}.png')
    frames[i].convert(mode='RGBA').save(f'rgba_gif_f{i}.gif')
    frames[i].convert(mode='RGBA').save(f'rgba_png_f{i}.png')

With PepePls.gif:
image

With PeepoCreepo.gif:
peepocreepo_frames

(I would shorten the gifs if I knew how to recreate their features, but I don't)

Black borders

The first thing you notice is that there are black borders everywhere. After researching and reading through GifImagePlugin.py, I think this is because in disposal_method==2 (which I think is "replace" whole frame) the load-image code (which I didn't locate precisely, I guess it's C somewhere?) pastes to a new image only the dispose_extent part of a frame which is the part read from the file (aka the part in the middle of the black borders). So that's fine but I guess the "new image" which is being pasted on is likely initialized wrongly, since black seems to be the default color in Pillow it may not be initialized at all.

I fixed this by copying the dispose_extent part onto a fully transparent image, but I'm not sure how to detected if the gif should be read that way (how to tell if it wants a transparent background?), code:

from PIL import Image

image = Image.open('PeepoCreepo.gif')

frames = []
try:
	while True:
		if image.__dict__.get('disposal_method', 0) == 2:
			frame = Image.new('RGBA', image.size, color=(0,0,0,0))
			frame.paste(image.crop(image.dispose_extent), box=(image.dispose_extent[0],image.dispose_extent[1]))
			frames.append(frame)
		else:
			frames.append(image.copy())
		image.seek(image.tell() + 1)
except EOFError:
	pass

for i in range(len(frames)):
	frames[i].save(f'gif_f{i}.gif')
	frames[i].save(f'png_f{i}.png')
	frames[i].convert(mode='RGBA').save(f'rgba_gif_f{i}.gif')
	frames[i].convert(mode='RGBA').save(f'rgba_png_f{i}.png')

image

peepocreepo_frames_dispose2

Apart from png_f2.png from PeepoCreepo.gif (no idea what is happening there either), all the black borders have disappeared when saving to png.

gif and rgba

I'm not sure why .convert(rgba).save(gif) makes the saved image file lose transparency. Didn't research that issue at all.

frame combine

Let's zoom in on the rgba_png_fX.png files, because I'm sure they went unnoticed among the hundreds of frames I dumped:
image
Notice rgba_png_f2.png has missing pixels
Here's a screenshot of GIMP (image manipulation program) with those frames which may help understand the issue:
image
Notice how in GIMP frame "Image vidéo 3" (which corresponds to our rgba_png_f2.png file) is "combine"
However if you add print(image.__dict__.get('disposal_method', None)) at the beginning of the while True: loop you'll get

1
1
2
[... just 2]
2
1

I assume 1 is "combine" (at time of writing) and 2 is "replace" (at time of writing)
So, Pillow is using the disposal_method value early? If it was delayed by a frame, rgba_png_f2.png would be correctly overlayed on rgba_png_f1.png

my full "debugging" script (can be ignored)

For reference, here's a script with a lot of useless stuff which I used for debugging, it does highlight what undocumented GifImageFile members seem to be relevant here though. It also fixes the "delay combine" issue with a local variable disposal_method_last

from PIL import Image

gif = 'PeepoCreepo.gif'
#gif = 'PepePls.gif'

image = Image.open(gif)

red = Image.new('RGBA', (85, 112), color='red')
redP = red.convert(mode='P')

imTest = Image.new('P', (10, 10), color='red')
imTest.im = Image.core.fill('P', imTest.size, 1)
#imTest.save('imtest.png')

# is this correct?
def getPaletteColor(im, n):
	# https://github.com/python-pillow/Pillow/blob/master/src/PIL/ImagePalette.py#L108
	try:
		return (im.palette.palette[n], im.palette.palette[n+256], im.palette.palette[n+512])
	except IndexError:
		return f'IndexError palette {n}'

loop = []
frames = []
durations = []

disposal_method_last = 0

try:
	while True:
		"""
		print('im=' + str(image.__dict__.get('im', None)))
		tile = image.__dict__.get('tile', None)
		print('tile=' + str(tile))
		print('tile[0]=' + str(tile[0]))
		# probably better than image.dispose_extent but idk
		print('tile[0][1]=' + str(tile[0][1]))
		"""
		print('dispose=' + str(image.__dict__.get('dispose', None)))
		print('dispose_extent=' + str(image.__dict__.get('dispose_extent', None)))
		print('disposal_method=' + str(image.__dict__.get('disposal_method', None)))
		"""
		print('background=' + str(image.info.get('background', None)))
		print('background color=' + str(getPaletteColor(image, image.info.get('background', 0))))
		print('transparency=' + str(image.info.get('transparency', None)))
		print('transparency color=' + str(getPaletteColor(image, image.info.get('transparency', 0))))
		# indeed the same as transparency
		print('upleft pixel dispose=' + str(image.getpixel((image.dispose_extent[0],image.dispose_extent[1]))))
		print('upleft_xy+1 pixel dispose=' + str(image.getpixel((image.dispose_extent[0]+1,image.dispose_extent[1]+1))))
		# indeed not transparency
		print('upleft pixel=' + str(image.getpixel((0,0))))
		print('upleft_xy+1 pixel=' + str(image.getpixel((1,1))))
		"""
		loop.append(image.info.get('loop', None))
		disposal_method = disposal_method_last
		disposal_method_last = image.__dict__.get('disposal_method', 0)
		if disposal_method == 2 or (disposal_method == 1 and frames == []):
			#"""
			frame = Image.new('RGBA', image.size, color=(255,0,0,0))
			"""
			frame = Image.new('P', image.size, color=image.info['background'])
			frame.palette = image.palette
			#"""
			frame.paste(image.crop(image.dispose_extent), box=(image.dispose_extent[0],image.dispose_extent[1]))
		elif disposal_method == 1:
			"""
			newStuff = Image.new('RGBA', image.size, color=(0,0,255,0))
			newStuff.paste(image.crop(image.dispose_extent), box=(image.dispose_extent[0],image.dispose_extent[1]))
			frame = Image.alpha_composite(frames[-1].convert(mode='RGBA'), newStuff.convert(mode='RGBA'))
			"""
			newStuff = image.crop(image.dispose_extent)
			frame = frames[-1].copy()
			"""
			background = Image.new('P', newStuff.size, color=image.info['background'])
			background.palette = image.palette
			frame.paste(background, image.dispose_extent)
			#"""
			frame.paste(newStuff, image.dispose_extent, newStuff.convert("RGBA"))
			#"""
		else:
			frame = image.copy()
		transparency = image.info.get('transparency', None)
		
		# "delta frames" not for PepePls apparently, or not how they work
		"""
		if len(frames) == 0:
			frame = image.convert(mode='RGBA')
		else:
			frame = Image.alpha_composite(frames[-1], image.convert(mode='RGBA'))
		"""
		# https://stackoverflow.com/questions/4904940/python-converting-gif-frames-to-png
		# didn't help
		"""
		if palette is None:
			palette = frame.getpalette()
		else:
			frame.putpalette(palette)
		"""
		frames.append(frame)
		durations.append(image.info.get('duration', None))
		"""
		"background" -> no difference
		"transparency" -> most borders changed color
		image.im = Image.core.fill("P", image.size, image.info["transparency"])
		image._prev_im = Image.core.fill("P", image.size, image.info["transparency"])
		"""
		"""
		same as above
		image.im = Image.new('P', image.size, color=200).im
		image._prev_im = Image.new('P', image.size, color=200).im
		"""
		image.seek(image.tell() + 1)
except EOFError:
	pass

for i in range(len(frames)):
	print(f'#{i} loop = {loop[i]} duration = {durations[i]} mode = {frames[i].mode} size = {frames[i].size}')
	frames[i].save(f'gif_f{i}.gif')
	frames[i].save(f'png_f{i}.png')
	frames[i].convert(mode='RGBA').save(f'rgba_gif_f{i}.gif')
	frames[i].convert(mode='RGBA').save(f'rgba_png_f{i}.png')
@radarhere radarhere added the GIF label May 23, 2020
@karolyi
Copy link

karolyi commented May 24, 2020

@Dragorn421 you might want to look into #4644

@Dragorn421
Copy link
Author

My issue is about loading gifs, not saving them, although in those tests I did you can indeed see gifs in Pillow hate transparency. Still, thanks

@karolyi
Copy link

karolyi commented May 24, 2020

My experience is if you convert a 'P' image to an RGBA, you get proper alpha values, except for when not (see also the issue for it).

Did you try your gifs with my processor? Also, in my experience, only the disposal=2 works, none other does, unfortunately.

@Dragorn421
Copy link
Author

Dragorn421 commented May 24, 2020

See your issue for discussing your processor

@StupidCrow-yuan
Copy link

gif_f74

I still get the error result! do you have any suggestion for me?

@Dragorn421
Copy link
Author

Until a proper fix I guess you can use what I posted on stackoverflow https://stackoverflow.com/questions/61958291/gifs-read-with-pillow-have-black-borders under "EDIT 2:"
It was a long time ago so I don't have more to say than what I wrote here and over on Stackoverflow

@StupidCrow-yuan
Copy link

Until a proper fix I guess you can use what I posted on stackoverflow https://stackoverflow.com/questions/61958291/gifs-read-with-pillow-have-black-borders under "EDIT 2:"
It was a long time ago so I don't have more to say than what I wrote here and over on Stackoverflow

yeah, I have tested your "EDIT 2" on stackouverflow and test your "EDIT 3" on github, but I still get other background color

@radarhere radarhere changed the title Gif frames: background, transparency, combining GIF frames: background, transparency, combining Jul 23, 2021
@AnasYasin
Copy link

AnasYasin commented Oct 11, 2021

Any solutions guys? I'm getting the same error results on large gifs with more colors. After the first frame all frames are getting black background.

Code:

gif = Image.open("gif4.GIF")
duration = 100
disposal = 2

frames = []
ind = 0
for frame in ImageSequence.Iterator(gif):
    bg2 = Image.open('test1.png')
    bg2.paste(frame, frame.convert('RGBA'))
    bg2.save('{}.png'.format(ind))
    frames.append(bg2)
    ind  += 1
    
frames[0].save('test.gif', save_all=True, append_images=frames[1:], loop=0, optimise=False, duration=duration, disposal=disposal, format='GIF')

Orignal Gif:

Background Image:

Final Result:

Frames:


@tomhaydn
Copy link

tomhaydn commented Nov 16, 2021

I have the exact same issue as above:

from PIL import Image, ImageSequence, GifImagePlugin
import os


bg = Image.open('images/bg/Background Sky.gif')
body = Image.open('images/body/Body Blue.gif')
eyes = Image.open('images/eyes/Eyes Green.gif')
headwear = Image.open('images/headwear/Headwear Party Hat.gif')

frames = []
for idx, frame in enumerate(ImageSequence.Iterator(bg)):
    frame = Image.new('RGBA', (640, 640), color=(255,0,0,0))
    frame.paste(ImageSequence.Iterator(bg)[idx])
    frame.paste(ImageSequence.Iterator(body)[idx], mask=ImageSequence.Iterator(body)[idx].convert("RGBA"))
    frame.paste(ImageSequence.Iterator(eyes)[idx], mask=ImageSequence.Iterator(eyes)[idx].convert("RGBA"))
    frame.paste(ImageSequence.Iterator(headwear)[idx], mask=ImageSequence.Iterator(headwear)[idx].convert("RGBA"))
    
    frames.append(frame)

print(frames)

#frames[0].save('images/generated/output.gif', save_all=True, append_images=frames[0:])
frames[0].save('images/generated/output.gif', save_all=True, append_images=frames[1:], loop=0)

@radarhere
Copy link
Member

The issue from @tomhaydn became #5837 and the issue from @AnasYasin became #5755

@radarhere
Copy link
Member

With #5857, the PNGs look correct.

@radarhere
Copy link
Member

#5857 has now been merged.

With #5859 and #5869, this should work correctly.

@radarhere
Copy link
Member

radarhere commented Dec 28, 2021

Those PRs have been merged, so this should be fixed in Pillow 9.0, due to be released on January 2.

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

No branches or pull requests

6 participants