Merge branch 'main' into winbuild-update

This commit is contained in:
Andrew Murray 2022-10-14 12:20:34 +11:00 committed by GitHub
commit 147c52f92f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 198 additions and 58 deletions

View File

@ -5,6 +5,30 @@ Changelog (Pillow)
9.3.0 (unreleased) 9.3.0 (unreleased)
------------------ ------------------
- Don't reassign crc on ChunkStream close #6627
[wiredfool, radarhere]
- Raise a warning if NumPy failed to raise an error during conversion #6594
[radarhere]
- Show all frames in ImageShow #6611
[radarhere]
- Allow FLI palette chunk to not be first #6626
[radarhere]
- If first GIF frame has transparency for RGB_ALWAYS loading strategy, use RGBA mode #6592
[radarhere]
- Round box position to integer when pasting embedded color #6517
[radarhere, nulano]
- Removed EXIF prefix when saving WebP #6582
[radarhere]
- Pad IM palette to 768 bytes when saving #6579
[radarhere]
- Added DDS BC6 reading #6449 - Added DDS BC6 reading #6449
[ShadelessFox, REDxEYE, radarhere] [ShadelessFox, REDxEYE, radarhere]

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

View File

@ -4,7 +4,7 @@ import pytest
from PIL import FliImagePlugin, Image from PIL import FliImagePlugin, Image
from .helper import assert_image_equal_tofile, is_pypy from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy
# created as an export of a palette image from Gimp2.6 # created as an export of a palette image from Gimp2.6
# save as...-> hopper.fli, default options. # save as...-> hopper.fli, default options.
@ -79,6 +79,12 @@ def test_invalid_file():
FliImagePlugin.FliImageFile(invalid_file) FliImagePlugin.FliImageFile(invalid_file)
def test_palette_chunk_second():
with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im:
with Image.open(static_test_file) as expected:
assert_image_equal(im.convert("RGB"), expected.convert("RGB"))
def test_n_frames(): def test_n_frames():
with Image.open(static_test_file) as im: with Image.open(static_test_file) as im:
assert im.n_frames == 1 assert im.n_frames == 1

View File

