Merge branch 'main' into accept

This commit is contained in:
Andrew Murray 2024-05-08 18:45:10 +10:00 committed by GitHub
commit c6a3f0fdd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 441 additions and 262 deletions

View File

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

View File

@ -55,6 +55,7 @@ jobs:
packages: > packages: >
gcc-g++ gcc-g++
ghostscript ghostscript
git
ImageMagick ImageMagick
jpeg jpeg
libfreetype-devel libfreetype-devel
@ -132,11 +133,12 @@ jobs:
bash.exe .ci/after_success.sh bash.exe .ci/after_success.sh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3.1.5 uses: codecov/codecov-action@v4
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Cygwin flags: GHA_Cygwin
name: Cygwin Python 3.${{ matrix.python-minor-version }} name: Cygwin Python 3.${{ matrix.python-minor-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success: success:
permissions: permissions:

View File

@ -36,8 +36,8 @@ jobs:
docker: [ docker: [
# Run slower jobs first to give them a headstart and reduce waiting time # Run slower jobs first to give them a headstart and reduce waiting time
ubuntu-22.04-jammy-arm64v8, ubuntu-22.04-jammy-arm64v8,
ubuntu-22.04-jammy-ppc64le, ubuntu-24.04-noble-ppc64le,
ubuntu-22.04-jammy-s390x, ubuntu-24.04-noble-s390x,
# Then run the remainder # Then run the remainder
alpine, alpine,
amazon-2-amd64, amazon-2-amd64,
@ -47,19 +47,20 @@ 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,
ubuntu-24.04-noble-amd64,
] ]
dockerTag: [main] dockerTag: [main]
include: include:
- docker: "ubuntu-22.04-jammy-arm64v8" - docker: "ubuntu-22.04-jammy-arm64v8"
qemu-arch: "aarch64" qemu-arch: "aarch64"
- docker: "ubuntu-22.04-jammy-ppc64le" - docker: "ubuntu-24.04-noble-ppc64le"
qemu-arch: "ppc64le" qemu-arch: "ppc64le"
- docker: "ubuntu-22.04-jammy-s390x" - docker: "ubuntu-24.04-noble-s390x"
qemu-arch: "s390x" qemu-arch: "s390x"
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
@ -81,8 +82,8 @@ jobs:
- name: Docker build - name: Docker build
run: | run: |
# The Pillow user in the docker container is UID 1000 # The Pillow user in the docker container is UID 1001
sudo chown -R 1000 $GITHUB_WORKSPACE sudo chown -R 1001 $GITHUB_WORKSPACE
docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
sudo chown -R runner $GITHUB_WORKSPACE sudo chown -R runner $GITHUB_WORKSPACE
@ -99,11 +100,12 @@ jobs:
MATRIX_DOCKER: ${{ matrix.docker }} MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3.1.5 uses: codecov/codecov-action@v4
with: with:
flags: GHA_Docker flags: GHA_Docker
name: ${{ matrix.docker }} name: ${{ matrix.docker }}
gcov: true gcov: true
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success: success:
permissions: permissions:

View File

@ -85,8 +85,9 @@ jobs:
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3.1.5 uses: codecov/codecov-action@v4
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows
name: "MSYS2 MinGW" name: "MSYS2 MinGW"
token: ${{ secrets.CODECOV_ORG_TOKEN }}

View File

@ -50,7 +50,7 @@ jobs:
- name: Build and Run Valgrind - name: Build and Run Valgrind
run: | run: |
# The Pillow user in the docker container is UID 1000 # The Pillow user in the docker container is UID 1001
sudo chown -R 1000 $GITHUB_WORKSPACE sudo chown -R 1001 $GITHUB_WORKSPACE
docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }}
sudo chown -R runner $GITHUB_WORKSPACE sudo chown -R runner $GITHUB_WORKSPACE

View File

