diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index ba277a776..8d8046280 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -78,6 +78,18 @@ def test_l_mode_subsequent_frames(): assert im.load()[0, 0] == (0, 255) +def test_strategy(): + with Image.open(TEST_GIF) as im: + expected = 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) + + GifImagePlugin.PALETTE_TO_RGB = GifImagePlugin.ModeStrategy.AFTER_FIRST + + def test_optimize(): def test_grayscale(optimize): im = Image.new("L", (1, 1), 0) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 5db1b8c7d..85004d8e3 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -110,6 +110,13 @@ 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 +available:: + + from PIL import GifImagePlugin + GifImagePlugin.PALETTE_TO_RGB = GifImagePlugin.ModeStrategy.ALWAYS + The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 764eda6ee..7f9c15400 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -28,12 +28,21 @@ import itertools import math import os import subprocess +from enum import IntEnum from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16le as i16 from ._binary import o8 from ._binary import o16le as o16 + +class ModeStrategy(IntEnum): + AFTER_FIRST = 0 + ALWAYS = 1 + + +PALETTE_TO_RGB = ModeStrategy.AFTER_FIRST + # -------------------------------------------------------------------- # Identify/read GIF files @@ -355,18 +364,22 @@ class GifImageFile(ImageFile.ImageFile): del self.info[k] if frame == 0: - self.mode = "P" if frame_palette else "L" + if frame_palette: + self.mode = "RGB" if PALETTE_TO_RGB == ModeStrategy.ALWAYS else "P" + else: + self.mode = "L" - if self.mode == "P" and not palette: + if not palette and self.global_palette: from copy import copy palette = copy(self.global_palette) self.palette = palette else: - self._frame_palette = frame_palette self._frame_transparency = frame_transparency + self._frame_palette = frame_palette def load_prepare(self): + self.mode = "P" if self._frame_palette else "L" if self.__frame == 0: if "transparency" in self.info: self.im = Image.core.fill( @@ -375,18 +388,19 @@ class GifImageFile(ImageFile.ImageFile): 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 + self._frame_palette = None super().load_prepare() def load_end(self): if self.__frame == 0: + if self.mode == "P" and PALETTE_TO_RGB == ModeStrategy.ALWAYS: + self.mode = "RGB" + self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) return if self.mode == "P": if self._frame_transparency is not None: