Merge branch 'main' into lab

This commit is contained in:
Andrew Murray 2024-04-26 09:08:43 +10:00 committed by GitHub
commit e932d9e3d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 357 additions and 201 deletions

View File

@ -1 +1 @@
mypy==1.9.0 mypy==1.10.0

View File

@ -47,8 +47,8 @@ jobs:
debian-11-bullseye-amd64, debian-11-bullseye-amd64,
debian-12-bookworm-x86, debian-12-bookworm-x86,
debian-12-bookworm-amd64, debian-12-bookworm-amd64,
fedora-38-amd64,
fedora-39-amd64, fedora-39-amd64,
fedora-40-amd64,
gentoo, gentoo,
ubuntu-20.04-focal-amd64, ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64, ubuntu-22.04-jammy-amd64,

View File

@ -57,9 +57,9 @@ jobs:
- python-version: "3.10" - python-version: "3.10"
PYTHONOPTIMIZE: 2 PYTHONOPTIMIZE: 2
# M1 only available for 3.10+ # M1 only available for 3.10+
- os: "macos-latest" - os: "macos-13"
python-version: "3.9" python-version: "3.9"
- os: "macos-latest" - os: "macos-13"
python-version: "3.8" python-version: "3.8"
exclude: exclude:
- os: "macos-14" - os: "macos-14"

View File

@ -97,7 +97,7 @@ jobs:
matrix: matrix:
include: include:
- name: "macOS x86_64" - name: "macOS x86_64"
os: macos-latest os: macos-13
cibw_arch: x86_64 cibw_arch: x86_64
macosx_deployment_target: "10.10" macosx_deployment_target: "10.10"
- name: "macOS arm64" - name: "macOS arm64"

View File

@ -6,6 +6,10 @@ build:
os: ubuntu-22.04 os: ubuntu-22.04
tools: tools:
python: "3" python: "3"
jobs:
post_checkout:
- git remote add upstream https://github.com/python-pillow/Pillow.git # For forks
- git fetch upstream --tags
python: python:
install: install:

View File

@ -5,6 +5,18 @@ Changelog (Pillow)
10.4.0 (unreleased) 10.4.0 (unreleased)
------------------- -------------------
- Support reading P mode TIFF images with padding #7996
[radarhere]
- Deprecate support for libtiff < 4 #7998
[radarhere, hugovk]
- Corrected ImageShow UnixViewer command #7987
[radarhere]
- Use functools.cached_property in ImageStat #7952
[nulano, hugovk, radarhere]
- Add support for reading BITMAPV2INFOHEADER and BITMAPV3INFOHEADER #7956 - Add support for reading BITMAPV2INFOHEADER and BITMAPV3INFOHEADER #7956
[Cirras, radarhere] [Cirras, radarhere]

View File

@ -29,6 +29,33 @@ elif "GITHUB_ACTIONS" in os.environ:
uploader = "github_actions" uploader = "github_actions"
modes = (
"1",
"L",
"LA",
"La",
"P",
"PA",
"F",
"I",
"I;16",
"I;16L",
"I;16B",
"I;16N",
"RGB",
"RGBA",
"RGBa",
"RGBX",
"BGR;15",
"BGR;16",
"BGR;24",
"CMYK",
"YCbCr",
"HSV",
"LAB",
)
def upload(a: Image.Image, b: Image.Image) -> str | None: def upload(a: Image.Image, b: Image.Image) -> str | None:
if uploader == "show": if uploader == "show":
# local img.show for errors. # local img.show for errors.
@ -273,13 +300,17 @@ def _cached_hopper(mode: str) -> Image.Image:
im = hopper("L") im = hopper("L")
else: else:
im = hopper() im = hopper()
try: if mode.startswith("BGR;"):
im = im.convert(mode) with pytest.warns(DeprecationWarning):
except ImportError: im = im.convert(mode)
if mode == "LAB": else:
im = Image.open("Tests/images/hopper.Lab.tif") try:
else: im = im.convert(mode)
raise except ImportError:
if mode == "LAB":
im = Image.open("Tests/images/hopper.Lab.tif")
else:
raise
return im return im

View File

@ -37,6 +37,8 @@ def test_version() -> None:
else: else:
assert function(name) == version assert function(name) == version
if name != "PIL": if name != "PIL":
if name == "zlib" and version is not None:
version = version.replace(".zlib-ng", "")
assert version is None or re.search(r"\d+(\.\d+)*$", version) assert version is None or re.search(r"\d+(\.\d+)*$", version)
for module in features.modules: for module in features.modules:

View File

