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

Retain RGBA transparency when saving multiple GIF frames #6128

Merged
merged 2 commits into from Mar 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions Tests/test_file_gif.py
Expand Up @@ -842,6 +842,17 @@ def test_rgb_transparency(tmp_path):
assert "transparency" not in reloaded.info


def test_rgba_transparency(tmp_path):
out = str(tmp_path / "temp.gif")

im = hopper("P")
im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])

with Image.open(out) as reloaded:
reloaded.seek(1)
assert_image_equal(hopper("P").convert("RGB"), reloaded)


def test_bbox(tmp_path):
out = str(tmp_path / "temp.gif")

Expand Down
38 changes: 15 additions & 23 deletions src/PIL/GifImagePlugin.py
Expand Up @@ -424,39 +424,28 @@ def _close__fp(self):
RAWMODE = {"1": "L", "L": "L", "P": "P"}


def _normalize_mode(im, initial_call=False):
def _normalize_mode(im):
"""
Takes an image (or frame), returns an image in a mode that is appropriate
for saving in a Gif.

It may return the original image, or it may return an image converted to
palette or 'L' mode.

UNDONE: What is the point of mucking with the initial call palette, for
an image that shouldn't have a palette, or it would be a mode 'P' and
get returned in the RAWMODE clause.

:param im: Image object
:param initial_call: Default false, set to true for a single frame.
:returns: Image object
"""
if im.mode in RAWMODE:
im.load()
return im
if Image.getmodebase(im.mode) == "RGB":
if initial_call:
palette_size = 256
if im.palette:
palette_size = len(im.palette.getdata()[1]) // 3
im = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=palette_size)
if im.palette.mode == "RGBA":
for rgba in im.palette.colors.keys():
if rgba[3] == 0:
im.info["transparency"] = im.palette.colors[rgba]
break
return im
else:
return im.convert("P")
im = im.convert("P", palette=Image.Palette.ADAPTIVE)
if im.palette.mode == "RGBA":
for rgba in im.palette.colors.keys():
if rgba[3] == 0:
im.info["transparency"] = im.palette.colors[rgba]
break
return im
return im.convert("L")


Expand Down Expand Up @@ -514,7 +503,7 @@ def _normalize_palette(im, palette, info):


def _write_single_frame(im, fp, palette):
im_out = _normalize_mode(im, True)
im_out = _normalize_mode(im)
for k, v in im_out.info.items():
im.encoderinfo.setdefault(k, v)
im_out = _normalize_palette(im_out, palette, im.encoderinfo)
Expand Down Expand Up @@ -646,11 +635,14 @@ def get_interlace(im):
def _write_local_header(fp, im, offset, flags):
transparent_color_exists = False
try:
transparency = im.encoderinfo["transparency"]
except KeyError:
if "transparency" in im.encoderinfo:
transparency = im.encoderinfo["transparency"]
else:
transparency = im.info["transparency"]
transparency = int(transparency)
except (KeyError, ValueError):
pass
else:
transparency = int(transparency)
# optimize the block away if transparent color is not used
transparent_color_exists = True

Expand Down