Skip to content

Commit

Permalink
src/PIL/GifImagePlugin.py: only convert image if palettes actually ch…
Browse files Browse the repository at this point in the history
…ange
  • Loading branch information
josch committed Jan 18, 2022
1 parent 82541b6 commit e576c3d
Show file tree
Hide file tree
Showing 10 changed files with 65 additions and 41 deletions.
Binary file added Tests/images/different_transparency_merged.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed Tests/images/different_transparency_merged.png
Binary file not shown.
Binary file added Tests/images/dispose_none_load_end_second.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed Tests/images/dispose_none_load_end_second.png
Binary file not shown.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed Tests/images/dispose_prev_first_frame_seeked.png
Binary file not shown.
Binary file added Tests/images/missing_background_first_frame.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed Tests/images/missing_background_first_frame.png
Binary file not shown.
18 changes: 7 additions & 11 deletions Tests/test_file_gif.py
Expand Up @@ -350,7 +350,7 @@ def test_dispose_none_load_end():
with Image.open("Tests/images/dispose_none_load_end.gif") as img:
img.seek(1)

assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png")
assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.gif")


def test_dispose_background():
Expand All @@ -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 All @@ -398,7 +394,7 @@ def test_dispose_previous_first_frame():
with Image.open("Tests/images/dispose_prev_first_frame.gif") as im:
im.seek(1)
assert_image_equal_tofile(
im, "Tests/images/dispose_prev_first_frame_seeked.png"
im, "Tests/images/dispose_prev_first_frame_seeked.gif"
)


Expand Down Expand Up @@ -538,7 +534,7 @@ def test_dispose2_background(tmp_path):

with Image.open(out) as im:
im.seek(1)
assert im.getpixel((0, 0)) == (255, 0, 0)
assert im.getpixel((0, 0)) == 0


def test_transparency_in_second_frame():
Expand All @@ -549,7 +545,7 @@ def test_transparency_in_second_frame():
im.seek(im.tell() + 1)
assert "transparency" not in im.info

assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.gif")


def test_no_transparency_in_second_frame():
Expand Down Expand Up @@ -956,7 +952,7 @@ 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, "Tests/images/missing_background_first_frame.gif")


def test_saving_rgba(tmp_path):
Expand Down
88 changes: 58 additions & 30 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 @@ -266,6 +256,32 @@ def _seek(self, frame):
# raise OSError, "illegal GIF tag `%x`" % s[0]

frame_palette = palette or self.global_palette
if self.__frame == 1:
self.pyaccess = None
if not self.constant_palette:
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)

if tmp_dispose_extent is not None:
self.dispose_extent = tmp_dispose_extent

# 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

def _rgb(color):
if frame_palette:
Expand All @@ -289,13 +305,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 +328,9 @@ 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,
_rgb(frame_transparency) + (0,),
)
except AttributeError:
pass
Expand Down Expand Up @@ -367,17 +390,22 @@ 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 not self.constant_palette:
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
self.mode = self.im.mode
if frame_im.mode == "RGBA":
self.im.paste(frame_im, self.dispose_extent, frame_im)
if not self.constant_palette:
if frame_im.mode == "RGBA":
self.im.paste(frame_im, self.dispose_extent, frame_im)
else:
self.im.paste(frame_im, self.dispose_extent)
else:
self.im.paste(frame_im, self.dispose_extent)

Expand Down

0 comments on commit e576c3d

Please sign in to comment.