@ -242,7 +242,24 @@ class TestFileLibTiff(LibTiffTestCase):
im.save(out, tiffinfo=new_ifd) im.save(out, tiffinfo=new_ifd)
def test_custom_metadata(self, tmp_path: Path) -> None: @pytest.mark.parametrize(
"libtiff",
(
pytest.param(
True,
marks=pytest.mark.skipif(
not getattr(Image.core, "libtiff_support_custom_tags", False),
reason="Custom tags not supported by older libtiff",
),
),
False,
),
)
def test_custom_metadata(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
class Tc(NamedTuple): class Tc(NamedTuple):
value: Any value: Any
type: int type: int
@ -281,53 +298,43 @@ class TestFileLibTiff(LibTiffTestCase):
) )
} }
libtiffs = [False] def check_tags(
if Image.core.libtiff_support_custom_tags: tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
libtiffs.append(True) ) -> None:
im = hopper()
for libtiff in libtiffs: out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = libtiff im.save(out, tiffinfo=tiffinfo)
def check_tags( with Image.open(out) as reloaded:
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] for tag, value in tiffinfo.items():
) -> None: reloaded_value = reloaded.tag_v2[tag]
im = hopper() if (
isinstance(reloaded_value, TiffImagePlugin.IFDRational)
and libtiff
):
# libtiff does not support real RATIONALS
assert round(abs(float(reloaded_value) - float(value)), 7) == 0
continue
out = str(tmp_path / "temp.tif") assert reloaded_value == value
im.save(out, tiffinfo=tiffinfo)
with Image.open(out) as reloaded: # Test with types
for tag, value in tiffinfo.items(): ifd = TiffImagePlugin.ImageFileDirectory_v2()
reloaded_value = reloaded.tag_v2[tag] for tag, tagdata in custom.items():
if ( ifd[tag] = tagdata.value
isinstance(reloaded_value, TiffImagePlugin.IFDRational) ifd.tagtype[tag] = tagdata.type
and libtiff check_tags(ifd)
):
# libtiff does not support real RATIONALS
assert (
round(abs(float(reloaded_value) - float(value)), 7) == 0
)
continue
assert reloaded_value == value # Test without types. This only works for some types, int for example are
# always encoded as LONG and not SIGNED_LONG.
# Test with types check_tags(
ifd = TiffImagePlugin.ImageFileDirectory_v2() {
for tag, tagdata in custom.items(): tag: tagdata.value
ifd[tag] = tagdata.value for tag, tagdata in custom.items()
ifd.tagtype[tag] = tagdata.type if tagdata.supported_by_default
check_tags(ifd) }
)
# Test without types. This only works for some types, int for example are
# always encoded as LONG and not SIGNED_LONG.
check_tags(
{
tag: tagdata.value
for tag, tagdata in custom.items()
if tagdata.supported_by_default
}
)
TiffImagePlugin.WRITE_LIBTIFF = False
def test_osubfiletype(self, tmp_path: Path) -> None: def test_osubfiletype(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
@ -741,7 +748,7 @@ class TestFileLibTiff(LibTiffTestCase):
pytest.param( pytest.param(
True, True,
marks=pytest.mark.skipif( marks=pytest.mark.skipif(
not Image.core.libtiff_support_custom_tags, not getattr(Image.core, "libtiff_support_custom_tags", False),
reason="Custom tags not supported by older libtiff", reason="Custom tags not supported by older libtiff",
), ),
), ),

View File

@ -85,7 +85,9 @@ class TestFilePng:
def test_sanity(self, tmp_path: Path) -> None: def test_sanity(self, tmp_path: Path) -> None:
# internal version number # internal version number
assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib")) assert re.search(
r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", features.version_codec("zlib")
)
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")

View File

