From a4a314f765443c088744deca074325ff4c005262 Mon Sep 17 00:00:00 2001 From: Glenn Maynard Date: Tue, 23 Oct 2018 22:33:57 -0500 Subject: [PATCH 1/3] GIF: Support transparency in the native decoder. Allow the transparency index to be passed to the native decoder. If not -1, pixels with this index will be left at their previous value. This only adds the decoder support and isn't active yet. --- src/PIL/GifImagePlugin.py | 2 +- src/decode.c | 4 +++- src/libImaging/Gif.h | 3 +++ src/libImaging/GifDecode.c | 38 +++++++++++++++++++++----------------- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 7c083bd8b..131aa939e 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -251,7 +251,7 @@ class GifImageFile(ImageFile.ImageFile): bits = self.fp.read(1)[0] self.__offset = self.fp.tell() self.tile = [ - ("gif", (x0, y0, x1, y1), self.__offset, (bits, interlace)) + ("gif", (x0, y0, x1, y1), self.__offset, (bits, interlace, -1)) ] break diff --git a/src/decode.c b/src/decode.c index 5d64bd0b9..a29c6a46e 100644 --- a/src/decode.c +++ b/src/decode.c @@ -430,7 +430,8 @@ PyImaging_GifDecoderNew(PyObject *self, PyObject *args) { char *mode; int bits = 8; int interlace = 0; - if (!PyArg_ParseTuple(args, "s|ii", &mode, &bits, &interlace)) { + int transparency = -1; + if (!PyArg_ParseTuple(args, "s|iii", &mode, &bits, &interlace, &transparency)) { return NULL; } @@ -448,6 +449,7 @@ PyImaging_GifDecoderNew(PyObject *self, PyObject *args) { ((GIFDECODERSTATE *)decoder->state.context)->bits = bits; ((GIFDECODERSTATE *)decoder->state.context)->interlace = interlace; + ((GIFDECODERSTATE *)decoder->state.context)->transparency = transparency; return (PyObject *)decoder; } diff --git a/src/libImaging/Gif.h b/src/libImaging/Gif.h index a85ce2b6e..0f44df795 100644 --- a/src/libImaging/Gif.h +++ b/src/libImaging/Gif.h @@ -30,6 +30,9 @@ typedef struct { */ int interlace; + /* The transparent palette index, or -1 for no transparency. */ + int transparency; + /* PRIVATE CONTEXT (set by decoder) */ /* Interlace parameters */ diff --git a/src/libImaging/GifDecode.c b/src/libImaging/GifDecode.c index 88ae3896c..5392198da 100644 --- a/src/libImaging/GifDecode.c +++ b/src/libImaging/GifDecode.c @@ -248,29 +248,33 @@ ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t /* To squeeze some extra pixels out of this loop, we test for some common cases and handle them separately. */ - /* FIXME: should we handle the transparency index in here??? */ - - if (i == 1) { - if (state->x < state->xsize - 1) { - /* Single pixel, not at the end of the line. */ - *out++ = p[0]; - state->x++; + /* If we have transparency, we need to use the regular loop. */ + if (context->transparency == -1) { + if (i == 1) { + if (state->x < state->xsize - 1) { + /* Single pixel, not at the end of the line. */ + *out++ = p[0]; + state->x++; + continue; + } + } else if (state->x + i <= state->xsize) { + /* This string fits into current line. */ + memcpy(out, p, i); + out += i; + state->x += i; + if (state->x == state->xsize) { + NEWLINE(state, context); + } continue; } - } else if (state->x + i <= state->xsize) { - /* This string fits into current line. */ - memcpy(out, p, i); - out += i; - state->x += i; - if (state->x == state->xsize) { - NEWLINE(state, context); - } - continue; } /* No shortcut, copy pixel by pixel */ for (c = 0; c < i; c++) { - *out++ = p[c]; + if (p[c] != context->transparency) { + *out = p[c]; + } + out++; if (++state->x >= state->xsize) { NEWLINE(state, context); } From 18854dcf14c7c66a7c56929ddf9714fb843af8a1 Mon Sep 17 00:00:00 2001 From: Glenn Maynard Date: Tue, 23 Oct 2018 23:15:15 -0500 Subject: [PATCH 2/3] GIF: Handle GIF transparency in the decoder. Remove the special case for disposal_method == 1 and handle GIF transparency by telling the decoder the transparent index. --- src/PIL/GifImagePlugin.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 131aa939e..bf2db4260 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -145,7 +145,6 @@ class GifImageFile(ImageFile.ImageFile): self.dispose_extent = [0, 0, 0, 0] # x0, y0, x1, y1 self.__frame = -1 self.__fp.seek(self.__rewind) - self._prev_im = None self.disposal_method = 0 else: # ensure that the previous frame was loaded @@ -250,9 +249,10 @@ class GifImageFile(ImageFile.ImageFile): # image data bits = self.fp.read(1)[0] self.__offset = self.fp.tell() - self.tile = [ - ("gif", (x0, y0, x1, y1), self.__offset, (bits, interlace, -1)) - ] + self.tile = [("gif", + (x0, y0, x1, y1), + self.__offset, + (bits, interlace, info.get("transparency", -1)))] break else: @@ -295,20 +295,6 @@ class GifImageFile(ImageFile.ImageFile): def tell(self): return self.__frame - def load_end(self): - ImageFile.ImageFile.load_end(self) - - # if the disposal method is 'do not dispose', transparent - # pixels should show the content of the previous frame - if self._prev_im and self._prev_disposal_method == 1: - # we do this by pasting the updated area onto the previous - # frame which we then use as the current image content - updated = self._crop(self.im, self.dispose_extent) - self._prev_im.paste(updated, self.dispose_extent, updated.convert("RGBA")) - self.im = self._prev_im - self._prev_im = self.im.copy() - self._prev_disposal_method = self.disposal_method - def _close__fp(self): try: if self.__fp != self.fp: From b216b367ac91d5d8514d97d42b30805df7529c86 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 Mar 2021 00:24:57 +1100 Subject: [PATCH 3/3] Only set info transparency on first frame --- Tests/images/different_transparency.gif | Bin 0 -> 4118 bytes .../images/different_transparency_merged.gif | Bin 0 -> 3046 bytes Tests/test_file_gif.py | 17 +++++++++-- Tests/test_file_webp_animated.py | 4 +-- src/PIL/GifImagePlugin.py | 27 +++++++++++++----- 5 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 Tests/images/different_transparency.gif create mode 100644 Tests/images/different_transparency_merged.gif diff --git a/Tests/images/different_transparency.gif b/Tests/images/different_transparency.gif new file mode 100644 index 0000000000000000000000000000000000000000..2d36bef9e360923246f7d29c705f39ef1d732021 GIT binary patch literal 4118 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 literal 0 HcmV?d00001 diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 1b2314d51..52d7f035d 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -468,12 +468,25 @@ def test_dispose2_background(tmp_path): assert im.getpixel((0, 0)) == 0 -def test_iss634(): +def test_transparency_in_second_frame(): + with Image.open("Tests/images/different_transparency.gif") as im: + assert im.info["transparency"] == 0 + + # Seek to the second frame + im.seek(im.tell() + 1) + assert im.info["transparency"] == 0 + + assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.gif") + + +def test_no_transparency_in_second_frame(): with Image.open("Tests/images/iss634.gif") as img: # Seek to the second frame img.seek(img.tell() + 1) + assert "transparency" not in img.info + # All transparent pixels should be replaced with the color from the first frame - assert img.histogram()[img.info["transparency"]] == 0 + assert img.histogram()[255] == 0 def test_duration(tmp_path): diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 26e903488..25ebffe02 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -45,12 +45,12 @@ def test_write_animation_L(tmp_path): # Compare first and last frames to the original animated GIF orig.load() im.load() - assert_image_similar(im, orig.convert("RGBA"), 25.0) + assert_image_similar(im, orig.convert("RGBA"), 32.9) orig.seek(orig.n_frames - 1) im.seek(im.n_frames - 1) orig.load() im.load() - assert_image_similar(im, orig.convert("RGBA"), 25.0) + assert_image_similar(im, orig.convert("RGBA"), 32.9) @pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index bf2db4260..2f6d98204 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -173,6 +173,8 @@ class GifImageFile(ImageFile.ImageFile): self.palette = copy(self.global_palette) info = {} + frame_transparency = None + interlace = None while True: s = self.fp.read(1) @@ -191,7 +193,7 @@ class GifImageFile(ImageFile.ImageFile): # flags = block[0] if flags & 1: - info["transparency"] = block[3] + frame_transparency = block[3] info["duration"] = i16(block, 1) * 10 # disposal method - find the value of bits 4 - 6 @@ -249,10 +251,6 @@ class GifImageFile(ImageFile.ImageFile): # image data bits = self.fp.read(1)[0] self.__offset = self.fp.tell() - self.tile = [("gif", - (x0, y0, x1, y1), - self.__offset, - (bits, interlace, info.get("transparency", -1)))] break else: @@ -278,11 +276,26 @@ class GifImageFile(ImageFile.ImageFile): except (AttributeError, KeyError): pass - if not self.tile: + if interlace is not None: + transparency = -1 + if frame_transparency is not None: + if frame == 0: + self.info["transparency"] = frame_transparency + else: + transparency = frame_transparency + self.tile = [ + ( + "gif", + (x0, y0, x1, y1), + self.__offset, + (bits, interlace, transparency), + ) + ] + else: # self.__fp = None raise EOFError - for k in ["transparency", "duration", "comment", "extension", "loop"]: + for k in ["duration", "comment", "extension", "loop"]: if k in info: self.info[k] = info[k] elif k in self.info: