Merge pull request #5857 from radarhere/gif

This commit is contained in:
Hugo van Kemenade 2021-12-06 20:58:21 +02:00 committed by GitHub
commit 94ca035495
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 135 additions and 60 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.

After

Width:  |  Height:  |  Size: 1.4 KiB

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

@ -163,6 +163,32 @@ def test_roundtrip_save_all(tmp_path):
assert reread.n_frames == 5 assert reread.n_frames == 5
@pytest.mark.parametrize(
"path, mode",
(
("Tests/images/dispose_bgnd.gif", "RGB"),
# Hexeditted copy of dispose_bgnd to add transparency
("Tests/images/dispose_bgnd_rgba.gif", "RGBA"),
),
)
def test_loading_multiple_palettes(path, mode):
with Image.open(path) as im:
assert im.mode == "P"
first_frame_colors = im.palette.colors.keys()
original_color = im.convert("RGB").load()[0, 0]
im.seek(1)
assert im.mode == mode
if mode == "RGBA":
im = im.convert("RGB")
# Check a color only from the old palette
assert im.load()[0, 0] == original_color
# Check a color from the new palette
assert im.load()[24, 24] not in first_frame_colors
def test_headers_saving_for_animated_gifs(tmp_path): def test_headers_saving_for_animated_gifs(tmp_path):
important_headers = ["background", "version", "duration", "loop"] important_headers = ["background", "version", "duration", "loop"]
# Multiframe image # Multiframe image
@ -324,7 +350,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 +366,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 +398,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 +538,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 +547,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 +956,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

@ -91,8 +91,9 @@ Pillow reads GIF87a and GIF89a versions of the GIF file format. The library
writes run-length encoded files in GIF87a by default, unless GIF89a features writes run-length encoded files in GIF87a by default, unless GIF89a features
are used or GIF89a is already in use. are used or GIF89a is already in use.
Note that GIF files are always read as grayscale (``L``) GIF files are initially read as grayscale (``L``) or palette mode (``P``)
or palette mode (``P``) images. images, but seeking to later frames in an image will change the mode to either
``RGB`` or ``RGBA``, depending on whether the first frame had transparency.
The :py:meth:`~PIL.Image.open` method sets the following The :py:meth:`~PIL.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties: :py:attr:`~PIL.Image.Image.info` properties:

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,24 +307,20 @@ 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
if interlace is not None: if interlace is not None:
transparency = -1 if frame == 0 and frame_transparency is not None:
if frame_transparency is not None: self.info["transparency"] = frame_transparency
if frame == 0:
self.info["transparency"] = frame_transparency
else:
transparency = frame_transparency
self.tile = [ self.tile = [
( (
"gif", "gif",
(x0, y0, x1, y1), (x0, y0, x1, y1),
self.__offset, self.__offset,
(bits, interlace, transparency), (bits, interlace),
) )
] ]
else: else:
@ -316,16 +333,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"),

View File

@ -433,8 +433,7 @@ PyImaging_GifDecoderNew(PyObject *self, PyObject *args) {
char *mode; char *mode;
int bits = 8; int bits = 8;
int interlace = 0; int interlace = 0;
int transparency = -1; if (!PyArg_ParseTuple(args, "s|ii", &mode, &bits, &interlace)) {
if (!PyArg_ParseTuple(args, "s|iii", &mode, &bits, &interlace, &transparency)) {
return NULL; return NULL;
} }
@ -452,7 +451,6 @@ PyImaging_GifDecoderNew(PyObject *self, PyObject *args) {
((GIFDECODERSTATE *)decoder->state.context)->bits = bits; ((GIFDECODERSTATE *)decoder->state.context)->bits = bits;
((GIFDECODERSTATE *)decoder->state.context)->interlace = interlace; ((GIFDECODERSTATE *)decoder->state.context)->interlace = interlace;
((GIFDECODERSTATE *)decoder->state.context)->transparency = transparency;
return (PyObject *)decoder; return (PyObject *)decoder;
} }

View File

@ -30,9 +30,6 @@ typedef struct {
*/ */
int interlace; int interlace;
/* The transparent palette index, or -1 for no transparency. */
int transparency;
/* PRIVATE CONTEXT (set by decoder) */ /* PRIVATE CONTEXT (set by decoder) */
/* Interlace parameters */ /* Interlace parameters */

View File

@ -248,33 +248,27 @@ ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t
/* To squeeze some extra pixels out of this loop, we test for /* To squeeze some extra pixels out of this loop, we test for
some common cases and handle them separately. */ some common cases and handle them separately. */
/* If we have transparency, we need to use the regular loop. */ if (i == 1) {
if (context->transparency == -1) { if (state->x < state->xsize - 1) {
if (i == 1) { /* Single pixel, not at the end of the line. */
if (state->x < state->xsize - 1) { *out++ = p[0];
/* Single pixel, not at the end of the line. */ state->x++;
*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; 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 */ /* No shortcut, copy pixel by pixel */
for (c = 0; c < i; c++) { for (c = 0; c < i; c++) {
if (p[c] != context->transparency) { *out++ = p[c];
*out = p[c];
}
out++;
if (++state->x >= state->xsize) { if (++state->x >= state->xsize) {
NEWLINE(state, context); NEWLINE(state, context);
} }