@ -28,45 +28,27 @@ from .helper import (
assert_image_similar_tofile, assert_image_similar_tofile,
assert_not_all_same, assert_not_all_same,
hopper, hopper,
is_big_endian,
is_win32, is_win32,
mark_if_feature_version, mark_if_feature_version,
modes,
skip_unless_feature, skip_unless_feature,
) )
# name, pixel size
image_modes = (
("1", 1),
("L", 1),
("LA", 4),
("La", 4),
("P", 1),
("PA", 4),
("F", 4),
("I", 4),
("I;16", 2),
("I;16L", 2),
("I;16B", 2),
("I;16N", 2),
("RGB", 4),
("RGBA", 4),
("RGBa", 4),
("RGBX", 4),
("BGR;15", 2),
("BGR;16", 2),
("BGR;24", 3),
("CMYK", 4),
("YCbCr", 4),
("HSV", 4),
("LAB", 4),
)
image_mode_names = [name for name, _ in image_modes] # Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning):
return Image.new(mode, size)
else:
return Image.new(mode, size)
class TestImage: class TestImage:
@pytest.mark.parametrize("mode", image_mode_names) @pytest.mark.parametrize("mode", modes)
def test_image_modes_success(self, mode: str) -> None: def test_image_modes_success(self, mode: str) -> None:
Image.new(mode, (1, 1)) helper_image_new(mode, (1, 1))
@pytest.mark.parametrize("mode", ("", "bad", "very very long")) @pytest.mark.parametrize("mode", ("", "bad", "very very long"))
def test_image_modes_fail(self, mode: str) -> None: def test_image_modes_fail(self, mode: str) -> None:
@ -1045,30 +1027,33 @@ class TestImage:
class TestImageBytes: class TestImageBytes:
@pytest.mark.parametrize("mode", image_mode_names) @pytest.mark.parametrize("mode", modes)
def test_roundtrip_bytes_constructor(self, mode: str) -> None: def test_roundtrip_bytes_constructor(self, mode: str) -> None:
im = hopper(mode) im = hopper(mode)
source_bytes = im.tobytes() source_bytes = im.tobytes()
reloaded = Image.frombytes(mode, im.size, source_bytes) if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning):
reloaded = Image.frombytes(mode, im.size, source_bytes)
else:
reloaded = Image.frombytes(mode, im.size, source_bytes)
assert reloaded.tobytes() == source_bytes assert reloaded.tobytes() == source_bytes
@pytest.mark.parametrize("mode", image_mode_names) @pytest.mark.parametrize("mode", modes)
def test_roundtrip_bytes_method(self, mode: str) -> None: def test_roundtrip_bytes_method(self, mode: str) -> None:
im = hopper(mode) im = hopper(mode)
source_bytes = im.tobytes() source_bytes = im.tobytes()
reloaded = Image.new(mode, im.size) reloaded = helper_image_new(mode, im.size)
reloaded.frombytes(source_bytes) reloaded.frombytes(source_bytes)
assert reloaded.tobytes() == source_bytes assert reloaded.tobytes() == source_bytes
@pytest.mark.parametrize(("mode", "pixelsize"), image_modes) @pytest.mark.parametrize("mode", modes)
def test_getdata_putdata(self, mode: str, pixelsize: int) -> None: def test_getdata_putdata(self, mode: str) -> None:
im = Image.new(mode, (2, 2)) if is_big_endian and mode == "BGR;15":
source_bytes = bytes(range(im.width * im.height * pixelsize)) pytest.xfail("Known failure of BGR;15 on big-endian")
im.frombytes(source_bytes) im = hopper(mode)
reloaded = helper_image_new(mode, im.size)
reloaded = Image.new(mode, im.size)
reloaded.putdata(im.getdata()) reloaded.putdata(im.getdata())
assert_image_equal(im, reloaded) assert_image_equal(im, reloaded)

View File