@ -84,17 +84,24 @@ def test_l_mode_transparency():
def test_strategy(): def test_strategy():
with Image.open("Tests/images/iss634.gif") as im:
expected_rgb_always = im.convert("RGB")
with Image.open("Tests/images/chi.gif") as im: with Image.open("Tests/images/chi.gif") as im:
expected_zero = im.convert("RGB") expected_rgb_always_rgba = im.convert("RGBA")
im.seek(1) im.seek(1)
expected_one = im.convert("RGB") expected_different = im.convert("RGB")
try: try:
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
with Image.open("Tests/images/chi.gif") as im: with Image.open("Tests/images/iss634.gif") as im:
assert im.mode == "RGB" assert im.mode == "RGB"
assert_image_equal(im, expected_zero) assert_image_equal(im, expected_rgb_always)
with Image.open("Tests/images/chi.gif") as im:
assert im.mode == "RGBA"
assert_image_equal(im, expected_rgb_always_rgba)
GifImagePlugin.LOADING_STRATEGY = ( GifImagePlugin.LOADING_STRATEGY = (
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
@ -105,7 +112,7 @@ def test_strategy():
im.seek(1) im.seek(1)
assert im.mode == "P" assert im.mode == "P"
assert_image_equal(im.convert("RGB"), expected_one) assert_image_equal(im.convert("RGB"), expected_different)
# Change to RGB mode when a frame has an individual palette # Change to RGB mode when a frame has an individual palette
with Image.open("Tests/images/iss634.gif") as im: with Image.open("Tests/images/iss634.gif") as im:

View File

@ -86,6 +86,18 @@ def test_roundtrip(mode, tmp_path):
assert_image_equal_tofile(im, out) assert_image_equal_tofile(im, out)
def test_small_palette(tmp_path):
im = Image.new("P", (1, 1))
colors = [0, 1, 2]
im.putpalette(colors)
out = str(tmp_path / "temp.im")
im.save(out)
with Image.open(out) as reloaded:
assert reloaded.getpalette() == colors + [0] * 765
def test_save_unsupported_mode(tmp_path): def test_save_unsupported_mode(tmp_path):
out = str(tmp_path / "temp.im") out = str(tmp_path / "temp.im")
im = hopper("HSV") im = hopper("HSV")

19
Tests/test_file_imt.py Normal file
View File

@ -0,0 +1,19 @@
import io
import pytest
from PIL import Image, ImtImagePlugin
from .helper import assert_image_equal_tofile
def test_sanity():
with Image.open("Tests/images/bw_gradient.imt") as im:
assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
@pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n"))
def test_invalid_file(data):
with io.BytesIO(data) as fp:
with pytest.raises(SyntaxError):
ImtImagePlugin.ImtImageFile(fp)

View File

@ -55,9 +55,7 @@ def test_write_exif_metadata():
test_buffer.seek(0) test_buffer.seek(0)
with Image.open(test_buffer) as webp_image: with Image.open(test_buffer) as webp_image:
webp_exif = webp_image.info.get("exif", None) webp_exif = webp_image.info.get("exif", None)
assert webp_exif assert webp_exif == expected_exif[6:], "WebP EXIF didn't match"
if webp_exif:
assert webp_exif == expected_exif, "WebP EXIF didn't match"
def test_read_icc_profile(): def test_read_icc_profile():

View File

@ -35,10 +35,13 @@ def test_toarray():
test_with_dtype(numpy.float64) test_with_dtype(numpy.float64)
test_with_dtype(numpy.uint8) test_with_dtype(numpy.uint8)
if parse_version(numpy.__version__) >= parse_version("1.23"): with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated:
with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: if parse_version(numpy.__version__) >= parse_version("1.23"):
with pytest.raises(OSError): with pytest.raises(OSError):
numpy.array(im_truncated) numpy.array(im_truncated)
else:
with pytest.warns(UserWarning):
numpy.array(im_truncated)
def test_fromarray(): def test_fromarray():

View File

@ -935,7 +935,30 @@ def test_standard_embedded_color(layout_engine):
d = ImageDraw.Draw(im) d = ImageDraw.Draw(im)
d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True) d.text((10, 10), txt, font=ttf, fill="#fa6", embedded_color=True)
assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 6.2) assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 3.1)
@pytest.mark.parametrize("fontmode", ("1", "L", "RGBA"))
def test_float_coord(layout_engine, fontmode):
txt = "Hello World!"
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
im = Image.new("RGB", (300, 64), "white")
d = ImageDraw.Draw(im)
if fontmode == "1":
d.fontmode = "1"
embedded_color = fontmode == "RGBA"
d.text((9.5, 9.5), txt, font=ttf, fill="#fa6", embedded_color=embedded_color)
try:
assert_image_similar_tofile(im, "Tests/images/text_float_coord.png", 3.9)
except AssertionError:
if fontmode == "1" and layout_engine == ImageFont.Layout.BASIC:
assert_image_similar_tofile(
im, "Tests/images/text_float_coord_1_alt.png", 1
)
else:
raise
def test_cbdt(layout_engine): def test_cbdt(layout_engine):

View File

@ -6,10 +6,8 @@ from PIL import Image, ImageMath
def pixel(im): def pixel(im):
if hasattr(im, "im"): if hasattr(im, "im"):
return f"{im.mode} {repr(im.getpixel((0, 0)))}" return f"{im.mode} {repr(im.getpixel((0, 0)))}"
else: elif isinstance(im, int):
if isinstance(im, int): return int(im) # hack to deal with booleans
return int(im) # hack to deal with booleans
print(im)
A = Image.new("L", (1, 1), 1) A = Image.new("L", (1, 1), 1)

View File

