mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 01:16:16 +03:00
Merge branch 'main' into winbuild-update
This commit is contained in:
commit
3b5f6e884f
12
CHANGES.rst
12
CHANGES.rst
|
@ -5,6 +5,18 @@ Changelog (Pillow)
|
||||||
9.3.0 (unreleased)
|
9.3.0 (unreleased)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
- Added conversion between RGB/RGBA/RGBX and LAB #6647
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Do not attempt normalization if mode is already normal #6644
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed seeking to an L frame in a GIF #6576
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Consider all frames when selecting mode for PNG save_all #6610
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
- Don't reassign crc on ChunkStream close #6627
|
- Don't reassign crc on ChunkStream close #6627
|
||||||
[wiredfool, radarhere]
|
[wiredfool, radarhere]
|
||||||
|
|
||||||
|
|
BIN
Tests/images/no_palette_after_rgb.gif
Normal file
BIN
Tests/images/no_palette_after_rgb.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 101 B |
BIN
Tests/images/palette_not_needed_for_second_frame.gif
Normal file
BIN
Tests/images/palette_not_needed_for_second_frame.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
|
@ -647,6 +647,16 @@ def test_seek_after_close():
|
||||||
im.seek(0)
|
im.seek(0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
|
||||||
|
def test_different_modes_in_later_frames(mode, tmp_path):
|
||||||
|
test_file = str(tmp_path / "temp.png")
|
||||||
|
|
||||||
|
im = Image.new("L", (1, 1))
|
||||||
|
im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))])
|
||||||
|
with Image.open(test_file) as reloaded:
|
||||||
|
assert reloaded.mode == mode
|
||||||
|
|
||||||
|
|
||||||
def test_constants_deprecation():
|
def test_constants_deprecation():
|
||||||
for enum, prefix in {
|
for enum, prefix in {
|
||||||
PngImagePlugin.Disposal: "APNG_DISPOSE_",
|
PngImagePlugin.Disposal: "APNG_DISPOSE_",
|
||||||
|
|
|
@ -83,6 +83,21 @@ def test_l_mode_transparency():
|
||||||
assert im.load()[0, 0] == 128
|
assert im.load()[0, 0] == 128
|
||||||
|
|
||||||
|
|
||||||
|
def test_l_mode_after_rgb():
|
||||||
|
with Image.open("Tests/images/no_palette_after_rgb.gif") as im:
|
||||||
|
im.seek(1)
|
||||||
|
assert im.mode == "RGB"
|
||||||
|
|
||||||
|
im.seek(2)
|
||||||
|
assert im.mode == "RGB"
|
||||||
|
|
||||||
|
|
||||||
|
def test_palette_not_needed_for_second_frame():
|
||||||
|
with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im:
|
||||||
|
im.seek(1)
|
||||||
|
assert_image_similar(im, hopper("L").convert("RGB"), 8)
|
||||||
|
|
||||||
|
|
||||||
def test_strategy():
|
def test_strategy():
|
||||||
with Image.open("Tests/images/iss634.gif") as im:
|
with Image.open("Tests/images/iss634.gif") as im:
|
||||||
expected_rgb_always = im.convert("RGB")
|
expected_rgb_always = im.convert("RGB")
|
||||||
|
|
|
@ -38,6 +38,12 @@ def test_sanity():
|
||||||
convert(im, output_mode)
|
convert(im, output_mode)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unsupported_conversion():
|
||||||
|
im = hopper()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
im.convert("INVALID")
|
||||||
|
|
||||||
|
|
||||||
def test_default():
|
def test_default():
|
||||||
|
|
||||||
im = hopper("P")
|
im = hopper("P")
|
||||||
|
@ -242,6 +248,17 @@ def test_p2pa_palette():
|
||||||
assert im_pa.getpalette() == im.getpalette()
|
assert im_pa.getpalette() == im.getpalette()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
|
||||||
|
def test_rgb_lab(mode):
|
||||||
|
im = Image.new(mode, (1, 1))
|
||||||
|
converted_im = im.convert("LAB")
|
||||||
|
assert converted_im.getpixel((0, 0)) == (0, 128, 128)
|
||||||
|
|
||||||
|
im = Image.new("LAB", (1, 1), (255, 0, 0))
|
||||||
|
converted_im = im.convert(mode)
|
||||||
|
assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255)
|
||||||
|
|
||||||
|
|
||||||
def test_matrix_illegal_conversion():
|
def test_matrix_illegal_conversion():
|
||||||
# Arrange
|
# Arrange
|
||||||
im = hopper("CMYK")
|
im = hopper("CMYK")
|
||||||
|
|
|
@ -202,7 +202,7 @@ Pillow now builds binary wheels for musllinux, suitable for Linux distributions
|
||||||
(rather than the glibc library used by manylinux wheels). See :pep:`656`.
|
(rather than the glibc library used by manylinux wheels). See :pep:`656`.
|
||||||
|
|
||||||
ImageShow temporary files on Unix
|
ImageShow temporary files on Unix
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
When calling :py:meth:`~PIL.Image.Image.show` or using :py:mod:`~PIL.ImageShow`,
|
When calling :py:meth:`~PIL.Image.Image.show` or using :py:mod:`~PIL.ImageShow`,
|
||||||
a temporary file is created from the image. On Unix, Pillow will no longer delete these
|
a temporary file is created from the image. On Unix, Pillow will no longer delete these
|
||||||
|
|
|
@ -63,7 +63,13 @@ TODO
|
||||||
Other Changes
|
Other Changes
|
||||||
=============
|
=============
|
||||||
|
|
||||||
Added DDS ATI1 and ATI2 reading
|
Added DDS ATI1, ATI2 and BC6H reading
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Support has been added to read the ATI1 and ATI2 formats of DDS images.
|
Support has been added to read the ATI1, ATI2 and BC6H formats of DDS images.
|
||||||
|
|
||||||
|
Show all frames with ImageShow
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
When calling :py:meth:`~PIL.Image.Image.show` or using
|
||||||
|
:py:mod:`~PIL.ImageShow`, all frames will now be shown.
|
||||||
|
|
|
@ -274,6 +274,8 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
p = self.fp.read(3 << bits)
|
p = self.fp.read(3 << bits)
|
||||||
if self._is_palette_needed(p):
|
if self._is_palette_needed(p):
|
||||||
palette = ImagePalette.raw("RGB", p)
|
palette = ImagePalette.raw("RGB", p)
|
||||||
|
else:
|
||||||
|
palette = False
|
||||||
|
|
||||||
# image data
|
# image data
|
||||||
bits = self.fp.read(1)[0]
|
bits = self.fp.read(1)[0]
|
||||||
|
@ -298,7 +300,7 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
if self.dispose:
|
if self.dispose:
|
||||||
self.im.paste(self.dispose, self.dispose_extent)
|
self.im.paste(self.dispose, self.dispose_extent)
|
||||||
|
|
||||||
self._frame_palette = palette or self.global_palette
|
self._frame_palette = palette if palette is not None else self.global_palette
|
||||||
self._frame_transparency = frame_transparency
|
self._frame_transparency = frame_transparency
|
||||||
if frame == 0:
|
if frame == 0:
|
||||||
if self._frame_palette:
|
if self._frame_palette:
|
||||||
|
@ -438,16 +440,13 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
self.mode = "RGB"
|
self.mode = "RGB"
|
||||||
self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
|
self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
|
||||||
return
|
return
|
||||||
if self.mode == "P" and self._prev_im:
|
if not self._prev_im:
|
||||||
|
return
|
||||||
if self._frame_transparency is not None:
|
if self._frame_transparency is not None:
|
||||||
self.im.putpalettealpha(self._frame_transparency, 0)
|
self.im.putpalettealpha(self._frame_transparency, 0)
|
||||||
frame_im = self.im.convert("RGBA")
|
frame_im = self.im.convert("RGBA")
|
||||||
else:
|
else:
|
||||||
frame_im = self.im.convert("RGB")
|
frame_im = self.im.convert("RGB")
|
||||||
else:
|
|
||||||
if not self._prev_im:
|
|
||||||
return
|
|
||||||
frame_im = self.im
|
|
||||||
frame_im = self._crop(frame_im, self.dispose_extent)
|
frame_im = self._crop(frame_im, self.dispose_extent)
|
||||||
|
|
||||||
self.im = self._prev_im
|
self.im = self._prev_im
|
||||||
|
|
|
@ -880,7 +880,7 @@ class Image:
|
||||||
and the palette can be represented without a palette.
|
and the palette can be represented without a palette.
|
||||||
|
|
||||||
The current version supports all possible conversions between
|
The current version supports all possible conversions between
|
||||||
"L", "RGB" and "CMYK." The ``matrix`` argument only supports "L"
|
"L", "RGB" and "CMYK". The ``matrix`` argument only supports "L"
|
||||||
and "RGB".
|
and "RGB".
|
||||||
|
|
||||||
When translating a color image to greyscale (mode "L"),
|
When translating a color image to greyscale (mode "L"),
|
||||||
|
@ -899,6 +899,9 @@ class Image:
|
||||||
this passes the operation to :py:meth:`~PIL.Image.Image.quantize`,
|
this passes the operation to :py:meth:`~PIL.Image.Image.quantize`,
|
||||||
and ``dither`` and ``palette`` are ignored.
|
and ``dither`` and ``palette`` are ignored.
|
||||||
|
|
||||||
|
When converting from "PA", if an "RGBA" palette is present, the alpha
|
||||||
|
channel from the image will be used instead of the values from the palette.
|
||||||
|
|
||||||
:param mode: The requested mode. See: :ref:`concept-modes`.
|
:param mode: The requested mode. See: :ref:`concept-modes`.
|
||||||
:param matrix: An optional conversion matrix. If given, this
|
:param matrix: An optional conversion matrix. If given, this
|
||||||
should be 4- or 12-tuple containing floating point values.
|
should be 4- or 12-tuple containing floating point values.
|
||||||
|
@ -1039,6 +1042,19 @@ class Image:
|
||||||
warnings.warn("Couldn't allocate palette entry for transparency")
|
warnings.warn("Couldn't allocate palette entry for transparency")
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
if "LAB" in (self.mode, mode):
|
||||||
|
other_mode = mode if self.mode == "LAB" else self.mode
|
||||||
|
if other_mode in ("RGB", "RGBA", "RGBX"):
|
||||||
|
from . import ImageCms
|
||||||
|
|
||||||
|
srgb = ImageCms.createProfile("sRGB")
|
||||||
|
lab = ImageCms.createProfile("LAB")
|
||||||
|
profiles = [lab, srgb] if self.mode == "LAB" else [srgb, lab]
|
||||||
|
transform = ImageCms.buildTransform(
|
||||||
|
profiles[0], profiles[1], self.mode, mode
|
||||||
|
)
|
||||||
|
return transform.apply(self)
|
||||||
|
|
||||||
# colorspace conversion
|
# colorspace conversion
|
||||||
if dither is None:
|
if dither is None:
|
||||||
dither = Dither.FLOYDSTEINBERG
|
dither = Dither.FLOYDSTEINBERG
|
||||||
|
@ -1048,7 +1064,10 @@ class Image:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
try:
|
try:
|
||||||
# normalize source image and try again
|
# normalize source image and try again
|
||||||
im = self.im.convert(getmodebase(self.mode))
|
modebase = getmodebase(self.mode)
|
||||||
|
if modebase == self.mode:
|
||||||
|
raise
|
||||||
|
im = self.im.convert(modebase)
|
||||||
im = im.convert(mode, dither)
|
im = im.convert(mode, dither)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
raise ValueError("illegal conversion") from e
|
raise ValueError("illegal conversion") from e
|
||||||
|
|
|
@ -1089,28 +1089,28 @@ class _fdat:
|
||||||
self.seq_num += 1
|
self.seq_num += 1
|
||||||
|
|
||||||
|
|
||||||
def _write_multiple_frames(im, fp, chunk, rawmode):
|
def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images):
|
||||||
default_image = im.encoderinfo.get("default_image", im.info.get("default_image"))
|
|
||||||
duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
|
duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
|
||||||
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
|
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
|
||||||
disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
|
disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
|
||||||
blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))
|
blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))
|
||||||
|
|
||||||
if default_image:
|
if default_image:
|
||||||
chain = itertools.chain(im.encoderinfo.get("append_images", []))
|
chain = itertools.chain(append_images)
|
||||||
else:
|
else:
|
||||||
chain = itertools.chain([im], im.encoderinfo.get("append_images", []))
|
chain = itertools.chain([im], append_images)
|
||||||
|
|
||||||
im_frames = []
|
im_frames = []
|
||||||
frame_count = 0
|
frame_count = 0
|
||||||
for im_seq in chain:
|
for im_seq in chain:
|
||||||
for im_frame in ImageSequence.Iterator(im_seq):
|
for im_frame in ImageSequence.Iterator(im_seq):
|
||||||
|
if im_frame.mode == rawmode:
|
||||||
im_frame = im_frame.copy()
|
im_frame = im_frame.copy()
|
||||||
if im_frame.mode != im.mode:
|
|
||||||
if im.mode == "P":
|
|
||||||
im_frame = im_frame.convert(im.mode, palette=im.palette)
|
|
||||||
else:
|
else:
|
||||||
im_frame = im_frame.convert(im.mode)
|
if rawmode == "P":
|
||||||
|
im_frame = im_frame.convert(rawmode, palette=im.palette)
|
||||||
|
else:
|
||||||
|
im_frame = im_frame.convert(rawmode)
|
||||||
encoderinfo = im.encoderinfo.copy()
|
encoderinfo = im.encoderinfo.copy()
|
||||||
if isinstance(duration, (list, tuple)):
|
if isinstance(duration, (list, tuple)):
|
||||||
encoderinfo["duration"] = duration[frame_count]
|
encoderinfo["duration"] = duration[frame_count]
|
||||||
|
@ -1221,6 +1221,25 @@ def _save_all(im, fp, filename):
|
||||||
def _save(im, fp, filename, chunk=putchunk, save_all=False):
|
def _save(im, fp, filename, chunk=putchunk, save_all=False):
|
||||||
# save an image to disk (called by the save method)
|
# save an image to disk (called by the save method)
|
||||||
|
|
||||||
|
if save_all:
|
||||||
|
default_image = im.encoderinfo.get(
|
||||||
|
"default_image", im.info.get("default_image")
|
||||||
|
)
|
||||||
|
modes = set()
|
||||||
|
append_images = im.encoderinfo.get("append_images", [])
|
||||||
|
if default_image:
|
||||||
|
chain = itertools.chain(append_images)
|
||||||
|
else:
|
||||||
|
chain = itertools.chain([im], append_images)
|
||||||
|
for im_seq in chain:
|
||||||
|
for im_frame in ImageSequence.Iterator(im_seq):
|
||||||
|
modes.add(im_frame.mode)
|
||||||
|
for mode in ("RGBA", "RGB", "P"):
|
||||||
|
if mode in modes:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
mode = modes.pop()
|
||||||
|
else:
|
||||||
mode = im.mode
|
mode = im.mode
|
||||||
|
|
||||||
if mode == "P":
|
if mode == "P":
|
||||||
|
@ -1373,7 +1392,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
|
||||||
chunk(fp, b"eXIf", exif)
|
chunk(fp, b"eXIf", exif)
|
||||||
|
|
||||||
if save_all:
|
if save_all:
|
||||||
_write_multiple_frames(im, fp, chunk, rawmode)
|
_write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
|
||||||
else:
|
else:
|
||||||
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
|
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
|
||||||
|
|
||||||
|
|
|
@ -355,9 +355,9 @@ deps = {
|
||||||
"libs": [r"imagequant.lib"],
|
"libs": [r"imagequant.lib"],
|
||||||
},
|
},
|
||||||
"harfbuzz": {
|
"harfbuzz": {
|
||||||
"url": "https://github.com/harfbuzz/harfbuzz/archive/5.3.0.zip",
|
"url": "https://github.com/harfbuzz/harfbuzz/archive/5.3.1.zip",
|
||||||
"filename": "harfbuzz-5.3.0.zip",
|
"filename": "harfbuzz-5.3.1.zip",
|
||||||
"dir": "harfbuzz-5.3.0",
|
"dir": "harfbuzz-5.3.1",
|
||||||
"license": "COPYING",
|
"license": "COPYING",
|
||||||
"build": [
|
"build": [
|
||||||
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
|
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user