@ -10,7 +10,7 @@ import pytest
from PIL import Image from PIL import Image
from .helper import assert_image_equal, hopper, is_win32 from .helper import assert_image_equal, hopper, is_win32, modes
# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2
# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670
@ -33,7 +33,7 @@ except ImportError:
class AccessTest: class AccessTest:
# initial value # Initial value
_init_cffi_access = Image.USE_CFFI_ACCESS _init_cffi_access = Image.USE_CFFI_ACCESS
_need_cffi_access = False _need_cffi_access = False
@ -138,8 +138,8 @@ class TestImageGetPixel(AccessTest):
if bands == 1: if bands == 1:
return 1 return 1
if mode in ("BGR;15", "BGR;16"): if mode in ("BGR;15", "BGR;16"):
# These modes have less than 8 bits per band # These modes have less than 8 bits per band,
# So (1, 2, 3) cannot be roundtripped # so (1, 2, 3) cannot be roundtripped.
return (16, 32, 49) return (16, 32, 49)
return tuple(range(1, bands + 1)) return tuple(range(1, bands + 1))
@ -151,7 +151,7 @@ class TestImageGetPixel(AccessTest):
self.color(mode) if expected_color_int is None else expected_color_int self.color(mode) if expected_color_int is None else expected_color_int
) )
# check putpixel # Check putpixel
im = Image.new(mode, (1, 1), None) im = Image.new(mode, (1, 1), None)
im.putpixel((0, 0), expected_color) im.putpixel((0, 0), expected_color)
actual_color = im.getpixel((0, 0)) actual_color = im.getpixel((0, 0))
@ -160,7 +160,7 @@ class TestImageGetPixel(AccessTest):
f"expected {expected_color} got {actual_color}" f"expected {expected_color} got {actual_color}"
) )
# check putpixel negative index # Check putpixel negative index
im.putpixel((-1, -1), expected_color) im.putpixel((-1, -1), expected_color)
actual_color = im.getpixel((-1, -1)) actual_color = im.getpixel((-1, -1))
assert actual_color == expected_color, ( assert actual_color == expected_color, (
@ -168,22 +168,21 @@ class TestImageGetPixel(AccessTest):
f"expected {expected_color} got {actual_color}" f"expected {expected_color} got {actual_color}"
) )
# Check 0 # Check 0x0 image with None initial color
im = Image.new(mode, (0, 0), None) im = Image.new(mode, (0, 0), None)
assert im.load() is not None assert im.load() is not None
error = ValueError if self._need_cffi_access else IndexError error = ValueError if self._need_cffi_access else IndexError
with pytest.raises(error): with pytest.raises(error):
im.putpixel((0, 0), expected_color) im.putpixel((0, 0), expected_color)
with pytest.raises(error): with pytest.raises(error):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check 0 negative index # Check negative index
with pytest.raises(error): with pytest.raises(error):
im.putpixel((-1, -1), expected_color) im.putpixel((-1, -1), expected_color)
with pytest.raises(error): with pytest.raises(error):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
# check initial color # Check initial color
im = Image.new(mode, (1, 1), expected_color) im = Image.new(mode, (1, 1), expected_color)
actual_color = im.getpixel((0, 0)) actual_color = im.getpixel((0, 0))
assert actual_color == expected_color, ( assert actual_color == expected_color, (
@ -191,45 +190,28 @@ class TestImageGetPixel(AccessTest):
f"expected {expected_color} got {actual_color}" f"expected {expected_color} got {actual_color}"
) )
# check initial color negative index # Check initial color negative index
actual_color = im.getpixel((-1, -1)) actual_color = im.getpixel((-1, -1))
assert actual_color == expected_color, ( assert actual_color == expected_color, (
f"initial color failed with negative index for mode {mode}, " f"initial color failed with negative index for mode {mode}, "
f"expected {expected_color} got {actual_color}" f"expected {expected_color} got {actual_color}"
) )
# Check 0 # Check 0x0 image with initial color
im = Image.new(mode, (0, 0), expected_color) im = Image.new(mode, (0, 0), expected_color)
with pytest.raises(error): with pytest.raises(error):
im.getpixel((0, 0)) im.getpixel((0, 0))
# Check 0 negative index # Check negative index
with pytest.raises(error): with pytest.raises(error):
im.getpixel((-1, -1)) im.getpixel((-1, -1))
@pytest.mark.parametrize( @pytest.mark.parametrize("mode", modes)
"mode",
(
"1",
"L",
"LA",
"I",
"I;16",
"I;16B",
"F",
"P",
"PA",
"BGR;15",
"BGR;16",
"BGR;24",
"RGB",
"RGBA",
"RGBX",
"CMYK",
"YCbCr",
),
)
def test_basic(self, mode: str) -> None: def test_basic(self, mode: str) -> None:
self.check(mode) if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning):
self.check(mode)
else:
self.check(mode)
def test_list(self) -> None: def test_list(self) -> None:
im = hopper() im = hopper()
@ -238,7 +220,7 @@ class TestImageGetPixel(AccessTest):
@pytest.mark.parametrize("mode", ("I;16", "I;16B")) @pytest.mark.parametrize("mode", ("I;16", "I;16B"))
@pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1))
def test_signedness(self, mode: str, expected_color: int) -> None: def test_signedness(self, mode: str, expected_color: int) -> None:
# see https://github.com/python-pillow/Pillow/issues/452 # See https://github.com/python-pillow/Pillow/issues/452
# pixelaccess is using signed int* instead of uint* # pixelaccess is using signed int* instead of uint*
self.check(mode, expected_color) self.check(mode, expected_color)
@ -298,13 +280,6 @@ class TestCffi(AccessTest):
im = Image.new(mode, (10, 10), 40000) im = Image.new(mode, (10, 10), 40000)
self._test_get_access(im) self._test_get_access(im)
# These don't actually appear to be modes that I can actually make,
# as unpack sets them directly into the I mode.
# im = Image.new('I;32L', (10, 10), -2**10)
# self._test_get_access(im)
# im = Image.new('I;32B', (10, 10), 2**10)
# self._test_get_access(im)
def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None: def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None:
"""Are we writing the correct bits into the image? """Are we writing the correct bits into the image?
@ -336,23 +311,18 @@ class TestCffi(AccessTest):
self._test_set_access(hopper("LA"), (128, 128)) self._test_set_access(hopper("LA"), (128, 128))
self._test_set_access(hopper("1"), 255) self._test_set_access(hopper("1"), 255)
self._test_set_access(hopper("P"), 128) self._test_set_access(hopper("P"), 128)
# self._test_set_access(i, (128, 128)) #PA -- undone how to make self._test_set_access(hopper("PA"), (128, 128))
self._test_set_access(hopper("F"), 1024.0) self._test_set_access(hopper("F"), 1024.0)
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
im = Image.new(mode, (10, 10), 40000) im = Image.new(mode, (10, 10), 40000)
self._test_set_access(im, 45000) self._test_set_access(im, 45000)
# im = Image.new('I;32L', (10, 10), -(2**10))
# self._test_set_access(im, -(2**13)+1)
# im = Image.new('I;32B', (10, 10), 2**10)
# self._test_set_access(im, 2**13-1)
@pytest.mark.filterwarnings("ignore::DeprecationWarning") @pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_not_implemented(self) -> None: def test_not_implemented(self) -> None:
assert PyAccess.new(hopper("BGR;15")) is None assert PyAccess.new(hopper("BGR;15")) is None
# ref https://github.com/python-pillow/Pillow/pull/2009 # Ref https://github.com/python-pillow/Pillow/pull/2009
def test_reference_counting(self) -> None: def test_reference_counting(self) -> None:
size = 10 size = 10
@ -361,7 +331,7 @@ class TestCffi(AccessTest):
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
px = Image.new("L", (size, 1), 0).load() px = Image.new("L", (size, 1), 0).load()
for i in range(size): for i in range(size):
# pixels can contain garbage if image is released # Pixels can contain garbage if image is released
assert px[i, 0] == 0 assert px[i, 0] == 0
@pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("mode", ("P", "PA"))
@ -478,7 +448,7 @@ int main(int argc, char* argv[])
env = os.environ.copy() env = os.environ.copy()
env["PATH"] = sys.prefix + ";" + env["PATH"] env["PATH"] = sys.prefix + ";" + env["PATH"]
# do not display the Windows Error Reporting dialog # Do not display the Windows Error Reporting dialog
getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002)
process = subprocess.Popen(["embed_pil.exe"], env=env) process = subprocess.Popen(["embed_pil.exe"], env=env)

