Merge branch 'main' into winbuild-update

This commit is contained in:
Andrew Murray 2022-10-20 18:23:01 +11:00 committed by GitHub
commit 3b5f6e884f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 127 additions and 30 deletions

View File

@ -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]

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -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_",

View File

@ -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")

View File

@ -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")

View File

@ -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

View File

@ -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.

View File

@ -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:
if self._frame_transparency is not None: return
self.im.putpalettealpha(self._frame_transparency, 0) if self._frame_transparency is not None:
frame_im = self.im.convert("RGBA") self.im.putpalettealpha(self._frame_transparency, 0)
else: frame_im = self.im.convert("RGBA")
frame_im = self.im.convert("RGB")
else: else:
if not self._prev_im: frame_im = self.im.convert("RGB")
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

View File

@ -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

View File

@ -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):
im_frame = im_frame.copy() if im_frame.mode == rawmode:
if im_frame.mode != im.mode: im_frame = im_frame.copy()
if im.mode == "P": else:
im_frame = im_frame.convert(im.mode, palette=im.palette) if rawmode == "P":
im_frame = im_frame.convert(rawmode, palette=im.palette)
else: else:
im_frame = im_frame.convert(im.mode) 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,7 +1221,26 @@ 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)
mode = im.mode 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
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)])

View File

@ -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"),