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

WIP: only convert GIF frames if palette actually changes #5974

Closed
wants to merge 1 commit into from
Closed
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
12 changes: 5 additions & 7 deletions Tests/test_file_gif.py
Expand Up @@ -366,16 +366,12 @@ def test_dispose_background():
def test_dispose_background_transparency():
with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
img.seek(2)
px = img.load()
px = img.convert("RGBA").load()
assert px[35, 30][3] == 0


def test_transparent_dispose():
expected_colors = [
(2, 1, 2),
((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
]
expected_colors = [(2, 1, 2), (0, 1, 0), (2, 1, 2)]
with Image.open("Tests/images/transparent_dispose.gif") as img:
for frame in range(3):
img.seek(frame)
Expand Down Expand Up @@ -956,7 +952,9 @@ def test_missing_background():
# but the disposal method is "Restore to background color"
with Image.open("Tests/images/missing_background.gif") as im:
im.seek(1)
assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png")
assert_image_equal_tofile(
im.convert("RGBA"), "Tests/images/missing_background_first_frame.png"
)


def test_saving_rgba(tmp_path):
Expand Down
92 changes: 64 additions & 28 deletions src/PIL/GifImagePlugin.py
Expand Up @@ -55,6 +55,9 @@ class GifImageFile(ImageFile.ImageFile):

global_palette = None

constant_palette = True
last_palette = None

def data(self):
s = self.fp.read(1)
if s and s[0]:
Expand Down Expand Up @@ -82,7 +85,7 @@ def _open(self):
for i in range(0, len(p), 3):
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
p = ImagePalette.raw("RGB", p)
self.global_palette = self.palette = p
self.global_palette = self.last_palette = self.palette = p
break

self.__fp = self.fp # FIXME: hack
Expand Down Expand Up @@ -164,25 +167,12 @@ def _seek(self, frame):
pass
self.__offset = 0

if self.__frame == 1:
self.pyaccess = None
if "transparency" in self.info:
self.mode = "RGBA"
self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.FLOYDSTEINBERG)

del self.info["transparency"]
else:
self.mode = "RGB"
self.im = self.im.convert("RGB", Image.FLOYDSTEINBERG)
if self.dispose:
self.im.paste(self.dispose, self.dispose_extent)

palette = None

info = {}
frame_transparency = None
interlace = None
tmp_dispose_extent = None
while True:

s = self.fp.read(1)
Expand Down Expand Up @@ -247,7 +237,7 @@ def _seek(self, frame):
x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
if x1 > self.size[0] or y1 > self.size[1]:
self._size = max(x1, self.size[0]), max(y1, self.size[1])
self.dispose_extent = x0, y0, x1, y1
tmp_dispose_extent = x0, y0, x1, y1
flags = s[8]

interlace = (flags & 64) != 0
Expand All @@ -267,6 +257,41 @@ def _seek(self, frame):

frame_palette = palette or self.global_palette

# as long as we have a constant palette, we check whether this frame
# still has the same palette
if self.constant_palette:
if self.last_palette is None:
self.last_palette = palette
else:
if self.last_palette != frame_palette:
self.constant_palette = False

if self.__frame > 0:
# for every frame after the first frame, we check whether the
# palette is still constant and convert to RGB or RGBA if not
self.pyaccess = None
if not self.constant_palette and self.mode not in ("RGB", "RGBA"):
if "transparency" in self.info:
self.mode = "RGBA"
self.im.putpalettealpha(self.info["transparency"], 0)
self.im = self.im.convert("RGBA", Image.FLOYDSTEINBERG)
if self.dispose:
self.dispose = self.dispose.convert(
"RGBA", Image.FLOYDSTEINBERG
)

del self.info["transparency"]
else:
self.mode = "RGB"
self.im = self.im.convert("RGB", Image.FLOYDSTEINBERG)
if self.dispose:
self.dispose = self.dispose.convert("RGB", Image.FLOYDSTEINBERG)
if self.dispose:
self.im.paste(self.dispose, self.dispose_extent)

if tmp_dispose_extent is not None:
self.dispose_extent = tmp_dispose_extent

def _rgb(color):
if frame_palette:
color = tuple(frame_palette.palette[color * 3 : color * 3 + 3])
Expand All @@ -289,13 +314,18 @@ def _rgb(color):

# by convention, attempt to use transparency first
color = self.info.get("transparency", frame_transparency)
if color is not None:
dispose_mode = "RGBA"
color = _rgb(color) + (0,)
if self.constant_palette:
if color is None:
color = self.info.get("background", 0)
self.dispose = Image.core.fill("P", dispose_size, color)
else:
dispose_mode = "RGB"
color = _rgb(self.info.get("background", 0))
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
if color is not None:
dispose_mode = "RGBA"
color = _rgb(color) + (0,)
else:
dispose_mode = "RGB"
color = _rgb(self.info.get("background", 0))
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
else:
# replace with previous contents
if self.im:
Expand All @@ -307,7 +337,11 @@ def _rgb(color):

Image._decompression_bomb_check(dispose_size)
self.dispose = Image.core.fill(
"RGBA", dispose_size, _rgb(frame_transparency) + (0,)
"P" if self.constant_palette else "RGBA",
dispose_size,
frame_transparency
if self.constant_palette
else _rgb(frame_transparency) + (0,),
)
except AttributeError:
pass
Expand Down Expand Up @@ -367,11 +401,13 @@ def load_prepare(self):
def load_end(self):
if self.__frame == 0:
return
if self._frame_transparency is not None:
self.im.putpalettealpha(self._frame_transparency, 0)
frame_im = self.im.convert("RGBA")
else:
frame_im = self.im.convert("RGB")
frame_im = self.im
if self._prev_im.mode != self.im.mode:
if self._frame_transparency is not None:
self.im.putpalettealpha(self._frame_transparency, 0)
frame_im = self.im.convert("RGBA")
else:
frame_im = self.im.convert("RGB")
frame_im = self._crop(frame_im, self.dispose_extent)

self.im = self._prev_im
Expand Down