View File

@ -81,7 +81,8 @@ def test_mode_F() -> None:
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
def test_mode_BGR(mode: str) -> None: def test_mode_BGR(mode: str) -> None:
data = [(16, 32, 49), (32, 32, 98)] data = [(16, 32, 49), (32, 32, 98)]
im = Image.new(mode, (1, 2)) with pytest.warns(DeprecationWarning):
im = Image.new(mode, (1, 2))
im.putdata(data) im.putdata(data)
assert list(im.getdata()) == data assert list(im.getdata()) == data

View File

@ -216,7 +216,10 @@ class TestLibPack:
) )
def test_I16(self) -> None: def test_I16(self) -> None:
self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) if sys.byteorder == "little":
self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605)
else:
self.assert_pack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506)
def test_F_float(self) -> None: def test_F_float(self) -> None:
self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34)
@ -359,11 +362,14 @@ class TestLibUnpack:
) )
def test_BGR(self) -> None: def test_BGR(self) -> None:
self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)) with pytest.warns(DeprecationWarning):
self.assert_unpack( self.assert_unpack(
"BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) "BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)
) )
self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_unpack(
"BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0)
)
self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
def test_RGBA(self) -> None: def test_RGBA(self) -> None:
self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))

View File

@ -28,6 +28,7 @@ needs_sphinx = "7.3"
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
"dater",
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
"sphinx.ext.extlinks", "sphinx.ext.extlinks",
"sphinx.ext.intersphinx", "sphinx.ext.intersphinx",

48
docs/dater.py Normal file
View File

@ -0,0 +1,48 @@
"""
Sphinx extension to add timestamps to release notes based on Git versions.
Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs.
"""
from __future__ import annotations
import re
import subprocess
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from sphinx.application import Sphinx
DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+")
VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n")
def get_date_for(git_version: str) -> str | None:
cmd = ["git", "log", "-1", "--format=%ai", git_version]
try:
out = subprocess.check_output(
cmd, stderr=subprocess.DEVNULL, text=True, encoding="utf-8"
)
except subprocess.CalledProcessError:
return None
return out.split()[0]
def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None:
if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])):
old_title = m.group(1)
if tag_date := get_date_for(old_title):
new_title = f"{old_title} ({tag_date})"
else:
new_title = f"{old_title} (unreleased)"
new_underline = "-" * len(new_title)
result = source[0].replace(m.group(0), f"{new_title}\n{new_underline}\n", 1)
source[0] = result
def setup(app: Sphinx) -> dict[str, bool]:
app.connect("source-read", add_date)
return {"parallel_read_safe": True}

View File

