Convert subsequent GIF frames to RGB or RGBA

This commit is contained in:
Andrew Murray 2021-11-29 17:49:06 +11:00
parent bd00cd9214
commit b383a175be
11 changed files with 87 additions and 24 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

View File

@ -324,7 +324,7 @@ def test_dispose_none_load_end():
with Image.open("Tests/images/dispose_none_load_end.gif") as img: with Image.open("Tests/images/dispose_none_load_end.gif") as img:
img.seek(1) img.seek(1)
assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.gif") assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png")
def test_dispose_background(): def test_dispose_background():
@ -340,12 +340,16 @@ def test_dispose_background():
def test_dispose_background_transparency(): def test_dispose_background_transparency():
with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
img.seek(2) img.seek(2)
px = img.convert("RGBA").load() px = img.load()
assert px[35, 30][3] == 0 assert px[35, 30][3] == 0
def test_transparent_dispose(): def test_transparent_dispose():
expected_colors = [(2, 1, 2), (0, 1, 0), (2, 1, 2)] 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)),
]
with Image.open("Tests/images/transparent_dispose.gif") as img: with Image.open("Tests/images/transparent_dispose.gif") as img:
for frame in range(3): for frame in range(3):
img.seek(frame) img.seek(frame)
@ -368,7 +372,7 @@ def test_dispose_previous_first_frame():
with Image.open("Tests/images/dispose_prev_first_frame.gif") as im: with Image.open("Tests/images/dispose_prev_first_frame.gif") as im:
im.seek(1) im.seek(1)
assert_image_equal_tofile( assert_image_equal_tofile(
im, "Tests/images/dispose_prev_first_frame_seeked.gif" im, "Tests/images/dispose_prev_first_frame_seeked.png"
) )
@ -508,7 +512,7 @@ def test_dispose2_background(tmp_path):
with Image.open(out) as im: with Image.open(out) as im:
im.seek(1) im.seek(1)
assert im.getpixel((0, 0)) == 0 assert im.getpixel((0, 0)) == (255, 0, 0)
def test_transparency_in_second_frame(): def test_transparency_in_second_frame():
@ -517,9 +521,9 @@ def test_transparency_in_second_frame():
# Seek to the second frame # Seek to the second frame
im.seek(im.tell() + 1) im.seek(im.tell() + 1)
assert im.info["transparency"] == 0 assert "transparency" not in im.info
assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.gif") assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
def test_no_transparency_in_second_frame(): def test_no_transparency_in_second_frame():
@ -926,4 +930,4 @@ def test_missing_background():
# but the disposal method is "Restore to background color" # but the disposal method is "Restore to background color"
with Image.open("Tests/images/missing_background.gif") as im: with Image.open("Tests/images/missing_background.gif") as im:
im.seek(1) im.seek(1)
assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.gif") assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png")

View File

@ -124,8 +124,7 @@ class GifImageFile(ImageFile.ImageFile):
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if frame < self.__frame: if frame < self.__frame:
if frame != 0: self.im = None
self.im = None
self._seek(0) self._seek(0)
last_frame = self.__frame last_frame = self.__frame
@ -165,12 +164,21 @@ class GifImageFile(ImageFile.ImageFile):
pass pass
self.__offset = 0 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: if self.dispose:
self.im.paste(self.dispose, self.dispose_extent) self.im.paste(self.dispose, self.dispose_extent)
from copy import copy palette = None
self.palette = copy(self.global_palette)
info = {} info = {}
frame_transparency = None frame_transparency = None
@ -246,7 +254,7 @@ class GifImageFile(ImageFile.ImageFile):
if flags & 128: if flags & 128:
bits = (flags & 7) + 1 bits = (flags & 7) + 1
self.palette = ImagePalette.raw("RGB", self.fp.read(3 << bits)) palette = ImagePalette.raw("RGB", self.fp.read(3 << bits))
# image data # image data
bits = self.fp.read(1)[0] bits = self.fp.read(1)[0]
@ -257,6 +265,15 @@ class GifImageFile(ImageFile.ImageFile):
pass pass
# raise OSError, "illegal GIF tag `%x`" % s[0] # raise OSError, "illegal GIF tag `%x`" % s[0]
frame_palette = palette or self.global_palette
def _rgb(color):
if frame_palette:
color = tuple(frame_palette.palette[color * 3 : color * 3 + 3])
else:
color = (color, color, color)
return color
try: try:
if self.disposal_method < 2: if self.disposal_method < 2:
# do not dispose or none specified # do not dispose or none specified
@ -272,9 +289,13 @@ class GifImageFile(ImageFile.ImageFile):
# by convention, attempt to use transparency first # by convention, attempt to use transparency first
color = self.info.get("transparency", frame_transparency) color = self.info.get("transparency", frame_transparency)
if color is None: if color is not None:
color = self.info.get("background", 0) dispose_mode = "RGBA"
self.dispose = Image.core.fill("P", dispose_size, color) 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: else:
# replace with previous contents # replace with previous contents
if self.im: if self.im:
@ -286,7 +307,7 @@ class GifImageFile(ImageFile.ImageFile):
Image._decompression_bomb_check(dispose_size) Image._decompression_bomb_check(dispose_size)
self.dispose = Image.core.fill( self.dispose = Image.core.fill(
"P", dispose_size, frame_transparency "RGBA", dispose_size, _rgb(frame_transparency) + (0,)
) )
except AttributeError: except AttributeError:
pass pass
@ -316,16 +337,54 @@ class GifImageFile(ImageFile.ImageFile):
elif k in self.info: elif k in self.info:
del self.info[k] del self.info[k]
self.mode = "L" if frame == 0:
if self.palette: self.mode = "P" if frame_palette else "L"
self.mode = "P"
if self.mode == "P" and not palette:
from copy import copy
palette = copy(self.global_palette)
self.palette = palette
else:
self._frame_palette = frame_palette
self._frame_transparency = frame_transparency
def load_prepare(self): def load_prepare(self):
if not self.im and "transparency" in self.info: if self.__frame == 0:
self.im = Image.core.fill(self.mode, self.size, self.info["transparency"]) if "transparency" in self.info:
self.im = Image.core.fill(
self.mode, self.size, self.info["transparency"]
)
else:
self._prev_im = self.im
if self._frame_palette:
self.mode = "P"
self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
self.im.putpalette(*self._frame_palette.getdata())
self._frame_palette = None
else:
self.mode = "L"
self.im = None
super().load_prepare() super().load_prepare()
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._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)
else:
self.im.paste(frame_im, self.dispose_extent)
def tell(self): def tell(self):
return self.__frame return self.__frame

View File

@ -135,7 +135,7 @@ def _save(im, fp, filename, save_all=False):
procset = "ImageB" # grayscale procset = "ImageB" # grayscale
elif im.mode == "P": elif im.mode == "P":
filter = "ASCIIHexDecode" filter = "ASCIIHexDecode"
palette = im.im.getpalette("RGB") palette = im.getpalette()
colorspace = [ colorspace = [
PdfParser.PdfName("Indexed"), PdfParser.PdfName("Indexed"),
PdfParser.PdfName("DeviceRGB"), PdfParser.PdfName("DeviceRGB"),