@ -213,11 +213,12 @@ jobs:
shell: pwsh shell: pwsh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3.1.5 uses: codecov/codecov-action@v4
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows
name: ${{ runner.os }} Python ${{ matrix.python-version }} name: ${{ runner.os }} Python ${{ matrix.python-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success: success:
permissions: permissions:

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"
@ -150,11 +150,12 @@ jobs:
.ci/after_success.sh .ci/after_success.sh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3.1.5 uses: codecov/codecov-action@v4
with: with:
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
gcov: true gcov: true
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success: success:
permissions: permissions:

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,24 @@ Changelog (Pillow)
10.4.0 (unreleased) 10.4.0 (unreleased)
------------------- -------------------
- Deprecate BGR;15, BGR;16 and BGR;24 modes #7978
[radarhere, hugovk]
- Fix ImagingAccess for I;16N on big-endian #7921
[Yay295, radarhere]
- 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

@ -2,7 +2,6 @@
.PHONY: clean .PHONY: clean
clean: clean:
python3 setup.py clean
rm src/PIL/*.so || true rm src/PIL/*.so || true
rm -r build || true rm -r build || true
find . -name __pycache__ | xargs rm -r || true find . -name __pycache__ | xargs rm -r || true

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,7 +300,18 @@ def _cached_hopper(mode: str) -> Image.Image:
im = hopper("L") im = hopper("L")
else: else:
im = hopper() im = hopper()
return im.convert(mode) if mode.startswith("BGR;"):
with pytest.warns(DeprecationWarning):
im = im.convert(mode)
else:
try:
im = im.convert(mode)
except ImportError:
if mode == "LAB":
im = Image.open("Tests/images/hopper.Lab.tif")
else:
raise
return im
def djpeg_available() -> bool: def djpeg_available() -> bool:

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,13 +31,13 @@ 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 13 Ventura | 3.8, 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | | macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
| | PyPy3 | | | | PyPy3 | |
@ -47,7 +47,9 @@ These platforms are built and tested for every change.
| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | | Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 |
| | 3.12, 3.13, PyPy3 | | | | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.10 | arm64v8, ppc64le, | | | 3.10 | arm64v8 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
| | | s390x | | | | s390x |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Windows Server 2016 | 3.8 | x86-64 | | Windows Server 2016 | 3.8 | 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

@ -271,16 +271,16 @@ class D3DFMT(IntEnum):
module = sys.modules[__name__] module = sys.modules[__name__]
for item in DDSD: for item in DDSD:
assert item.name is not None assert item.name is not None
setattr(module, "DDSD_" + item.name, item.value) setattr(module, f"DDSD_{item.name}", item.value)
for item1 in DDSCAPS: for item1 in DDSCAPS:
assert item1.name is not None assert item1.name is not None
setattr(module, "DDSCAPS_" + item1.name, item1.value) setattr(module, f"DDSCAPS_{item1.name}", item1.value)
for item2 in DDSCAPS2: for item2 in DDSCAPS2:
assert item2.name is not None assert item2.name is not None
setattr(module, "DDSCAPS2_" + item2.name, item2.value) setattr(module, f"DDSCAPS2_{item2.name}", item2.value)
for item3 in DDPF: for item3 in DDPF:
assert item3.name is not None assert item3.name is not None
setattr(module, "DDPF_" + item3.name, item3.value) setattr(module, f"DDPF_{item3.name}", item3.value)
DDS_FOURCC = DDPF.FOURCC DDS_FOURCC = DDPF.FOURCC
DDS_RGB = DDPF.RGB DDS_RGB = DDPF.RGB

View File

@ -346,7 +346,7 @@ class Interop(IntEnum):
InteropVersion = 2 InteropVersion = 2
RelatedImageFileFormat = 4096 RelatedImageFileFormat = 4096
RelatedImageWidth = 4097 RelatedImageWidth = 4097
RleatedImageHeight = 4098 RelatedImageHeight = 4098
class IFD(IntEnum): class IFD(IntEnum):

View File

@ -196,7 +196,7 @@ class ImImageFile(ImageFile.ImageFile):
n += 1 n += 1
else: else:
msg = "Syntax error in IM header: " + s.decode("ascii", "replace") msg = f"Syntax error in IM header: {s.decode('ascii', 'replace')}"
raise SyntaxError(msg) raise SyntaxError(msg)
if not n: if not n:

View File

@ -41,7 +41,7 @@ import warnings
from collections.abc import Callable, MutableMapping from collections.abc import Callable, MutableMapping
from enum import IntEnum from enum import IntEnum
from types import ModuleType from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, cast from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, cast
# VERSION was removed in Pillow 6.0.0. # VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0.
@ -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
@ -404,7 +405,7 @@ def _getdecoder(mode, decoder_name, args, extra=()):
try: try:
# get decoder # get decoder
decoder = getattr(core, decoder_name + "_decoder") decoder = getattr(core, f"{decoder_name}_decoder")
except AttributeError as e: except AttributeError as e:
msg = f"decoder {decoder_name} not available" msg = f"decoder {decoder_name} not available"
raise OSError(msg) from e raise OSError(msg) from e
@ -427,7 +428,7 @@ def _getencoder(mode, encoder_name, args, extra=()):
try: try:
# get encoder # get encoder
encoder = getattr(core, encoder_name + "_encoder") encoder = getattr(core, f"{encoder_name}_encoder")
except AttributeError as e: except AttributeError as e:
msg = f"encoder {encoder_name} not available" msg = f"encoder {encoder_name} not available"
raise OSError(msg) from e raise OSError(msg) from e
@ -602,7 +603,7 @@ class Image:
) -> str: ) -> str:
suffix = "" suffix = ""
if format: if format:
suffix = "." + format suffix = f".{format}"
if not file: if not file:
f, filename = tempfile.mkstemp(suffix) f, filename = tempfile.mkstemp(suffix)
@ -876,7 +877,7 @@ class Image:
return self.pyaccess return self.pyaccess
return self.im.pixel_access(self.readonly) return self.im.pixel_access(self.readonly)
def verify(self): def verify(self) -> None:
""" """
Verifies the contents of a file. For data read from a file, this Verifies the contents of a file. For data read from a file, this
method attempts to determine if the file is broken, without method attempts to determine if the file is broken, without
@ -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
@ -1263,7 +1267,9 @@ class Image:
return im.crop((x0, y0, x1, y1)) return im.crop((x0, y0, x1, y1))
def draft(self, mode, size): def draft(
self, mode: str, size: tuple[int, int]
) -> tuple[str, tuple[int, int, float, float]] | None:
""" """
Configures the image file loader so it returns a version of the Configures the image file loader so it returns a version of the
image that as closely as possible matches the given mode and image that as closely as possible matches the given mode and
@ -1286,7 +1292,7 @@ class Image:
""" """
pass pass
def _expand(self, xmargin, ymargin=None): def _expand(self, xmargin: int, ymargin: int | None = None) -> Image:
if ymargin is None: if ymargin is None:
ymargin = xmargin ymargin = xmargin
self.load() self.load()
@ -2174,7 +2180,7 @@ class Image:
(Resampling.HAMMING, "Image.Resampling.HAMMING"), (Resampling.HAMMING, "Image.Resampling.HAMMING"),
) )
] ]
msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}"
raise ValueError(msg) raise ValueError(msg)
if reducing_gap is not None and reducing_gap < 1.0: if reducing_gap is not None and reducing_gap < 1.0:
@ -2819,7 +2825,7 @@ class Image:
(Resampling.BICUBIC, "Image.Resampling.BICUBIC"), (Resampling.BICUBIC, "Image.Resampling.BICUBIC"),
) )
] ]
msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] msg += f" Use {', '.join(filters[:-1])} or {filters[-1]}"
raise ValueError(msg) raise ValueError(msg)
image.load() image.load()
@ -2956,6 +2962,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:
@ -3214,8 +3223,8 @@ _fromarray_typemap = {
((1, 1, 3), "|u1"): ("RGB", "RGB"), ((1, 1, 3), "|u1"): ("RGB", "RGB"),
((1, 1, 4), "|u1"): ("RGBA", "RGBA"), ((1, 1, 4), "|u1"): ("RGBA", "RGBA"),
# shortcuts: # shortcuts:
((1, 1), _ENDIAN + "i4"): ("I", "I"), ((1, 1), f"{_ENDIAN}i4"): ("I", "I"),
((1, 1), _ENDIAN + "f4"): ("F", "F"), ((1, 1), f"{_ENDIAN}f4"): ("F", "F"),
} }
@ -3443,7 +3452,7 @@ def eval(image, *args):
return image.point(args[0]) return image.point(args[0])
def merge(mode, bands): def merge(mode: str, bands: Sequence[Image]) -> Image:
""" """
Merge a set of single band images into a new multiband image. Merge a set of single band images into a new multiband image.

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

@ -163,7 +163,7 @@ class ImageFile(Image.Image):
self.tile = [] self.tile = []
super().__setstate__(state) super().__setstate__(state)
def verify(self): def verify(self) -> None:
"""Check file integrity""" """Check file integrity"""
# raise exception if something's wrong. must be called # raise exception if something's wrong. must be called

View File

@ -61,7 +61,7 @@ class _Operand:
out = Image.new(mode or im_1.mode, im_1.size, None) out = Image.new(mode or im_1.mode, im_1.size, None)
im_1.load() im_1.load()
try: try:
op = getattr(_imagingmath, op + "_" + im_1.mode) op = getattr(_imagingmath, f"{op}_{im_1.mode}")
except AttributeError as e: except AttributeError as e:
msg = f"bad operand type for '{op}'" msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e raise TypeError(msg) from e
@ -89,7 +89,7 @@ class _Operand:
im_1.load() im_1.load()
im_2.load() im_2.load()
try: try:
op = getattr(_imagingmath, op + "_" + im_1.mode) op = getattr(_imagingmath, f"{op}_{im_1.mode}")
except AttributeError as e: except AttributeError as e:
msg = f"bad operand type for '{op}'" msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e raise TypeError(msg) from e

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."""
@ -42,8 +44,8 @@ def getmode(mode: str) -> ModeDescriptor:
# Bits need to be extended to bytes # Bits need to be extended to bytes
"1": ("L", "L", ("1",), "|b1"), "1": ("L", "L", ("1",), "|b1"),
"L": ("L", "L", ("L",), "|u1"), "L": ("L", "L", ("L",), "|u1"),
"I": ("L", "I", ("I",), endian + "i4"), "I": ("L", "I", ("I",), f"{endian}i4"),
"F": ("L", "F", ("F",), endian + "f4"), "F": ("L", "F", ("F",), f"{endian}f4"),
"P": ("P", "L", ("P",), "|u1"), "P": ("P", "L", ("P",), "|u1"),
"RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"),
"RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"),
@ -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)
@ -74,8 +78,8 @@ def getmode(mode: str) -> ModeDescriptor:
"I;16LS": "<i2", "I;16LS": "<i2",
"I;16B": ">u2", "I;16B": ">u2",
"I;16BS": ">i2", "I;16BS": ">i2",
"I;16N": endian + "u2", "I;16N": f"{endian}u2",
"I;16NS": endian + "i2", "I;16NS": f"{endian}i2",
"I;32": "<u4", "I;32": "<u4",
"I;32B": ">u4", "I;32B": ">u4",
"I;32L": "<u4", "I;32L": "<u4",

View File

@ -84,7 +84,7 @@ class LutBuilder:
], ],
} }
if op_name not in known_patterns: if op_name not in known_patterns:
msg = "Unknown pattern " + op_name + "!" msg = f"Unknown pattern {op_name}!"
raise Exception(msg) raise Exception(msg)
self.patterns = known_patterns[op_name] self.patterns = known_patterns[op_name]

View File

@ -196,7 +196,7 @@ class Window:
) )
def __dispatcher(self, action, *args): def __dispatcher(self, action, *args):
return getattr(self, "ui_handle_" + action)(*args) return getattr(self, f"ui_handle_{action}")(*args)
def ui_handle_clear(self, dc, x0, y0, x1, y1): def ui_handle_clear(self, dc, x0, y0, x1, y1):
pass pass

View File

@ -424,13 +424,15 @@ class JpegImageFile(ImageFile.ImageFile):
return s return s
def draft(self, mode, size): def draft(
self, mode: str, size: tuple[int, int]
) -> tuple[str, tuple[int, int, float, float]] | None:
if len(self.tile) != 1: if len(self.tile) != 1:
return return None
# Protect from second call # Protect from second call
if self.decoderconfig: if self.decoderconfig:
return return None
d, e, o, a = self.tile[0] d, e, o, a = self.tile[0]
scale = 1 scale = 1

View File

@ -142,7 +142,7 @@ def _save(im, fp, filename):
# we ignore the palette here # we ignore the palette here
im.mode = "P" im.mode = "P"
rawmode = "P;" + str(bpp) rawmode = f"P;{bpp}"
version = 1 version = 1
elif im.mode == "1": elif im.mode == "1":

View File

@ -144,9 +144,7 @@ class XrefTable:
elif key in self.deleted_entries: elif key in self.deleted_entries:
generation = self.deleted_entries[key] generation = self.deleted_entries[key]
else: else:
msg = ( msg = f"object ID {key} cannot be deleted because it doesn't exist"
"object ID " + str(key) + " cannot be deleted because it doesn't exist"
)
raise IndexError(msg) raise IndexError(msg)
def __contains__(self, key): def __contains__(self, key):
@ -225,7 +223,7 @@ class PdfName:
return hash(self.name) return hash(self.name)
def __repr__(self): def __repr__(self):
return f"PdfName({repr(self.name)})" return f"{self.__class__.__name__}({repr(self.name)})"
@classmethod @classmethod
def from_pdf_stream(cls, data): def from_pdf_stream(cls, data):
@ -884,7 +882,7 @@ class PdfParser:
if m: if m:
return cls.get_literal_string(data, m.end()) return cls.get_literal_string(data, m.end())
# return None, offset # fallback (only for debugging) # return None, offset # fallback (only for debugging)
msg = "unrecognized object: " + repr(data[offset : offset + 32]) msg = f"unrecognized object: {repr(data[offset : offset + 32])}"
raise PdfFormatError(msg) raise PdfFormatError(msg)
re_lit_str_token = re.compile( re_lit_str_token = re.compile(

View File

@ -189,7 +189,7 @@ class ChunkStream:
"""Call the appropriate chunk handler""" """Call the appropriate chunk handler"""
logger.debug("STREAM %r %s %s", cid, pos, length) logger.debug("STREAM %r %s %s", cid, pos, length)
return getattr(self, "chunk_" + cid.decode("ascii"))(pos, length) return getattr(self, f"chunk_{cid.decode('ascii')}")(pos, length)
def crc(self, cid, data): def crc(self, cid, data):
"""Read and verify checksum""" """Read and verify checksum"""
@ -783,7 +783,7 @@ class PngImageFile(ImageFile.ImageFile):
self.seek(frame) self.seek(frame)
return self._text return self._text
def verify(self): def verify(self) -> None:
"""Verify PNG file""" """Verify PNG file"""
if self.fp is None: if self.fp is None:

View File

@ -218,7 +218,7 @@ def loadImageSeries(filelist=None):
im = im.convert2byte() im = im.convert2byte()
except Exception: except Exception:
if not isSpiderImage(img): if not isSpiderImage(img):
print(img + " is not a Spider image file") print(f"{img} is not a Spider image file")
continue continue
im.info["filename"] = img im.info["filename"] = img
imglist.append(im) imglist.append(im)
@ -299,10 +299,10 @@ if __name__ == "__main__":
sys.exit() sys.exit()
with Image.open(filename) as im: with Image.open(filename) as im:
print("image: " + str(im)) print(f"image: {im}")
print("format: " + str(im.format)) print(f"format: {im.format}")
print("size: " + str(im.size)) print(f"size: {im.size}")
print("mode: " + str(im.mode)) print(f"mode: {im.mode}")
print("max, min: ", end=" ") print("max, min: ", end=" ")
print(im.getextrema()) print(im.getextrema())

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
@ -464,7 +469,7 @@ def _register_basic(idx_fmt_name):
idx, fmt, name = idx_fmt_name idx, fmt, name = idx_fmt_name
TYPES[idx] = name TYPES[idx] = name
size = struct.calcsize("=" + fmt) size = struct.calcsize(f"={fmt}")
_load_dispatch[idx] = ( # noqa: F821 _load_dispatch[idx] = ( # noqa: F821
size, size,
lambda self, data, legacy_api=True: ( lambda self, data, legacy_api=True: (
@ -982,8 +987,8 @@ ImageFileDirectory_v2._load_dispatch = _load_dispatch
ImageFileDirectory_v2._write_dispatch = _write_dispatch ImageFileDirectory_v2._write_dispatch = _write_dispatch
for idx, name in TYPES.items(): for idx, name in TYPES.items():
name = name.replace(" ", "_") name = name.replace(" ", "_")
setattr(ImageFileDirectory_v2, "load_" + name, _load_dispatch[idx][1]) setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1])
setattr(ImageFileDirectory_v2, "write_" + name, _write_dispatch[idx]) setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx])
del _load_dispatch, _write_dispatch, idx, name del _load_dispatch, _write_dispatch, idx, name
@ -2020,9 +2025,9 @@ class AppendingTiffWriter:
def setEndian(self, endian): def setEndian(self, endian):
self.endian = endian self.endian = endian
self.longFmt = self.endian + "L" self.longFmt = f"{self.endian}L"
self.shortFmt = self.endian + "H" self.shortFmt = f"{self.endian}H"
self.tagFormat = self.endian + "HHL" self.tagFormat = f"{self.endian}HHL"
def skipIFDs(self): def skipIFDs(self):
while True: while True:

View File

@ -89,7 +89,7 @@ def check_codec(feature):
codec, lib = codecs[feature] codec, lib = codecs[feature]
return codec + "_encoder" in dir(Image.core) return f"{codec}_encoder" in dir(Image.core)
def version_codec(feature): def version_codec(feature):
@ -105,7 +105,7 @@ def version_codec(feature):
codec, lib = codecs[feature] codec, lib = codecs[feature]
version = getattr(Image.core, lib + "_version") version = getattr(Image.core, f"{lib}_version")
if feature == "libtiff": if feature == "libtiff":
return version.split("\n")[0].split("Version ")[1] return version.split("\n")[0].split("Version ")[1]

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},