@ -100,6 +100,21 @@ ImageMath eval()
``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or ``ImageMath.eval()`` has been deprecated. Use :py:meth:`~PIL.ImageMath.lambda_eval` or
:py:meth:`~PIL.ImageMath.unsafe_eval` instead. :py:meth:`~PIL.ImageMath.unsafe_eval` instead.
BGR;15, BGR 16 and BGR;24
^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.4.0
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated.
Support for LibTIFF earlier than 4
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.4.0
Support for LibTIFF earlier than version 4 has been deprecated.
Upgrade to a newer version of LibTIFF instead.
Removed features Removed features
---------------- ----------------

View File

@ -59,9 +59,6 @@ Pillow also provides limited support for a few additional modes, including:
* ``I;16L`` (16-bit little endian unsigned integer pixels) * ``I;16L`` (16-bit little endian unsigned integer pixels)
* ``I;16B`` (16-bit big endian unsigned integer pixels) * ``I;16B`` (16-bit big endian unsigned integer pixels)
* ``I;16N`` (16-bit native endian unsigned integer pixels) * ``I;16N`` (16-bit native endian unsigned integer pixels)
* ``BGR;15`` (15-bit reversed true colour)
* ``BGR;16`` (16-bit reversed true colour)
* ``BGR;24`` (24-bit reversed true colour)
Premultiplied alpha is where the values for each other channel have been Premultiplied alpha is where the values for each other channel have been
multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)``

View File

@ -31,10 +31,10 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 12 Bookworm | 3.11 | x86, x86-64 | | Debian 12 Bookworm | 3.11 | x86, x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 38 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 39 | 3.12 | x86-64 | | Fedora 39 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 40 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.9 | x86-64 | | Gentoo | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| macOS 12 Monterey | 3.8, 3.9 | x86-64 | | macOS 12 Monterey | 3.8, 3.9 | x86-64 |

View File

@ -0,0 +1,59 @@
10.4.0
------
Security
========
TODO
^^^^
TODO
:cve:`YYYY-XXXXX`: TODO
^^^^^^^^^^^^^^^^^^^^^^^
TODO
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations
============
BGR;15, BGR 16 and BGR;24
^^^^^^^^^^^^^^^^^^^^^^^^^
The experimental BGR;15, BGR;16 and BGR;24 modes have been deprecated.
Support for LibTIFF earlier than 4
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Support for LibTIFF earlier than version 4 has been deprecated.
Upgrade to a newer version of LibTIFF instead.
API Changes
===========
TODO
^^^^
TODO
API Additions
=============
TODO
^^^^
TODO
Other Changes
=============
TODO
^^^^
TODO

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
10.4.0
10.3.0 10.3.0
10.2.0 10.2.0
10.1.0 10.1.0

View File

@ -55,6 +55,7 @@ from . import (
_plugins, _plugins,
) )
from ._binary import i32le, o32be, o32le from ._binary import i32le, o32be, o32le
from ._deprecate import deprecate
from ._typing import StrOrBytesPath, TypeGuard from ._typing import StrOrBytesPath, TypeGuard
from ._util import DeferredError, is_path from ._util import DeferredError, is_path
@ -939,6 +940,9 @@ class Image:
:returns: An :py:class:`~PIL.Image.Image` object. :returns: An :py:class:`~PIL.Image.Image` object.
""" """
if mode in ("BGR;15", "BGR;16", "BGR;24"):
deprecate(mode, 12)
self.load() self.load()
has_transparency = "transparency" in self.info has_transparency = "transparency" in self.info
@ -2956,6 +2960,9 @@ def new(
:returns: An :py:class:`~PIL.Image.Image` object. :returns: An :py:class:`~PIL.Image.Image` object.
""" """
if mode in ("BGR;15", "BGR;16", "BGR;24"):
deprecate(mode, 12)
_check_size(size) _check_size(size)
if color is None: if color is None:

View File

@ -838,8 +838,8 @@ def getProfileName(profile: _CmsProfileCompatible) -> str:
if not (model or manufacturer): if not (model or manufacturer):
return (profile.profile.profile_description or "") + "\n" return (profile.profile.profile_description or "") + "\n"
if not manufacturer or len(model) > 30: # type: ignore[arg-type] if not manufacturer or (model and len(model) > 30):
return model + "\n" # type: ignore[operator] return f"{model}\n"
return f"{model} - {manufacturer}\n" return f"{model} - {manufacturer}\n"
except (AttributeError, OSError, TypeError, ValueError) as v: except (AttributeError, OSError, TypeError, ValueError) as v:

View File

@ -18,6 +18,8 @@ import sys
from functools import lru_cache from functools import lru_cache
from typing import NamedTuple from typing import NamedTuple
from ._deprecate import deprecate
class ModeDescriptor(NamedTuple): class ModeDescriptor(NamedTuple):
"""Wrapper for mode strings.""" """Wrapper for mode strings."""
@ -63,6 +65,8 @@ def getmode(mode: str) -> ModeDescriptor:
"PA": ("RGB", "L", ("P", "A"), "|u1"), "PA": ("RGB", "L", ("P", "A"), "|u1"),
} }
if mode in modes: if mode in modes:
if mode in ("BGR;15", "BGR;16", "BGR;24"):
deprecate(mode, 12)
base_mode, base_type, bands, type_str = modes[mode] base_mode, base_type, bands, type_str = modes[mode]
return ModeDescriptor(mode, bands, base_mode, base_type, type_str) return ModeDescriptor(mode, bands, base_mode, base_type, type_str)