@ -60,7 +60,10 @@ Pillow also provides limited support for a few additional modes, including:
* ``BGR;24`` (24-bit reversed true colour) * ``BGR;24`` (24-bit reversed true colour)
* ``BGR;32`` (32-bit reversed true colour) * ``BGR;32`` (32-bit reversed true colour)
However, Pillow doesnt support user-defined modes; if you need to handle band Apart from these additional modes, Pillow doesn't yet support multichannel
images with a depth of more than 8 bits per channel.
Pillow also doesnt support user-defined modes; if you need to handle band
combinations that are not listed above, use a sequence of Image objects. combinations that are not listed above, use a sequence of Image objects.
You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode` You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode`

View File

@ -15,6 +15,7 @@
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
import os
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16 from ._binary import i16le as i16
@ -80,11 +81,19 @@ class FliImageFile(ImageFile.ImageFile):
if i16(s, 4) == 0xF1FA: if i16(s, 4) == 0xF1FA:
# look for palette chunk # look for palette chunk
s = self.fp.read(6) number_of_subchunks = i16(s, 6)
if i16(s, 4) == 11: chunk_size = None
self._palette(palette, 2) for _ in range(number_of_subchunks):
elif i16(s, 4) == 4: if chunk_size is not None:
self._palette(palette, 0) self.fp.seek(chunk_size - 6, os.SEEK_CUR)
s = self.fp.read(6)
chunk_type = i16(s, 4)
if chunk_type in (4, 11):
self._palette(palette, 2 if chunk_type == 11 else 0)
break
chunk_size = i32(s)
if not chunk_size:
break
palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette] palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette]
self.palette = ImagePalette.raw("RGB", b"".join(palette)) self.palette = ImagePalette.raw("RGB", b"".join(palette))

View File

@ -299,11 +299,13 @@ class GifImageFile(ImageFile.ImageFile):
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 or self.global_palette
self._frame_transparency = frame_transparency
if frame == 0: if frame == 0:
if self._frame_palette: if self._frame_palette:
self.mode = ( if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
"RGB" if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS else "P" self.mode = "RGBA" if frame_transparency is not None else "RGB"
) else:
self.mode = "P"
else: else:
self.mode = "L" self.mode = "L"
@ -313,7 +315,6 @@ class GifImageFile(ImageFile.ImageFile):
palette = copy(self.global_palette) palette = copy(self.global_palette)
self.palette = palette self.palette = palette
else: else:
self._frame_transparency = frame_transparency
if self.mode == "P": if self.mode == "P":
if ( if (
LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY LOADING_STRATEGY != LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
@ -386,7 +387,8 @@ class GifImageFile(ImageFile.ImageFile):
transparency = -1 transparency = -1
if frame_transparency is not None: if frame_transparency is not None:
if frame == 0: if frame == 0:
self.info["transparency"] = frame_transparency if LOADING_STRATEGY != LoadingStrategy.RGB_ALWAYS:
self.info["transparency"] = frame_transparency
elif self.mode not in ("RGB", "RGBA"): elif self.mode not in ("RGB", "RGBA"):
transparency = frame_transparency transparency = frame_transparency
self.tile = [ self.tile = [
@ -410,9 +412,9 @@ class GifImageFile(ImageFile.ImageFile):
temp_mode = "P" if self._frame_palette else "L" temp_mode = "P" if self._frame_palette else "L"
self._prev_im = None self._prev_im = None
if self.__frame == 0: if self.__frame == 0:
if "transparency" in self.info: if self._frame_transparency is not None:
self.im = Image.core.fill( self.im = Image.core.fill(
temp_mode, self.size, self.info["transparency"] temp_mode, self.size, self._frame_transparency
) )
elif self.mode in ("RGB", "RGBA"): elif self.mode in ("RGB", "RGBA"):
self._prev_im = self.im self._prev_im = self.im
@ -429,8 +431,12 @@ class GifImageFile(ImageFile.ImageFile):
def load_end(self): def load_end(self):
if self.__frame == 0: if self.__frame == 0:
if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
self.mode = "RGB" if self._frame_transparency is not None:
self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) self.im.putpalettealpha(self._frame_transparency, 0)
self.mode = "RGBA"
else:
self.mode = "RGB"
self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG)
return return
if self.mode == "P" and self._prev_im: if self.mode == "P" and self._prev_im:
if self._frame_transparency is not None: if self._frame_transparency is not None:

View File

@ -352,7 +352,13 @@ def _save(im, fp, filename):
fp.write(b"Lut: 1\r\n") fp.write(b"Lut: 1\r\n")
fp.write(b"\000" * (511 - fp.tell()) + b"\032") fp.write(b"\000" * (511 - fp.tell()) + b"\032")
if im.mode in ["P", "PA"]: if im.mode in ["P", "PA"]:
fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes im_palette = im.im.getpalette("RGB", "RGB;L")
colors = len(im_palette) // 3
palette = b""
for i in range(3):
palette += im_palette[colors * i : colors * (i + 1)]
palette += b"\x00" * (256 - colors)
fp.write(palette) # 768 bytes
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]) ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))])

