Skip to content

Commit

Permalink
Merge branch 'main' into winbuild-update
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere committed Oct 20, 2022
2 parents 147c52f + 0a4c6ab commit 3b5f6e8
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 30 deletions.
12 changes: 12 additions & 0 deletions CHANGES.rst
Expand Up @@ -5,6 +5,18 @@ Changelog (Pillow)
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
[wiredfool, radarhere]

Expand Down
Binary file added Tests/images/no_palette_after_rgb.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions Tests/test_file_apng.py
Expand Up @@ -647,6 +647,16 @@ def test_seek_after_close():
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():
for enum, prefix in {
PngImagePlugin.Disposal: "APNG_DISPOSE_",
Expand Down
15 changes: 15 additions & 0 deletions Tests/test_file_gif.py
Expand Up @@ -83,6 +83,21 @@ def test_l_mode_transparency():
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():
with Image.open("Tests/images/iss634.gif") as im:
expected_rgb_always = im.convert("RGB")
Expand Down
17 changes: 17 additions & 0 deletions Tests/test_image_convert.py
Expand Up @@ -38,6 +38,12 @@ def convert(im, mode):
convert(im, output_mode)


def test_unsupported_conversion():
im = hopper()
with pytest.raises(ValueError):
im.convert("INVALID")


def test_default():

im = hopper("P")
Expand Down Expand Up @@ -242,6 +248,17 @@ def test_p2pa_palette():
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():
# Arrange
im = hopper("CMYK")
Expand Down
2 changes: 1 addition & 1 deletion docs/releasenotes/9.1.0.rst
Expand Up @@ -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`.

ImageShow temporary files on Unix
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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
Expand Down
12 changes: 9 additions & 3 deletions docs/releasenotes/9.3.0.rst
Expand Up @@ -63,7 +63,13 @@ TODO
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.
19 changes: 9 additions & 10 deletions src/PIL/GifImagePlugin.py
Expand Up @@ -274,6 +274,8 @@ def _seek(self, frame, update_image=True):
p = self.fp.read(3 << bits)
if self._is_palette_needed(p):
palette = ImagePalette.raw("RGB", p)
else:
palette = False

# image data
bits = self.fp.read(1)[0]
Expand All @@ -298,7 +300,7 @@ def _seek(self, frame, update_image=True):
if self.dispose:
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
if frame == 0:
if self._frame_palette:
Expand Down Expand Up @@ -438,16 +440,13 @@ def load_end(self):
self.mode = "RGB"
self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
return
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")
if not self._prev_im:
return
if self._frame_transparency is not None:
self.im.putpalettealpha(self._frame_transparency, 0)
frame_im = self.im.convert("RGBA")
else:
if not self._prev_im:
return
frame_im = self.im
frame_im = self.im.convert("RGB")
frame_im = self._crop(frame_im, self.dispose_extent)

self.im = self._prev_im
Expand Down
23 changes: 21 additions & 2 deletions src/PIL/Image.py
Expand Up @@ -880,7 +880,7 @@ def convert(
and the palette can be represented without a palette.
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".
When translating a color image to greyscale (mode "L"),
Expand All @@ -899,6 +899,9 @@ def convert(
this passes the operation to :py:meth:`~PIL.Image.Image.quantize`,
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 matrix: An optional conversion matrix. If given, this
should be 4- or 12-tuple containing floating point values.
Expand Down Expand Up @@ -1039,6 +1042,19 @@ def convert_transparency(m, v):
warnings.warn("Couldn't allocate palette entry for transparency")
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
if dither is None:
dither = Dither.FLOYDSTEINBERG
Expand All @@ -1048,7 +1064,10 @@ def convert_transparency(m, v):
except ValueError:
try:
# 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)
except KeyError as e:
raise ValueError("illegal conversion") from e
Expand Down
41 changes: 30 additions & 11 deletions src/PIL/PngImagePlugin.py
Expand Up @@ -1089,28 +1089,28 @@ def write(self, data):
self.seq_num += 1


def _write_multiple_frames(im, fp, chunk, rawmode):
default_image = im.encoderinfo.get("default_image", im.info.get("default_image"))
def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images):
duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))

if default_image:
chain = itertools.chain(im.encoderinfo.get("append_images", []))
chain = itertools.chain(append_images)
else:
chain = itertools.chain([im], im.encoderinfo.get("append_images", []))
chain = itertools.chain([im], append_images)

im_frames = []
frame_count = 0
for im_seq in chain:
for im_frame in ImageSequence.Iterator(im_seq):
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)
if im_frame.mode == rawmode:
im_frame = im_frame.copy()
else:
if rawmode == "P":
im_frame = im_frame.convert(rawmode, palette=im.palette)
else:
im_frame = im_frame.convert(im.mode)
im_frame = im_frame.convert(rawmode)
encoderinfo = im.encoderinfo.copy()
if isinstance(duration, (list, tuple)):
encoderinfo["duration"] = duration[frame_count]
Expand Down Expand Up @@ -1221,7 +1221,26 @@ def _save_all(im, fp, filename):
def _save(im, fp, filename, chunk=putchunk, save_all=False):
# 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":

Expand Down Expand Up @@ -1373,7 +1392,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False):
chunk(fp, b"eXIf", exif)

if save_all:
_write_multiple_frames(im, fp, chunk, rawmode)
_write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
else:
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])

Expand Down
6 changes: 3 additions & 3 deletions winbuild/build_prepare.py
Expand Up @@ -355,9 +355,9 @@ def cmd_msbuild(
"libs": [r"imagequant.lib"],
},
"harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/5.3.0.zip",
"filename": "harfbuzz-5.3.0.zip",
"dir": "harfbuzz-5.3.0",
"url": "https://github.com/harfbuzz/harfbuzz/archive/5.3.1.zip",
"filename": "harfbuzz-5.3.1.zip",
"dir": "harfbuzz-5.3.1",
"license": "COPYING",
"build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
Expand Down

0 comments on commit 3b5f6e8

Please sign in to comment.