Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Tests/images/rewind.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/transparent_dispose.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions Tests/test_file_gif.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,46 @@ def test_dispose_background(self):
except EOFError:
pass

def test_n_frames_invariant(self):
# Regression test: make sure that reading n_frames doesn't cause the
# current image to change.
img = Image.open("Tests/images/transparent_dispose.gif")
before = img.tobytes()

self.assertEqual(img.n_frames, 3)
self.assertEqual(before, img.tobytes())

def test_transparent_dispose(self):
img = Image.open("Tests/images/transparent_dispose.gif")

expected_colors = [(2, 1, 2), (0, 1, 0), (2, 1, 2)]
for frame in range(3):
img.seek(frame)
for x in range(3):
color = img.getpixel((x,0))
self.assertEqual(color, expected_colors[frame][x],
'frame %i, x %i' % (frame, x))

def test_rewind(self):
img = Image.open("Tests/images/rewind.gif")

# Seek forwards to frame 2. This will decode frames 0 and 1.
img.seek(2)

# Seek back to frame 1. This will rewind to frame 0, then seek
# to frame 1.
img.seek(1)

# Read data. This should decode frame 0 and then frame 1.
img.getpixel((0,1))

expected_colors = [(2, 3), (3, 1)]
for x in range(2):
for y in range(2):
color = img.getpixel((x,y))
self.assertEqual(color, expected_colors[x][y],
'%ix%i' % (x, y))

def test_dispose_previous(self):
img = Image.open("Tests/images/dispose_prev.gif")
try:
Expand Down
36 changes: 16 additions & 20 deletions src/PIL/GifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,10 @@ def _seek(self, frame):
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
if not self.im:
if self.tile:
self.load()

if frame != self.__frame + 1:
Expand Down Expand Up @@ -247,7 +246,7 @@ def _seek(self, frame):
self.tile = [("gif",
(x0, y0, x1, y1),
self.__offset,
(bits, interlace))]
(bits, interlace, info.get("transparency", -1)))]
break

else:
Expand All @@ -259,9 +258,10 @@ def _seek(self, frame):
# do not dispose or none specified
self.dispose = None
elif self.disposal_method == 2:
# replace with background colour
self.dispose = Image.core.fill("P", self.size,
self.info["background"])
# replace with transparency if there is one, otherwise
# background colour
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why potentially use the transparency? I don't see this in the spec.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've found https://legacy.imagemagick.org/Usage/anim_basics/#dispose

is cleared to transparency
...
There is some thinking that rather than clearing the overlaid area to the transparent color, this disposal should clear it to the 'background' color meta-data setting stored in the GIF animation
...
However all modern web browsers clear just the area that was last overlaid to transparency, as such this is now accepted practice, and what IM now follows.

bg = info.get("transparency", self.info["background"])
self.dispose = Image.core.fill("P", self.size, bg)
else:
# replace with previous contents
if self.im:
Expand All @@ -287,23 +287,19 @@ def _seek(self, frame):
if self.palette:
self.mode = "P"

def load_prepare(self):
super(GifImageFile, self).load_prepare()

if self.__frame == 0:
# On the first frame, clear the buffer. Use the transparency index
# if we have one, otherwise use the background index.
default_color = self.info.get("transparency",
self.info.get("background", 0))
self.im.paste(default_color, (0,0, self.size[0], self.size[1]))

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.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()

def _close__fp(self):
try:
if self.__fp != self.fp:
Expand Down
4 changes: 3 additions & 1 deletion src/decode.c
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,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;

if (strcmp(mode, "L") != 0 && strcmp(mode, "P") != 0) {
Expand All @@ -451,6 +452,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;
}
Expand Down
3 changes: 3 additions & 0 deletions src/libImaging/Gif.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ typedef struct {
*/
int interlace;

/* The transparent palette index, or -1 for no transparency. */
int transparency;

/* PRIVATE CONTEXT (set by decoder) */

/* Interlace parameters */
Expand Down
38 changes: 21 additions & 17 deletions src/libImaging/GifDecode.c
Original file line number Diff line number Diff line change
Expand Up @@ -264,29 +264,33 @@ ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes)
/* 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);
}
Expand Down