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

Broken background on transparent GIFs built from individual frames #6115

Closed
balt-dev opened this issue Mar 8, 2022 · 8 comments · Fixed by #6128
Closed

Broken background on transparent GIFs built from individual frames #6115

balt-dev opened this issue Mar 8, 2022 · 8 comments · Fixed by #6128
Labels

Comments

@balt-dev
Copy link

balt-dev commented Mar 8, 2022

The background of transparent GIFs constructed from Images seems to not quite see the transparent color correctly.

from PIL import Image

with Image.open('test.gif') as im:
	images = []
	durations = []
	for i in range(im.n_frames):
		im.seek(i)
		im.convert('RGBA')
		im_temp = Image.new('RGBA',im.size,(0,0,0,0))
		im_temp.paste(im,(0,0))
		images.append(im_temp)
		durations.append(im.info['duration'])
	images[0].save(
		'test-out.gif', 
		interlace=True,
		save_all=True,
		append_images=images[1:],
		loop=0,
		duration=durations,
		disposal=2,
		transparency = 255,
		background = 255,
		optimize=False
	)

This should copy the image perfectly, but there are artifacts.
Input
Output
Here's the venv I used to test this, in a zip.

@radarhere radarhere added the GIF label Mar 8, 2022
@resnbl
Copy link

resnbl commented Mar 8, 2022

I suspect that setting transparency and background to 255 is the issue. These values are indexes into the color palette but your palette only has 13 entries, not 255. I fed the test input and output .gifs provided by @balt-is-you-and-shift into a small script I wrote, and here is the output:

% python3 mk_ani.py -i test.gif
PIL 9.0.1
"test.gif" size=(32, 32) mode=P is_ani=True #frames=8 loop=0 palette_size=13
0: disposal=2 transparency=0 duration=1000 background='idx=0 color=(0, 0, 0) #000000'
1: disposal=2 transparency=None duration=50 background='idx=0 color=(0, 0, 0) #000000'
2: disposal=2 transparency=None duration=70 background='idx=0 color=(0, 0, 0) #000000'
3: disposal=2 transparency=None duration=200 background='idx=0 color=(0, 0, 0) #000000'
4: disposal=2 transparency=None duration=100 background='idx=0 color=(0, 0, 0) #000000'
5: disposal=2 transparency=None duration=100 background='idx=0 color=(0, 0, 0) #000000'
6: disposal=2 transparency=None duration=100 background='idx=0 color=(0, 0, 0) #000000'
7: disposal=2 transparency=None duration=50 background='idx=0 color=(0, 0, 0) #000000'
% python3 mk_ani.py -i test-out.gif
PIL 9.0.1
"test-out.gif" size=(32, 32) mode=P is_ani=True #frames=8 loop=0 palette_size=13
0: disposal=2 transparency=255 duration=1000 background='idx=255 color=None None'
1: disposal=2 transparency=None duration=50 background='idx=255 color=None None'
2: disposal=2 transparency=None duration=70 background='idx=255 color=None None'
3: disposal=2 transparency=None duration=200 background='idx=255 color=None None'
4: disposal=2 transparency=None duration=100 background='idx=255 color=None None'
5: disposal=2 transparency=None duration=100 background='idx=255 color=None None'
6: disposal=2 transparency=None duration=100 background='idx=255 color=None None'
7: disposal=2 transparency=None duration=50 background='idx=255 color=None None'

When I copied the background and transparency from the source to the save() call, I think I got the results you desire:

% python3 mk_ani.py -i test-out-t0-bg-none.gif
PIL 9.0.1
"test-out-t0-bg-none.gif" size=(32, 32) mode=P is_ani=True #frames=8 loop=0 palette_size=13
0: disposal=2 transparency=0 duration=1000 background='idx=0 color=(0, 0, 0) #000000'
1: disposal=2 transparency=None duration=50 background='idx=0 color=(0, 0, 0) #000000'
2: disposal=2 transparency=None duration=70 background='idx=0 color=(0, 0, 0) #000000'
3: disposal=2 transparency=None duration=200 background='idx=0 color=(0, 0, 0) #000000'
4: disposal=2 transparency=None duration=100 background='idx=0 color=(0, 0, 0) #000000'
5: disposal=2 transparency=None duration=100 background='idx=0 color=(0, 0, 0) #000000'
6: disposal=2 transparency=None duration=100 background='idx=0 color=(0, 0, 0) #000000'
7: disposal=2 transparency=None duration=50 background='idx=0 color=(0, 0, 0) #000000'

test-out-t0-bg-none

@radarhere
Copy link
Member

Take a look at this. If I set the transparency using the key from the info dictionary, it works.

from PIL import Image

