mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-01-12 18:26:17 +03:00
Added setting to convert to RGB only at a different palette
This commit is contained in:
parent
66bb2bd5e8
commit
ce8c682748
|
@ -79,15 +79,37 @@ def test_l_mode_subsequent_frames():
|
|||
|
||||
|
||||
def test_strategy():
|
||||
with Image.open(TEST_GIF) as im:
|
||||
expected = im.convert("RGB")
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
expected_zero = im.convert("RGB")
|
||||
|
||||
GifImagePlugin.PALETTE_TO_RGB = GifImagePlugin.ModeStrategy.ALWAYS
|
||||
with Image.open(TEST_GIF) as im:
|
||||
assert im.mode == "RGB"
|
||||
assert_image_equal(im, expected)
|
||||
im.seek(1)
|
||||
expected_one = im.convert("RGB")
|
||||
|
||||
GifImagePlugin.PALETTE_TO_RGB = GifImagePlugin.ModeStrategy.AFTER_FIRST
|
||||
try:
|
||||
GifImagePlugin.PALETTE_TO_RGB = GifImagePlugin.ModeStrategy.ALWAYS
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert im.mode == "RGB"
|
||||
assert_image_equal(im, expected_zero)
|
||||
|
||||
GifImagePlugin.PALETTE_TO_RGB = (
|
||||
GifImagePlugin.ModeStrategy.DIFFERENT_PALETTE_ONLY
|
||||
)
|
||||
# Stay in P mode with only a global palette
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert im.mode == "P"
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "P"
|
||||
assert_image_equal(im.convert("RGB"), expected_one)
|
||||
|
||||
# Change to RGB mode when a frame has an individual palette
|
||||
with Image.open("Tests/images/iss634.gif") as im:
|
||||
assert im.mode == "P"
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "RGB"
|
||||
finally:
|
||||
GifImagePlugin.PALETTE_TO_RGB = GifImagePlugin.ModeStrategy.AFTER_FIRST
|
||||
|
||||
|
||||
def test_optimize():
|
||||
|
@ -414,12 +436,29 @@ def test_dispose_background_transparency():
|
|||
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)),
|
||||
]
|
||||
@pytest.mark.parametrize(
|
||||
"mode_strategy, expected_colors",
|
||||
(
|
||||
(
|
||||
GifImagePlugin.ModeStrategy.DIFFERENT_PALETTE_ONLY,
|
||||
(
|
||||
(2, 1, 2),
|
||||
(0, 1, 0),
|
||||
(2, 1, 2),
|
||||
),
|
||||
),
|
||||
(
|
||||
GifImagePlugin.ModeStrategy.AFTER_FIRST,
|
||||
(
|
||||
(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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_transparent_dispose(mode_strategy, expected_colors):
|
||||
GifImagePlugin.PALETTE_TO_RGB = mode_strategy
|
||||
with Image.open("Tests/images/transparent_dispose.gif") as img:
|
||||
for frame in range(3):
|
||||
img.seek(frame)
|
||||
|
|
|
@ -110,13 +110,25 @@ images. Seeking to later frames in a ``P`` image will change the image to
|
|||
``RGB`` (or ``RGBA`` if the first frame had transparency). ``L`` images will
|
||||
stay in ``L`` mode (or change to ``LA`` if the first frame had transparency).
|
||||
|
||||
If you would prefer the first ``P`` image frame to be ``RGB``, so that ``P``
|
||||
frames are always converted to ``RGB`` or ``RGBA`` mode, there is a setting
|
||||
``P`` mode images are changed to ``RGB`` because each frame of a GIF may
|
||||
introduce up to 256 colors. Because ``P`` can only have up to 256 colors, the
|
||||
image is converted to handle all of the colors.
|
||||
|
||||
If you would prefer the first ``P`` image frame to be ``RGB`` as well, so that
|
||||
every ``P`` frame is converted to ``RGB`` or ``RGBA`` mode, there is a setting
|
||||
available::
|
||||
|
||||
from PIL import GifImagePlugin
|
||||
GifImagePlugin.PALETTE_TO_RGB = GifImagePlugin.ModeStrategy.ALWAYS
|
||||
|
||||
GIF frames do not always contain individual palettes however. If there is only
|
||||
a global palette, then all of the colors can fit within ``P`` mode. If you would
|
||||
prefer the frames to be kept as ``P`` in that case, there is also a setting
|
||||
available::
|
||||
|
||||
from PIL import GifImagePlugin
|
||||
GifImagePlugin.PALETTE_TO_RGB = GifImagePlugin.ModeStrategy.DIFFERENT_PALETTE_ONLY
|
||||
|
||||
The :py:meth:`~PIL.Image.open` method sets the following
|
||||
:py:attr:`~PIL.Image.Image.info` properties:
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ from ._binary import o16le as o16
|
|||
class ModeStrategy(IntEnum):
|
||||
AFTER_FIRST = 0
|
||||
ALWAYS = 1
|
||||
DIFFERENT_PALETTE_ONLY = 2
|
||||
|
||||
|
||||
PALETTE_TO_RGB = ModeStrategy.AFTER_FIRST
|
||||
|
@ -152,7 +153,6 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
# rewind
|
||||
self.__offset = 0
|
||||
self.dispose = None
|
||||
self.dispose_extent = [0, 0, 0, 0] # x0, y0, x1, y1
|
||||
self.__frame = -1
|
||||
self.__fp.seek(self.__rewind)
|
||||
self.disposal_method = 0
|
||||
|
@ -180,33 +180,12 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
|
||||
self.tile = []
|
||||
|
||||
if update_image:
|
||||
if self.__frame == 1:
|
||||
self.pyaccess = None
|
||||
if self.mode == "P":
|
||||
if "transparency" in self.info:
|
||||
self.im.putpalettealpha(self.info["transparency"], 0)
|
||||
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
|
||||
self.mode = "RGBA"
|
||||
del self.info["transparency"]
|
||||
else:
|
||||
self.mode = "RGB"
|
||||
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
|
||||
elif "transparency" in self.info:
|
||||
self.im = self.im.convert_transparent(
|
||||
"LA", self.info["transparency"]
|
||||
)
|
||||
self.mode = "LA"
|
||||
del self.info["transparency"]
|
||||
|
||||
if self.dispose:
|
||||
self.im.paste(self.dispose, self.dispose_extent)
|
||||
|
||||
palette = None
|
||||
|
||||
info = {}
|
||||
frame_transparency = None
|
||||
interlace = None
|
||||
frame_dispose_extent = None
|
||||
while True:
|
||||
|
||||
if not s:
|
||||
|
@ -273,7 +252,7 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
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
|
||||
frame_dispose_extent = x0, y0, x1, y1
|
||||
flags = s[8]
|
||||
|
||||
interlace = (flags & 64) != 0
|
||||
|
@ -298,15 +277,48 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
if not update_image:
|
||||
return
|
||||
|
||||
frame_palette = palette or self.global_palette
|
||||
if self.dispose:
|
||||
self.im.paste(self.dispose, self.dispose_extent)
|
||||
|
||||
self._frame_palette = palette or self.global_palette
|
||||
if frame == 0:
|
||||
if self._frame_palette:
|
||||
self.mode = "RGB" if PALETTE_TO_RGB == ModeStrategy.ALWAYS else "P"
|
||||
else:
|
||||
self.mode = "L"
|
||||
|
||||
if not palette and self.global_palette:
|
||||
from copy import copy
|
||||
|
||||
palette = copy(self.global_palette)
|
||||
self.palette = palette
|
||||
else:
|
||||
self._frame_transparency = frame_transparency
|
||||
if self.mode == "P":
|
||||
if PALETTE_TO_RGB != ModeStrategy.DIFFERENT_PALETTE_ONLY or palette:
|
||||
self.pyaccess = None
|
||||
if "transparency" in self.info:
|
||||
self.im.putpalettealpha(self.info["transparency"], 0)
|
||||
self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG)
|
||||
self.mode = "RGBA"
|
||||
del self.info["transparency"]
|
||||
else:
|
||||
self.mode = "RGB"
|
||||
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
|
||||
elif self.mode == "L" and "transparency" in self.info:
|
||||
self.pyaccess = None
|
||||
self.im = self.im.convert_transparent("LA", self.info["transparency"])
|
||||
self.mode = "LA"
|
||||
del self.info["transparency"]
|
||||
|
||||
def _rgb(color):
|
||||
if frame_palette:
|
||||
color = tuple(frame_palette.palette[color * 3 : color * 3 + 3])
|
||||
if self._frame_palette:
|
||||
color = tuple(self._frame_palette.palette[color * 3 : color * 3 + 3])
|
||||
else:
|
||||
color = (color, color, color)
|
||||
return color
|
||||
|
||||
self.dispose_extent = frame_dispose_extent
|
||||
try:
|
||||
if self.disposal_method < 2:
|
||||
# do not dispose or none specified
|
||||
|
@ -321,13 +333,17 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
Image._decompression_bomb_check(dispose_size)
|
||||
|
||||
# by convention, attempt to use transparency first
|
||||
dispose_mode = "P"
|
||||
color = self.info.get("transparency", frame_transparency)
|
||||
if color is not None:
|
||||
dispose_mode = "RGBA"
|
||||
color = _rgb(color) + (0,)
|
||||
if self.mode in ("RGB", "RGBA"):
|
||||
dispose_mode = "RGBA"
|
||||
color = _rgb(color) + (0,)
|
||||
else:
|
||||
dispose_mode = "RGB"
|
||||
color = _rgb(self.info.get("background", 0))
|
||||
color = self.info.get("background", 0)
|
||||
if self.mode in ("RGB", "RGBA"):
|
||||
dispose_mode = "RGB"
|
||||
color = _rgb(color)
|
||||
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
|
||||
else:
|
||||
# replace with previous contents
|
||||
|
@ -339,21 +355,28 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
dispose_size = (x1 - x0, y1 - y0)
|
||||
|
||||
Image._decompression_bomb_check(dispose_size)
|
||||
self.dispose = Image.core.fill(
|
||||
"RGBA", dispose_size, _rgb(frame_transparency) + (0,)
|
||||
)
|
||||
dispose_mode = "P"
|
||||
color = frame_transparency
|
||||
if self.mode in ("RGB", "RGBA"):
|
||||
dispose_mode = "RGBA"
|
||||
color = _rgb(frame_transparency) + (0,)
|
||||
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if interlace is not None:
|
||||
if frame == 0 and frame_transparency is not None:
|
||||
self.info["transparency"] = frame_transparency
|
||||
transparency = -1
|
||||
if frame_transparency is not None:
|
||||
if frame == 0:
|
||||
self.info["transparency"] = frame_transparency
|
||||
elif self.mode not in ("RGB", "RGBA", "LA"):
|
||||
transparency = frame_transparency
|
||||
self.tile = [
|
||||
(
|
||||
"gif",
|
||||
(x0, y0, x1, y1),
|
||||
self.__offset,
|
||||
(bits, interlace),
|
||||
(bits, interlace, transparency),
|
||||
)
|
||||
]
|
||||
|
||||
|
@ -363,35 +386,22 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
elif k in self.info:
|
||||
del self.info[k]
|
||||
|
||||
if frame == 0:
|
||||
if frame_palette:
|
||||
self.mode = "RGB" if PALETTE_TO_RGB == ModeStrategy.ALWAYS else "P"
|
||||
else:
|
||||
self.mode = "L"
|
||||
|
||||
if not palette and self.global_palette:
|
||||
from copy import copy
|
||||
|
||||
palette = copy(self.global_palette)
|
||||
self.palette = palette
|
||||
else:
|
||||
self._frame_transparency = frame_transparency
|
||||
self._frame_palette = frame_palette
|
||||
|
||||
def load_prepare(self):
|
||||
self.mode = "P" if self._frame_palette else "L"
|
||||
temp_mode = "P" if self._frame_palette else "L"
|
||||
self._prev_im = None
|
||||
if self.__frame == 0:
|
||||
if "transparency" in self.info:
|
||||
self.im = Image.core.fill(
|
||||
self.mode, self.size, self.info["transparency"]
|
||||
temp_mode, self.size, self.info["transparency"]
|
||||
)
|
||||
else:
|
||||
elif self.mode in ("RGB", "RGBA", "LA"):
|
||||
self._prev_im = self.im
|
||||
if self._frame_palette:
|
||||
self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
|
||||
self.im.putpalette(*self._frame_palette.getdata())
|
||||
else:
|
||||
self.im = None
|
||||
self.mode = temp_mode
|
||||
self._frame_palette = None
|
||||
|
||||
super().load_prepare()
|
||||
|
@ -402,17 +412,18 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
self.mode = "RGB"
|
||||
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG)
|
||||
return
|
||||
if self.mode == "P":
|
||||
if self.mode == "P" and self._prev_im:
|
||||
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")
|
||||
elif self.mode == "L" and self._frame_transparency is not None:
|
||||
frame_im = self.im.convert_transparent("LA", self._frame_transparency)
|
||||
else:
|
||||
if self._frame_transparency is not None:
|
||||
frame_im = self.im.convert_transparent("LA", self._frame_transparency)
|
||||
else:
|
||||
frame_im = self.im
|
||||
if not self._prev_im:
|
||||
return
|
||||
frame_im = self.im
|
||||
frame_im = self._crop(frame_im, self.dispose_extent)
|
||||
|
||||
self.im = self._prev_im
|
||||
|
|
|
@ -433,7 +433,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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -248,27 +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. */
|
||||
|
||||
if (i == 1) {
|
||||
if (state->x < state->xsize - 1) {
|
||||
/* Single pixel, not at the end of the line. */
|
||||
*out++ = p[0];
|
||||
state->x++;
|
||||
/* This cannot be used if there is transparency */
|
||||
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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user