View File

@ -679,12 +679,24 @@ class Image:
new["shape"] = shape new["shape"] = shape
new["typestr"] = typestr new["typestr"] = typestr
new["version"] = 3 new["version"] = 3
if self.mode == "1": try:
# Binary images need to be extended from bits to bytes if self.mode == "1":
# See: https://github.com/python-pillow/Pillow/issues/350 # Binary images need to be extended from bits to bytes
new["data"] = self.tobytes("raw", "L") # See: https://github.com/python-pillow/Pillow/issues/350
else: new["data"] = self.tobytes("raw", "L")
new["data"] = self.tobytes() else:
new["data"] = self.tobytes()
except Exception as e:
if not isinstance(e, (MemoryError, RecursionError)):
try:
import numpy
from packaging.version import parse as parse_version
except ImportError:
pass
else:
if parse_version(numpy.__version__) < parse_version("1.23"):
warnings.warn(e)
raise
return new return new
def __getstate__(self): def __getstate__(self):

View File

@ -482,8 +482,8 @@ class ImageDraw:
# extract mask and set text alpha # extract mask and set text alpha
color, mask = mask, mask.getband(3) color, mask = mask, mask.getband(3)
color.fillband(3, (ink >> 24) & 0xFF) color.fillband(3, (ink >> 24) & 0xFF)
coord2 = coord[0] + mask.size[0], coord[1] + mask.size[1] x, y = (int(c) for c in coord)
self.im.paste(color, coord + coord2, mask) self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask)
else: else:
self.draw.draw_bitmap(coord, mask, ink) self.draw.draw_bitmap(coord, mask, ink)

View File