with Image.open('test.gif') as im:
	images = []
	durations = []
	transparency = im.info["transparency"]
	for i in range(im.n_frames):
		im.seek(i)
		im.convert('RGBA')
		im_temp = Image.new('RGBA',im.size,(0,0,0,0))
		im_temp.paste(im,(0,0))
		images.append(im_temp)
		durations.append(im.info['duration'])
	images[0].save(
		'test-out.gif', 
		interlace=True,
		save_all=True,
		append_images=images[1:],
		loop=0,
		duration=durations,
		disposal=2,
		transparency = transparency,
		background = 255,
		optimize=False
	)

test-out

@balt-dev
Copy link
Author

balt-dev commented Mar 8, 2022

This wouldn't solve the issue well, bad example. I'll make a better example of the issue when I can.

@balt-dev
Copy link
Author

balt-dev commented Mar 8, 2022

test-v2.zip
Not very copy-pastable, but it's a much better example of the problem at hand.

Sorry for the multiple edits, I'm trying to get an example that best shows the issue.

from PIL import Image
import json

with open('durations.json','rb') as f:
  durations = json.load(f)
images = []
for n in range(8):
  with Image.open(f'test{n}.png') as im:
    im = im.convert('RGBA').resize([n*8 for n in im.size],Image.NEAREST)
    images.append(im)
images[0].save( #GIFs have this issue
  'test.gif', 
  interlace=True,
  save_all=True,
  append_images=images[1:],
  loop=0,
  duration=durations,
  disposal=2,
  transparency = 255,
  background = 255,
  optimize=False
)
images[0].save( #APNGs don't have this issue, showing it's an issue with GIF saving
  'test.png', 
  format='PNG',
  save_all=True,
  append_images=images,
  default_image=True,
  loop=0,
  durations=durations
)

test
APNG
test
GIF
@resnbl @radarhere

@resnbl
Copy link

resnbl commented Mar 8, 2022

Not sure why you are setting transparency=255 (or why PIL is not complaining about it!). According to the docs (https://pillow.readthedocs.io/en/latest/handbook/image-file-formats.html#saving), transparency is an "index", i.e. offset into the color palette indicating which "color" should denote transparent pixels. Your image palette does not contain 256 colors (only 13 are used), so you are not getting the proper results. For the test provided, using transparency=0 works.

The same applies to the background setting: it is an index into the color palette. Again, setting to 0 will get what you want for this. (@radarhere : your solution is missing this part as well.)

FYI: here is the color palette for your generated GIF:

Global palette:
  0 #000000 (0, 0, 0)
  1 #00E436 (0, 228, 54)
  2 #FFEC27 (255, 236, 39)
  3 #FF004D (255, 0, 77)
  4 #29ADFF (41, 173, 255)
  5 #00B543 (0, 181, 67)
  6 #FF6C24 (255, 108, 36)
  7 #BE1250 (190, 18, 80)
  8 #065AB5 (6, 90, 181)
  9 #A8E72E (168, 231, 46)
  10 #C2C3C7 (194, 195, 199)
  11 #F3EF7D (243, 239, 125)
  12 #FF6E59 (255, 110, 89)

If there is anything amiss with PIL, I think it is that the .save() method did not complain about the transparency and background arguments being outside the range of the generated palette table.

On a side note: I went through the same thing as you: generating an animated image from a list of images. In my solution, I chose a file naming convention over a .json durations file. I named my inputs "nn-dDDD-blah.png" where "nn" is the image order index (for easily sorting the output of a glob.glob('*.png') call), "DDD" is the duration in msecs., and "blah" is anything but was used for the default output filename "blah.{gif|png}". Just another way of doing things...

test

@balt-dev
Copy link
Author

balt-dev commented Mar 8, 2022

See, the problem with that is that setting transparency and background to 255 worked fine in previous versions, and setting them both to 0 won't work if the majority of the image isn't transparent.

@radarhere
Copy link
Member

I've created PR #6128 to resolve this. With it, your code works when you remove the transparency setting altogether. What do you think of that?

from PIL import Image

with Image.open('test.gif') as im:
    images = []
    durations = []
    for i in range(im.n_frames):
        im.seek(i)
        im.convert('RGBA')
        im_temp = Image.new('RGBA',im.size,(0,0,0,0))
        im_temp.paste(im,(0,0))
        images.append(im_temp)
        durations.append(im.info['duration'])
    images[0].save(
        'test-out.gif', 
        interlace=True,
        save_all=True,
        append_images=images[1:],
        loop=0,
        duration=durations,
        disposal=2,
        background = 255,
        optimize=False
    )

@balt-dev
Copy link
Author

Seems great, thanks!

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

Successfully merging a pull request may close this issue.

3 participants