View File

@ -56,6 +56,7 @@ from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._deprecate import deprecate
from .TiffTags import TYPES from .TiffTags import TYPES
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -244,6 +245,7 @@ OPEN_INFO = {
(MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
(II, 3, (1,), 1, (8,), ()): ("P", "P"), (II, 3, (1,), 1, (8,), ()): ("P", "P"),
(MM, 3, (1,), 1, (8,), ()): ("P", "P"), (MM, 3, (1,), 1, (8,), ()): ("P", "P"),
(II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"),
(II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
(MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
(II, 3, (1,), 2, (8,), ()): ("P", "P;R"), (II, 3, (1,), 2, (8,), ()): ("P", "P;R"),
@ -276,6 +278,9 @@ PREFIXES = [
b"II\x2B\x00", # BigTIFF with little-endian byte order b"II\x2B\x00", # BigTIFF with little-endian byte order
] ]
if not getattr(Image.core, "libtiff_support_custom_tags", True):
deprecate("Support for LibTIFF earlier than version 4", 12)
def _accept(prefix: bytes) -> bool: def _accept(prefix: bytes) -> bool:
return prefix[:4] in PREFIXES return prefix[:4] in PREFIXES

View File

@ -3737,7 +3737,7 @@ _getattr_unsafe_ptrs(ImagingObject *self, void *closure) {
self->image->image32, self->image->image32,
"image", "image",
self->image->image); self->image->image);
}; }
static struct PyGetSetDef getsetters[] = { static struct PyGetSetDef getsetters[] = {
{"mode", (getter)_getattr_mode}, {"mode", (getter)_getattr_mode},

View File

@ -213,34 +213,37 @@ cms_transform_dealloc(CmsTransformObject *self) {
static cmsUInt32Number static cmsUInt32Number
findLCMStype(char *PILmode) { findLCMStype(char *PILmode) {
if (strcmp(PILmode, "RGB") == 0) { if (
strcmp(PILmode, "RGB") == 0 ||
strcmp(PILmode, "RGBA") == 0 ||
strcmp(PILmode, "RGBX") == 0
) {
return TYPE_RGBA_8; return TYPE_RGBA_8;
} else if (strcmp(PILmode, "RGBA") == 0) { }
return TYPE_RGBA_8; if (strcmp(PILmode, "RGBA;16B") == 0) {
} else if (strcmp(PILmode, "RGBX") == 0) {
return TYPE_RGBA_8;
} else if (strcmp(PILmode, "RGBA;16B") == 0) {
return TYPE_RGBA_16; return TYPE_RGBA_16;
} else if (strcmp(PILmode, "CMYK") == 0) { }
if (strcmp(PILmode, "CMYK") == 0) {
return TYPE_CMYK_8; return TYPE_CMYK_8;
} else if (strcmp(PILmode, "L") == 0) { }
return TYPE_GRAY_8; if (strcmp(PILmode, "L;16") == 0) {
} else if (strcmp(PILmode, "L;16") == 0) {
return TYPE_GRAY_16; return TYPE_GRAY_16;
} else if (strcmp(PILmode, "L;16B") == 0) { }
if (strcmp(PILmode, "L;16B") == 0) {
return TYPE_GRAY_16_SE; return TYPE_GRAY_16_SE;
} else if (strcmp(PILmode, "YCCA") == 0) { }
if (
strcmp(PILmode, "YCCA") == 0 ||
strcmp(PILmode, "YCC") == 0
) {
return TYPE_YCbCr_8; return TYPE_YCbCr_8;
} else if (strcmp(PILmode, "YCC") == 0) { }
return TYPE_YCbCr_8; if (strcmp(PILmode, "LAB") == 0) {
} else if (strcmp(PILmode, "LAB") == 0) {
// LabX equivalent like ALab, but not reversed -- no #define in lcms2 // LabX equivalent like ALab, but not reversed -- no #define in lcms2
return (COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1)); return (COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1));
} }
else { /* presume "L" by default */
/* take a wild guess... */ return TYPE_GRAY_8;
return TYPE_GRAY_8;
}
} }
#define Cms_Min(a, b) ((a) < (b) ? (a) : (b)) #define Cms_Min(a, b) ((a) < (b) ? (a) : (b))

View File

@ -427,7 +427,6 @@ error:
PyObject * PyObject *
PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) {
int clip;
HANDLE handle = NULL; HANDLE handle = NULL;
int size; int size;
void *data; void *data;

View File

@ -81,12 +81,6 @@ get_pixel_16B(Imaging im, int x, int y, void *color) {
#endif #endif
} }
static void
get_pixel_16(Imaging im, int x, int y, void *color) {
UINT8 *in = (UINT8 *)&im->image[y][x + x];
memcpy(color, in, sizeof(UINT16));
}
static void static void
get_pixel_BGR15(Imaging im, int x, int y, void *color) { get_pixel_BGR15(Imaging im, int x, int y, void *color) {
UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; UINT8 *in = (UINT8 *)&im->image8[y][x * 2];
@ -207,7 +201,11 @@ ImagingAccessInit() {
ADD("I;16", get_pixel_16L, put_pixel_16L); ADD("I;16", get_pixel_16L, put_pixel_16L);
ADD("I;16L", get_pixel_16L, put_pixel_16L); ADD("I;16L", get_pixel_16L, put_pixel_16L);
ADD("I;16B", get_pixel_16B, put_pixel_16B); ADD("I;16B", get_pixel_16B, put_pixel_16B);
ADD("I;16N", get_pixel_16, put_pixel_16L); #ifdef WORDS_BIGENDIAN
ADD("I;16N", get_pixel_16B, put_pixel_16B);
#else
ADD("I;16N", get_pixel_16L, put_pixel_16L);
#endif
ADD("I;32L", get_pixel_32L, put_pixel_32L); ADD("I;32L", get_pixel_32L, put_pixel_32L);
ADD("I;32B", get_pixel_32B, put_pixel_32B); ADD("I;32B", get_pixel_32B, put_pixel_32B);
ADD("F", get_pixel_32, put_pixel_32); ADD("F", get_pixel_32, put_pixel_32);

View File

@ -254,9 +254,8 @@ static void
rgb2i16l(UINT8 *out_, const UINT8 *in, int xsize) { rgb2i16l(UINT8 *out_, const UINT8 *in, int xsize) {
int x; int x;
for (x = 0; x < xsize; x++, in += 4) { for (x = 0; x < xsize; x++, in += 4) {
UINT8 v = CLIP16(L24(in) >> 16); *out_++ = L24(in) >> 16;
*out_++ = v; *out_++ = 0;
*out_++ = v >> 8;
} }
} }
@ -264,9 +263,8 @@ static void
rgb2i16b(UINT8 *out_, const UINT8 *in, int xsize) { rgb2i16b(UINT8 *out_, const UINT8 *in, int xsize) {
int x; int x;
for (x = 0; x < xsize; x++, in += 4) { for (x = 0; x < xsize; x++, in += 4) {
UINT8 v = CLIP16(L24(in) >> 16); *out_++ = 0;
*out_++ = v >> 8; *out_++ = L24(in) >> 16;
*out_++ = v;
} }
} }

View File

@ -24,11 +24,11 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) {
ImagingSectionCookie cookie; ImagingSectionCookie cookie;
/* Assume there's enough data in the buffer */ /* Assume there's enough data in the buffer */
if (!im) { if (!im || im->bands != 3) {
return (Imaging)ImagingError_ModeError(); return (Imaging)ImagingError_ModeError();
} }
if (strcmp(mode, "L") == 0 && im->bands == 3) { if (strcmp(mode, "L") == 0) {
imOut = ImagingNewDirty("L", im->xsize, im->ysize); imOut = ImagingNewDirty("L", im->xsize, im->ysize);
if (!imOut) { if (!imOut) {
return NULL; return NULL;
@ -47,7 +47,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) {
} }
ImagingSectionLeave(&cookie); ImagingSectionLeave(&cookie);
} else if (strlen(mode) == 3 && im->bands == 3) { } else if (strlen(mode) == 3) {
imOut = ImagingNewDirty(mode, im->xsize, im->ysize); imOut = ImagingNewDirty(mode, im->xsize, im->ysize);
if (!imOut) { if (!imOut) {
return NULL; return NULL;

View File

@ -1582,6 +1582,7 @@ static struct {
{"P", "P", 8, copy1}, {"P", "P", 8, copy1},
{"P", "P;R", 8, unpackLR}, {"P", "P;R", 8, unpackLR},
{"P", "L", 8, copy1}, {"P", "L", 8, copy1},
{"P", "PX", 16, unpackL16B},
/* palette w. alpha */ /* palette w. alpha */
{"PA", "PA", 16, unpackLA}, {"PA", "PA", 16, unpackLA},