@ -136,7 +136,7 @@ class WindowsViewer(Viewer):
"""The default viewer on Windows is the default system application for PNG files.""" """The default viewer on Windows is the default system application for PNG files."""
format = "PNG" format = "PNG"
options = {"compress_level": 1} options = {"compress_level": 1, "save_all": True}
def get_command(self, file, **options): def get_command(self, file, **options):
return ( return (
@ -154,7 +154,7 @@ class MacViewer(Viewer):
"""The default viewer on macOS using ``Preview.app``.""" """The default viewer on macOS using ``Preview.app``."""
format = "PNG" format = "PNG"
options = {"compress_level": 1} options = {"compress_level": 1, "save_all": True}
def get_command(self, file, **options): def get_command(self, file, **options):
# on darwin open returns immediately resulting in the temp # on darwin open returns immediately resulting in the temp
@ -197,7 +197,7 @@ if sys.platform == "darwin":
class UnixViewer(Viewer): class UnixViewer(Viewer):
format = "PNG" format = "PNG"
options = {"compress_level": 1} options = {"compress_level": 1, "save_all": True}
def get_command(self, file, **options): def get_command(self, file, **options):
command = self.get_command_ex(file, **options)[0] command = self.get_command_ex(file, **options)[0]

View File

@ -39,15 +39,19 @@ class ImtImageFile(ImageFile.ImageFile):
# Quick rejection: if there's not a LF among the first # Quick rejection: if there's not a LF among the first
# 100 bytes, this is (probably) not a text header. # 100 bytes, this is (probably) not a text header.
if b"\n" not in self.fp.read(100): buffer = self.fp.read(100)
if b"\n" not in buffer:
raise SyntaxError("not an IM file") raise SyntaxError("not an IM file")
self.fp.seek(0)
xsize = ysize = 0 xsize = ysize = 0
while True: while True:
s = self.fp.read(1) if buffer:
s = buffer[:1]
buffer = buffer[1:]
else:
s = self.fp.read(1)
if not s: if not s:
break break
@ -55,7 +59,12 @@ class ImtImageFile(ImageFile.ImageFile):
# image data begins # image data begins
self.tile = [ self.tile = [
("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1)) (
"raw",
(0, 0) + self.size,
self.fp.tell() - len(buffer),
(self.mode, 0, 1),
)
] ]
break break
@ -63,8 +72,11 @@ class ImtImageFile(ImageFile.ImageFile):
else: else:
# read key/value pair # read key/value pair
# FIXME: dangerous, may read whole file if b"\n" not in buffer:
s = s + self.fp.readline() buffer += self.fp.read(100)
lines = buffer.split(b"\n")
s += lines.pop(0)
buffer = b"\n".join(lines)
if len(s) == 1 or len(s) > 100: if len(s) == 1 or len(s) > 100:
break break
if s[0] == ord(b"*"): if s[0] == ord(b"*"):
@ -74,13 +86,13 @@ class ImtImageFile(ImageFile.ImageFile):
if not m: if not m:
break break
k, v = m.group(1, 2) k, v = m.group(1, 2)
if k == "width": if k == b"width":
xsize = int(v) xsize = int(v)
self._size = xsize, ysize self._size = xsize, ysize
elif k == "height": elif k == b"height":
ysize = int(v) ysize = int(v)
self._size = xsize, ysize self._size = xsize, ysize
elif k == "pixel" and v == "n8": elif k == b"pixel" and v == b"n8":
self.mode = "L" self.mode = "L"

View File

@ -189,7 +189,7 @@ class ChunkStream:
self.close() self.close()
def close(self): def close(self):
self.queue = self.crc = self.fp = None self.queue = self.fp = None
def push(self, cid, pos, length): def push(self, cid, pos, length):
@ -224,7 +224,7 @@ class ChunkStream:
) from e ) from e
def crc_skip(self, cid, data): def crc_skip(self, cid, data):
"""Read checksum. Used if the C module is not present""" """Read checksum"""
self.fp.read(4) self.fp.read(4)

View File

@ -311,9 +311,11 @@ def _save(im, fp, filename):
lossless = im.encoderinfo.get("lossless", False) lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80) quality = im.encoderinfo.get("quality", 80)
icc_profile = im.encoderinfo.get("icc_profile") or "" icc_profile = im.encoderinfo.get("icc_profile") or ""
exif = im.encoderinfo.get("exif", "") exif = im.encoderinfo.get("exif", b"")
if isinstance(exif, Image.Exif): if isinstance(exif, Image.Exif):
exif = exif.tobytes() exif = exif.tobytes()
if exif.startswith(b"Exif\x00\x00"):
exif = exif[6:]
xmp = im.encoderinfo.get("xmp", "") xmp = im.encoderinfo.get("xmp", "")
method = im.encoderinfo.get("method", 4) method = im.encoderinfo.get("method", 4)

View File

@ -138,9 +138,9 @@ deps = {
"bins": ["cjpeg.exe", "djpeg.exe"], "bins": ["cjpeg.exe", "djpeg.exe"],
}, },
"zlib": { "zlib": {
"url": "https://zlib.net/zlib1212.zip", "url": "https://zlib.net/zlib1213.zip",
"filename": "zlib1212.zip", "filename": "zlib1213.zip",
"dir": "zlib-1.2.12", "dir": "zlib-1.2.13",
"license": "README", "license": "README",
"license_pattern": "Copyright notice:\n\n(.+)$", "license_pattern": "Copyright notice:\n\n(.+)$",
"build": [ "build": [