Merge branch 'main' into main

This commit is contained in:
Andrew Murray 2024-05-18 20:26:57 +10:00 committed by GitHub
commit 88750f42c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
90 changed files with 795 additions and 576 deletions

View File

@ -1 +1 @@
cibuildwheel==2.17.0 cibuildwheel==2.18.0

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

@ -1,12 +1,12 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.4 rev: v0.4.3
hooks: hooks:
- id: ruff - id: ruff
args: [--exit-non-zero-on-fix] args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.3.0 rev: 24.4.2
hooks: hooks:
- id: black - id: black
@ -29,7 +29,7 @@ repos:
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0 rev: v4.6.0
hooks: hooks:
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable - id: check-shebang-scripts-are-executable
@ -43,7 +43,7 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema - repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.28.1 rev: 0.28.2
hooks: hooks:
- id: check-github-workflows - id: check-github-workflows
- id: check-readthedocs - id: check-readthedocs
@ -55,7 +55,7 @@ repos:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: 1.7.0 rev: 1.8.0
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt

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

@ -273,7 +273,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

@ -336,9 +336,7 @@ def test_readline_psfile(tmp_path: Path) -> None:
strings = ["something", "else", "baz", "bif"] strings = ["something", "else", "baz", "bif"]
def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None: def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
ending = "Failure with line ending: %s" % ( ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}"
"".join("%s" % ord(s) for s in ending)
)
assert t.readline().strip("\r\n") == "something", ending assert t.readline().strip("\r\n") == "something", ending
assert t.readline().strip("\r\n") == "else", ending assert t.readline().strip("\r\n") == "else", ending
assert t.readline().strip("\r\n") == "baz", ending assert t.readline().strip("\r\n") == "baz", ending

View File

@ -185,7 +185,9 @@ class TestFileLibTiff(LibTiffTestCase):
assert field in reloaded, f"{field} not in metadata" assert field in reloaded, f"{field} not in metadata"
@pytest.mark.valgrind_known_error(reason="Known invalid metadata") @pytest.mark.valgrind_known_error(reason="Known invalid metadata")
def test_additional_metadata(self, tmp_path: Path) -> None: def test_additional_metadata(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
# these should not crash. Seriously dummy data, most of it doesn't make # these should not crash. Seriously dummy data, most of it doesn't make
# any sense, so we're running up against limits where we're asking # any sense, so we're running up against limits where we're asking
# libtiff to do stupid things. # libtiff to do stupid things.
@ -236,13 +238,28 @@ class TestFileLibTiff(LibTiffTestCase):
del new_ifd[338] del new_ifd[338]
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out, tiffinfo=new_ifd) im.save(out, tiffinfo=new_ifd)
TiffImagePlugin.WRITE_LIBTIFF = False @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)
def test_custom_metadata(self, tmp_path: Path) -> None:
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")
@ -343,24 +350,24 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not segfault # Should not segfault
im.save(outfile) im.save(outfile)
def test_xmlpacket_tag(self, tmp_path: Path) -> None: def test_xmlpacket_tag(
TiffImagePlugin.WRITE_LIBTIFF = True self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
if 700 in reloaded.tag_v2: if 700 in reloaded.tag_v2:
assert reloaded.tag_v2[700] == b"xmlpacket tag" assert reloaded.tag_v2[700] == b"xmlpacket tag"
def test_int_dpi(self, tmp_path: Path) -> None: def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
# issue #1765 # issue #1765
im = hopper("RGB") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out, dpi=(72, 72)) im.save(out, dpi=(72, 72))
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert reloaded.info["dpi"] == (72.0, 72.0) assert reloaded.info["dpi"] == (72.0, 72.0)
@ -422,13 +429,13 @@ class TestFileLibTiff(LibTiffTestCase):
assert "temp.tif" == reread.tag_v2[269] assert "temp.tif" == reread.tag_v2[269]
assert "temp.tif" == reread.tag[269][0] assert "temp.tif" == reread.tag[269][0]
def test_12bit_rawmode(self) -> None: def test_12bit_rawmode(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Are we generating the same interpretation """Are we generating the same interpretation
of the image as Imagemagick is?""" of the image as Imagemagick is?"""
TiffImagePlugin.READ_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/12bit.cropped.tif") as im: with Image.open("Tests/images/12bit.cropped.tif") as im:
im.load() im.load()
TiffImagePlugin.READ_LIBTIFF = False monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", False)
# to make the target -- # to make the target --
# convert 12bit.cropped.tif -depth 16 tmp.tif # convert 12bit.cropped.tif -depth 16 tmp.tif
# convert tmp.tif -evaluate RightShift 4 12in16bit2.tif # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif
@ -514,12 +521,13 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, out) assert_image_equal_tofile(im, out)
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
def test_palette_save(self, im: Image.Image, tmp_path: Path) -> None: def test_palette_save(
self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out) im.save(out)
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
# colormap/palette tag # colormap/palette tag
@ -548,9 +556,9 @@ class TestFileLibTiff(LibTiffTestCase):
with pytest.raises(OSError): with pytest.raises(OSError):
os.close(fn) os.close(fn)
def test_multipage(self) -> None: def test_multipage(self, monkeypatch: pytest.MonkeyPatch) -> None:
# issue #862 # issue #862
TiffImagePlugin.READ_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
@ -569,11 +577,9 @@ class TestFileLibTiff(LibTiffTestCase):
assert im.size == (20, 20) assert im.size == (20, 20)
assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255)
TiffImagePlugin.READ_LIBTIFF = False def test_multipage_nframes(self, monkeypatch: pytest.MonkeyPatch) -> None:
def test_multipage_nframes(self) -> None:
# issue #862 # issue #862
TiffImagePlugin.READ_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
frames = im.n_frames frames = im.n_frames
assert frames == 3 assert frames == 3
@ -582,10 +588,8 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not raise ValueError: I/O operation on closed file # Should not raise ValueError: I/O operation on closed file
im.load() im.load()
TiffImagePlugin.READ_LIBTIFF = False def test_multipage_seek_backwards(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
def test_multipage_seek_backwards(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
im.seek(1) im.seek(1)
im.load() im.load()
@ -593,24 +597,21 @@ class TestFileLibTiff(LibTiffTestCase):
im.seek(0) im.seek(0)
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
TiffImagePlugin.READ_LIBTIFF = False def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
def test__next(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True
with Image.open("Tests/images/hopper.tif") as im: with Image.open("Tests/images/hopper.tif") as im:
assert not im.tag.next assert not im.tag.next
im.load() im.load()
assert not im.tag.next assert not im.tag.next
def test_4bit(self) -> None: def test_4bit(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange # Arrange
test_file = "Tests/images/hopper_gray_4bpp.tif" test_file = "Tests/images/hopper_gray_4bpp.tif"
original = hopper("L") original = hopper("L")
# Act # Act
TiffImagePlugin.READ_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open(test_file) as im: with Image.open(test_file) as im:
TiffImagePlugin.READ_LIBTIFF = False
# Assert # Assert
assert im.size == (128, 128) assert im.size == (128, 128)
@ -650,12 +651,12 @@ class TestFileLibTiff(LibTiffTestCase):
assert im2.mode == "L" assert im2.mode == "L"
assert_image_equal(im, im2) assert_image_equal(im, im2)
def test_save_bytesio(self) -> None: def test_save_bytesio(self, monkeypatch: pytest.MonkeyPatch) -> None:
# PR 1011 # PR 1011
# Test TIFF saving to io.BytesIO() object. # Test TIFF saving to io.BytesIO() object.
TiffImagePlugin.WRITE_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
TiffImagePlugin.READ_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
# Generate test image # Generate test image
pilim = hopper() pilim = hopper()
@ -672,9 +673,6 @@ class TestFileLibTiff(LibTiffTestCase):
save_bytesio("packbits") save_bytesio("packbits")
save_bytesio("tiff_lzw") save_bytesio("tiff_lzw")
TiffImagePlugin.WRITE_LIBTIFF = False
TiffImagePlugin.READ_LIBTIFF = False
def test_save_ycbcr(self, tmp_path: Path) -> None: def test_save_ycbcr(self, tmp_path: Path) -> None:
im = hopper("YCbCr") im = hopper("YCbCr")
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")
@ -694,15 +692,16 @@ class TestFileLibTiff(LibTiffTestCase):
if Image.core.libtiff_support_custom_tags: if Image.core.libtiff_support_custom_tags:
assert reloaded.tag_v2[34665] == 125456 assert reloaded.tag_v2[34665] == 125456
def test_crashing_metadata(self, tmp_path: Path) -> None: def test_crashing_metadata(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
# issue 1597 # issue 1597
with Image.open("Tests/images/rdf.tif") as im: with Image.open("Tests/images/rdf.tif") as im:
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
# this shouldn't crash # this shouldn't crash
im.save(out, format="TIFF") im.save(out, format="TIFF")
TiffImagePlugin.WRITE_LIBTIFF = False
def test_page_number_x_0(self, tmp_path: Path) -> None: def test_page_number_x_0(self, tmp_path: Path) -> None:
# Issue 973 # Issue 973
@ -733,36 +732,41 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not raise PermissionError. # Should not raise PermissionError.
os.remove(tmpfile) os.remove(tmpfile)
def test_read_icc(self) -> None: def test_read_icc(self, monkeypatch: pytest.MonkeyPatch) -> None:
with Image.open("Tests/images/hopper.iccprofile.tif") as img: with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc = img.info.get("icc_profile") icc = img.info.get("icc_profile")
assert icc is not None assert icc is not None
TiffImagePlugin.READ_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/hopper.iccprofile.tif") as img: with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc_libtiff = img.info.get("icc_profile") icc_libtiff = img.info.get("icc_profile")
assert icc_libtiff is not None assert icc_libtiff is not None
TiffImagePlugin.READ_LIBTIFF = False
assert icc == icc_libtiff assert icc == icc_libtiff
def test_write_icc(self, tmp_path: Path) -> None: @pytest.mark.parametrize(
def check_write(libtiff: bool) -> None: "libtiff",
TiffImagePlugin.WRITE_LIBTIFF = 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_write_icc(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
with Image.open("Tests/images/hopper.iccprofile.tif") as img: with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc_profile = img.info["icc_profile"] icc_profile = img.info["icc_profile"]
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
img.save(out, icc_profile=icc_profile) img.save(out, icc_profile=icc_profile)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert icc_profile == reloaded.info["icc_profile"] assert icc_profile == reloaded.info["icc_profile"]
libtiffs = []
if Image.core.libtiff_support_custom_tags:
libtiffs.append(True)
libtiffs.append(False)
for libtiff in libtiffs:
check_write(libtiff)
def test_multipage_compression(self) -> None: def test_multipage_compression(self) -> None:
with Image.open("Tests/images/compression.tif") as im: with Image.open("Tests/images/compression.tif") as im:
@ -840,12 +844,13 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB")
def test_sampleformat_write(self, tmp_path: Path) -> None: def test_sampleformat_write(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
im = Image.new("F", (1, 1)) im = Image.new("F", (1, 1))
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out) im.save(out)
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
assert reloaded.mode == "F" assert reloaded.mode == "F"
@ -1091,15 +1096,14 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as im: with Image.open(out) as im:
im.load() im.load()
def test_realloc_overflow(self) -> None: def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
TiffImagePlugin.READ_LIBTIFF = True monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
with pytest.raises(OSError) as e: with pytest.raises(OSError) as e:
im.load() im.load()
# Assert that the error code is IMAGING_CODEC_MEMORY # Assert that the error code is IMAGING_CODEC_MEMORY
assert str(e.value) == "-9" assert str(e.value) == "-9"
TiffImagePlugin.READ_LIBTIFF = False
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:

39
Tests/test_file_mpeg.py Normal file
View File

@ -0,0 +1,39 @@
from __future__ import annotations
from io import BytesIO
import pytest
from PIL import Image, MpegImagePlugin
def test_identify() -> None:
# Arrange
b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01")
# Act
with Image.open(b) as im:
# Assert
assert im.format == "MPEG"
assert im.mode == "RGB"
assert im.size == (16, 1)
def test_invalid_file() -> None:
# Arrange
invalid_file = "Tests/images/flower.jpg"
# Act / Assert
with pytest.raises(SyntaxError):
MpegImagePlugin.MpegImageFile(invalid_file)
def test_load() -> None:
# Arrange
b = BytesIO(b"\x00\x00\x01\xb3\x01\x00\x01")
with Image.open(b) as im:
# Act / Assert: cannot load
with pytest.raises(OSError):
im.load()

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,26 @@ 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,
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", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
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 +1026,33 @@ class TestImage:
class TestImageBytes: class TestImageBytes:
@pytest.mark.parametrize("mode", image_mode_names) @pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
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", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
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", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
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

@ -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,46 +190,30 @@ 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", Image.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) self.check(mode)
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
def test_deprecated(self, mode: str) -> None:
with pytest.warns(DeprecationWarning):
self.check(mode)
def test_list(self) -> None: def test_list(self) -> None:
im = hopper() im = hopper()
assert im.getpixel([0, 0]) == (20, 20, 70) assert im.getpixel([0, 0]) == (20, 20, 70)
@ -238,7 +221,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 +281,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 +312,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 +332,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"))
@ -439,13 +410,14 @@ class TestEmbeddable:
from setuptools.command import build_ext from setuptools.command import build_ext
with open("embed_pil.c", "w", encoding="utf-8") as fh: with open("embed_pil.c", "w", encoding="utf-8") as fh:
home = sys.prefix.replace("\\", "\\\\")
fh.write( fh.write(
""" f"""
#include "Python.h" #include "Python.h"
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {{
char *home = "%s"; char *home = "{home}";
wchar_t *whome = Py_DecodeLocale(home, NULL); wchar_t *whome = Py_DecodeLocale(home, NULL);
Py_SetPythonHome(whome); Py_SetPythonHome(whome);
@ -460,9 +432,8 @@ int main(int argc, char* argv[])
PyMem_RawFree(whome); PyMem_RawFree(whome);
return 0; return 0;
} }}
""" """
% sys.prefix.replace("\\", "\\\\")
) )
compiler = getattr(build_ext, "new_compiler")() compiler = getattr(build_ext, "new_compiler")()
@ -478,7 +449,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

@ -47,7 +47,11 @@ def test_iterator_min_frame() -> None:
assert i[index] == next(i) assert i[index] == next(i)
def _test_multipage_tiff() -> None: @pytest.mark.parametrize(
"libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False)
)
def test_multipage_tiff(monkeypatch: pytest.MonkeyPatch, libtiff: bool) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", libtiff)
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
for index, frame in enumerate(ImageSequence.Iterator(im)): for index, frame in enumerate(ImageSequence.Iterator(im)):
frame.load() frame.load()
@ -55,17 +59,6 @@ def _test_multipage_tiff() -> None:
frame.convert("RGB") frame.convert("RGB")
def test_tiff() -> None:
_test_multipage_tiff()
@skip_unless_feature("libtiff")
def test_libtiff() -> None:
TiffImagePlugin.READ_LIBTIFF = True
_test_multipage_tiff()
TiffImagePlugin.READ_LIBTIFF = False
def test_consecutive() -> None: def test_consecutive() -> None:
with Image.open("Tests/images/multipage.tiff") as im: with Image.open("Tests/images/multipage.tiff") as im:
first_frame = None first_frame = None

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

@ -3,10 +3,12 @@ from __future__ import annotations
from fractions import Fraction from fractions import Fraction
from pathlib import Path from pathlib import Path
from PIL import Image, TiffImagePlugin, features import pytest
from PIL import Image, TiffImagePlugin
from PIL.TiffImagePlugin import IFDRational from PIL.TiffImagePlugin import IFDRational
from .helper import hopper from .helper import hopper, skip_unless_feature
def _test_equal(num, denom, target) -> None: def _test_equal(num, denom, target) -> None:
@ -52,18 +54,18 @@ def test_nonetype() -> None:
assert xres and yres assert xres and yres
def test_ifd_rational_save(tmp_path: Path) -> None: @pytest.mark.parametrize(
methods = [True] "libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), False)
if features.check("libtiff"): )
methods.append(False) def test_ifd_rational_save(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, libtiff: bool
) -> None:
im = hopper()
out = str(tmp_path / "temp.tiff")
res = IFDRational(301, 1)
for libtiff in methods: monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
TiffImagePlugin.WRITE_LIBTIFF = libtiff im.save(out, dpi=(res, res), compression="raw")
im = hopper() with Image.open(out) as reloaded:
out = str(tmp_path / "temp.tiff") assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282])
res = IFDRational(301, 1)
im.save(out, dpi=(res, res), compression="raw")
with Image.open(out) as reloaded:
assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282])

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

@ -165,9 +165,9 @@ if __name__ == "__main__":
print("Running selftest:") print("Running selftest:")
status = doctest.testmod(sys.modules[__name__]) status = doctest.testmod(sys.modules[__name__])
if status[0]: if status[0]:
print("*** %s tests of %d failed." % status) print(f"*** {status[0]} tests of {status[1]} failed.")
exit_status = 1 exit_status = 1
else: else:
print("--- %s tests passed." % status[1]) print(f"--- {status[1]} tests passed.")
sys.exit(exit_status) sys.exit(exit_status)

View File

@ -253,7 +253,7 @@ class BlpImageFile(ImageFile.ImageFile):
format = "BLP" format = "BLP"
format_description = "Blizzard Mipmap Format" format_description = "Blizzard Mipmap Format"
def _open(self): def _open(self) -> None:
self.magic = self.fp.read(4) self.magic = self.fp.read(4)
self.fp.seek(5, os.SEEK_CUR) self.fp.seek(5, os.SEEK_CUR)
@ -333,7 +333,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
class BLP1Decoder(_BLPBaseDecoder): class BLP1Decoder(_BLPBaseDecoder):
def _load(self): def _load(self) -> None:
if self._blp_compression == Format.JPEG: if self._blp_compression == Format.JPEG:
self._decode_jpeg_stream() self._decode_jpeg_stream()
@ -418,7 +418,7 @@ class BLP2Decoder(_BLPBaseDecoder):
class BLPEncoder(ImageFile.PyEncoder): class BLPEncoder(ImageFile.PyEncoder):
_pushes_fd = True _pushes_fd = True
def _write_palette(self): def _write_palette(self) -> bytes:
data = b"" data = b""
palette = self.im.getpalette("RGBA", "RGBA") palette = self.im.getpalette("RGBA", "RGBA")
for i in range(len(palette) // 4): for i in range(len(palette) // 4):

View File

@ -286,7 +286,7 @@ class BmpImageFile(ImageFile.ImageFile):
) )
] ]
def _open(self): def _open(self) -> None:
"""Open file, check magic number and read header""" """Open file, check magic number and read header"""
# read 14 bytes: magic number, filesize, reserved, header final offset # read 14 bytes: magic number, filesize, reserved, header final offset
head_data = self.fp.read(14) head_data = self.fp.read(14)
@ -379,7 +379,7 @@ class DibImageFile(BmpImageFile):
format = "DIB" format = "DIB"
format_description = "Windows Bitmap" format_description = "Windows Bitmap"
def _open(self): def _open(self) -> None:
self._bitmap() self._bitmap()

View File

@ -37,7 +37,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
format = "BUFR" format = "BUFR"
format_description = "BUFR" format_description = "BUFR"
def _open(self): def _open(self) -> None:
offset = self.fp.tell() offset = self.fp.tell()
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):

View File

@ -208,7 +208,7 @@ class CurImageFile(IcoImagePlugin.IcoImageFile):
format = "CUR" format = "CUR"
format_description = "Windows Cursor" format_description = "Windows Cursor"
def _open(self): def _open(self) -> None:
self.ico = CurFile(self.fp) self.ico = CurFile(self.fp)
self.info["sizes"] = self.ico.sizes() self.info["sizes"] = self.ico.sizes()
self.info["hotspots"] = self.ico.hotspots() self.info["hotspots"] = self.ico.hotspots()

View File

@ -63,7 +63,7 @@ class DcxImageFile(PcxImageFile):
self.is_animated = self.n_frames > 1 self.is_animated = self.n_frames > 1
self.seek(0) self.seek(0)
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
self.frame = frame self.frame = frame
@ -71,7 +71,7 @@ class DcxImageFile(PcxImageFile):
self.fp.seek(self._offset[frame]) self.fp.seek(self._offset[frame])
PcxImageFile._open(self) PcxImageFile._open(self)
def tell(self): def tell(self) -> int:
return self.frame return self.frame

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
@ -331,7 +331,7 @@ class DdsImageFile(ImageFile.ImageFile):
format = "DDS" format = "DDS"
format_description = "DirectDraw Surface" format_description = "DirectDraw Surface"
def _open(self): def _open(self) -> None:
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):
msg = "not a DDS file" msg = "not a DDS file"
raise SyntaxError(msg) raise SyntaxError(msg)
@ -472,7 +472,7 @@ class DdsImageFile(ImageFile.ImageFile):
else: else:
self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)] self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)]
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
pass pass

View File

@ -42,7 +42,7 @@ gs_binary: str | bool | None = None
gs_windows_binary = None gs_windows_binary = None
def has_ghostscript(): def has_ghostscript() -> bool:
global gs_binary, gs_windows_binary global gs_binary, gs_windows_binary
if gs_binary is None: if gs_binary is None:
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
@ -178,7 +178,7 @@ class PSFile:
self.char = None self.char = None
self.fp.seek(offset, whence) self.fp.seek(offset, whence)
def readline(self): def readline(self) -> str:
s = [self.char or b""] s = [self.char or b""]
self.char = None self.char = None
@ -212,7 +212,7 @@ class EpsImageFile(ImageFile.ImageFile):
mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
def _open(self): def _open(self) -> None:
(length, offset) = self._find_offset(self.fp) (length, offset) = self._find_offset(self.fp)
# go to offset - start of "%!PS" # go to offset - start of "%!PS"
@ -404,7 +404,7 @@ class EpsImageFile(ImageFile.ImageFile):
self.tile = [] self.tile = []
return Image.Image.load(self) return Image.Image.load(self)
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
# we can't incrementally load, so force ImageFile.parser to # we can't incrementally load, so force ImageFile.parser to
# use our custom load method by defining this method. # use our custom load method by defining this method.
pass pass

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

@ -123,7 +123,7 @@ class FliImageFile(ImageFile.ImageFile):
palette[i] = (r, g, b) palette[i] = (r, g, b)
i += 1 i += 1
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if frame < self.__frame: if frame < self.__frame:
@ -132,7 +132,7 @@ class FliImageFile(ImageFile.ImageFile):
for f in range(self.__frame + 1, frame + 1): for f in range(self.__frame + 1, frame + 1):
self._seek(f) self._seek(f)
def _seek(self, frame): def _seek(self, frame: int) -> None:
if frame == 0: if frame == 0:
self.__frame = -1 self.__frame = -1
self._fp.seek(self.__rewind) self._fp.seek(self.__rewind)
@ -162,7 +162,7 @@ class FliImageFile(ImageFile.ImageFile):
self.__offset += framesize self.__offset += framesize
def tell(self): def tell(self) -> int:
return self.__frame return self.__frame

View File

@ -237,7 +237,7 @@ class FpxImageFile(ImageFile.ImageFile):
return ImageFile.ImageFile.load(self) return ImageFile.ImageFile.load(self)
def close(self): def close(self) -> None:
self.ole.close() self.ole.close()
super().close() super().close()

View File

@ -71,7 +71,7 @@ class FtexImageFile(ImageFile.ImageFile):
format = "FTEX" format = "FTEX"
format_description = "Texture File Format (IW2:EOC)" format_description = "Texture File Format (IW2:EOC)"
def _open(self): def _open(self) -> None:
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):
msg = "not an FTEX file" msg = "not an FTEX file"
raise SyntaxError(msg) raise SyntaxError(msg)
@ -103,7 +103,7 @@ class FtexImageFile(ImageFile.ImageFile):
self.fp.close() self.fp.close()
self.fp = BytesIO(data) self.fp = BytesIO(data)
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
pass pass

View File

@ -41,7 +41,7 @@ class GbrImageFile(ImageFile.ImageFile):
format = "GBR" format = "GBR"
format_description = "GIMP brush file" format_description = "GIMP brush file"
def _open(self): def _open(self) -> None:
header_size = i32(self.fp.read(4)) header_size = i32(self.fp.read(4))
if header_size < 20: if header_size < 20:
msg = "not a GIMP brush" msg = "not a GIMP brush"

View File

@ -76,19 +76,19 @@ class GifImageFile(ImageFile.ImageFile):
global_palette = None global_palette = None
def data(self): def data(self) -> bytes | None:
s = self.fp.read(1) s = self.fp.read(1)
if s and s[0]: if s and s[0]:
return self.fp.read(s[0]) return self.fp.read(s[0])
return None return None
def _is_palette_needed(self, p): def _is_palette_needed(self, p: bytes) -> bool:
for i in range(0, len(p), 3): for i in range(0, len(p), 3):
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]): if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
return True return True
return False return False
def _open(self): def _open(self) -> None:
# Screen # Screen
s = self.fp.read(13) s = self.fp.read(13)
if not _accept(s): if not _accept(s):
@ -147,7 +147,7 @@ class GifImageFile(ImageFile.ImageFile):
self.seek(current) self.seek(current)
return self._is_animated return self._is_animated
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if frame < self.__frame: if frame < self.__frame:
@ -417,7 +417,7 @@ class GifImageFile(ImageFile.ImageFile):
elif k in self.info: elif k in self.info:
del self.info[k] del self.info[k]
def load_prepare(self): def load_prepare(self) -> None:
temp_mode = "P" if self._frame_palette else "L" temp_mode = "P" if self._frame_palette else "L"
self._prev_im = None self._prev_im = None
if self.__frame == 0: if self.__frame == 0:
@ -437,7 +437,7 @@ class GifImageFile(ImageFile.ImageFile):
super().load_prepare() super().load_prepare()
def load_end(self): def load_end(self) -> None:
if self.__frame == 0: if self.__frame == 0:
if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
if self._frame_transparency is not None: if self._frame_transparency is not None:
@ -463,7 +463,7 @@ class GifImageFile(ImageFile.ImageFile):
else: else:
self.im.paste(frame_im, self.dispose_extent) self.im.paste(frame_im, self.dispose_extent)
def tell(self): def tell(self) -> int:
return self.__frame return self.__frame
@ -474,7 +474,7 @@ class GifImageFile(ImageFile.ImageFile):
RAWMODE = {"1": "L", "L": "L", "P": "P"} RAWMODE = {"1": "L", "L": "L", "P": "P"}
def _normalize_mode(im): def _normalize_mode(im: Image.Image) -> Image.Image:
""" """
Takes an image (or frame), returns an image in a mode that is appropriate Takes an image (or frame), returns an image in a mode that is appropriate
for saving in a Gif. for saving in a Gif.
@ -887,7 +887,7 @@ def _get_optimize(im, info):
return used_palette_colors return used_palette_colors
def _get_color_table_size(palette_bytes): def _get_color_table_size(palette_bytes: bytes) -> int:
# calculate the palette size for the header # calculate the palette size for the header
if not palette_bytes: if not palette_bytes:
return 0 return 0
@ -897,7 +897,7 @@ def _get_color_table_size(palette_bytes):
return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1 return math.ceil(math.log(len(palette_bytes) // 3, 2)) - 1
def _get_header_palette(palette_bytes): def _get_header_palette(palette_bytes: bytes) -> bytes:
""" """
Returns the palette, null padded to the next power of 2 (*3) bytes Returns the palette, null padded to the next power of 2 (*3) bytes
suitable for direct inclusion in the GIF header suitable for direct inclusion in the GIF header
@ -915,7 +915,7 @@ def _get_header_palette(palette_bytes):
return palette_bytes return palette_bytes
def _get_palette_bytes(im): def _get_palette_bytes(im: Image.Image) -> bytes:
""" """
Gets the palette for inclusion in the gif header Gets the palette for inclusion in the gif header

View File

@ -53,5 +53,5 @@ class GimpPaletteFile:
self.palette = b"".join(self.palette) self.palette = b"".join(self.palette)
def getpalette(self): def getpalette(self) -> tuple[bytes, str]:
return self.palette, self.rawmode return self.palette, self.rawmode

View File

@ -37,7 +37,7 @@ class GribStubImageFile(ImageFile.StubImageFile):
format = "GRIB" format = "GRIB"
format_description = "GRIB" format_description = "GRIB"
def _open(self): def _open(self) -> None:
offset = self.fp.tell() offset = self.fp.tell()
if not _accept(self.fp.read(8)): if not _accept(self.fp.read(8)):

View File

@ -37,7 +37,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
format = "HDF5" format = "HDF5"
format_description = "HDF5" format_description = "HDF5"
def _open(self): def _open(self) -> None:
offset = self.fp.tell() offset = self.fp.tell()
if not _accept(self.fp.read(8)): if not _accept(self.fp.read(8)):

View File

@ -252,7 +252,7 @@ class IcnsImageFile(ImageFile.ImageFile):
format = "ICNS" format = "ICNS"
format_description = "Mac OS icns resource" format_description = "Mac OS icns resource"
def _open(self): def _open(self) -> None:
self.icns = IcnsFile(self.fp) self.icns = IcnsFile(self.fp)
self._mode = "RGBA" self._mode = "RGBA"
self.info["sizes"] = self.icns.itersizes() self.info["sizes"] = self.icns.itersizes()

View File

@ -302,7 +302,7 @@ class IcoImageFile(ImageFile.ImageFile):
format = "ICO" format = "ICO"
format_description = "Windows Icon" format_description = "Windows Icon"
def _open(self): def _open(self) -> None:
self.ico = IcoFile(self.fp) self.ico = IcoFile(self.fp)
self.info["sizes"] = self.ico.sizes() self.info["sizes"] = self.ico.sizes()
self.size = self.ico.entry[0]["dim"] self.size = self.ico.entry[0]["dim"]
@ -341,7 +341,7 @@ class IcoImageFile(ImageFile.ImageFile):
self.size = im.size self.size = im.size
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
# Flag the ImageFile.Parser so that it # Flag the ImageFile.Parser so that it
# just does all the decode at the end. # just does all the decode at the end.
pass pass

View File

@ -119,7 +119,7 @@ class ImImageFile(ImageFile.ImageFile):
format_description = "IFUNC Image Memory" format_description = "IFUNC Image Memory"
_close_exclusive_fp_after_loading = False _close_exclusive_fp_after_loading = False
def _open(self): def _open(self) -> None:
# Quick rejection: if there's not an LF among the first # Quick rejection: if there's not an LF among the first
# 100 bytes, this is (probably) not a text header. # 100 bytes, this is (probably) not a text header.
@ -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:
@ -271,14 +271,14 @@ class ImImageFile(ImageFile.ImageFile):
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
@property @property
def n_frames(self): def n_frames(self) -> int:
return self.info[FRAMES] return self.info[FRAMES]
@property @property
def is_animated(self): def is_animated(self) -> bool:
return self.info[FRAMES] > 1 return self.info[FRAMES] > 1
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
@ -296,7 +296,7 @@ class ImImageFile(ImageFile.ImageFile):
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
def tell(self): def tell(self) -> int:
return self.frame return self.frame

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
@ -248,7 +249,28 @@ def _conv_type_shape(im):
return shape, m.typestr return shape, m.typestr
MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"] MODES = [
"1",
"CMYK",
"F",
"HSV",
"I",
"I;16",
"I;16B",
"I;16L",
"I;16N",
"L",
"LA",
"La",
"LAB",
"P",
"PA",
"RGB",
"RGBA",
"RGBa",
"RGBX",
"YCbCr",
]
# raw modes that may be memory mapped. NOTE: if you change this, you # raw modes that may be memory mapped. NOTE: if you change this, you
# may have to modify the stride calculation in map.c too! # may have to modify the stride calculation in map.c too!
@ -404,7 +426,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 +449,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 +624,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 +898,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 +961,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 +1288,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,13 +1313,16 @@ 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()
return self._new(self.im.expand(xmargin, ymargin)) return self._new(self.im.expand(xmargin, ymargin))
def filter(self, filter): if TYPE_CHECKING:
from . import ImageFilter
def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image:
""" """
Filters this image using the given filter. For a list of Filters this image using the given filter. For a list of
available filters, see the :py:mod:`~PIL.ImageFilter` module. available filters, see the :py:mod:`~PIL.ImageFilter` module.
@ -1304,7 +1334,7 @@ class Image:
self.load() self.load()
if isinstance(filter, Callable): if callable(filter):
filter = filter() filter = filter()
if not hasattr(filter, "filter"): if not hasattr(filter, "filter"):
msg = "filter argument should be ImageFilter.Filter instance or class" msg = "filter argument should be ImageFilter.Filter instance or class"
@ -2174,7 +2204,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 +2849,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 +2986,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 +3247,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 +3476,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

@ -34,7 +34,7 @@ from __future__ import annotations
import math import math
import numbers import numbers
import struct import struct
from typing import Sequence, cast from typing import TYPE_CHECKING, Sequence, cast
from . import Image, ImageColor from . import Image, ImageColor
from ._typing import Coords from ._typing import Coords
@ -92,7 +92,10 @@ class ImageDraw:
self.fontmode = "L" # aliasing is okay for other modes self.fontmode = "L" # aliasing is okay for other modes
self.fill = False self.fill = False
def getfont(self): if TYPE_CHECKING:
from . import ImageFont
def getfont(self) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
""" """
Get the current default font. Get the current default font.

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
@ -311,7 +311,7 @@ class ImageFile(Image.Image):
return Image.Image.load(self) return Image.Image.load(self)
def load_prepare(self): def load_prepare(self) -> None:
# create image memory if necessary # create image memory if necessary
if not self.im or self.im.mode != self.mode or self.im.size != self.size: if not self.im or self.im.mode != self.mode or self.im.size != self.size:
self.im = Image.core.new(self.mode, self.size) self.im = Image.core.new(self.mode, self.size)
@ -319,16 +319,16 @@ class ImageFile(Image.Image):
if self.mode == "P": if self.mode == "P":
Image.Image.load(self) Image.Image.load(self)
def load_end(self): def load_end(self) -> None:
# may be overridden # may be overridden
pass pass
# may be defined for contained formats # may be defined for contained formats
# def load_seek(self, pos): # def load_seek(self, pos: int) -> None:
# pass # pass
# may be defined for blocked formats (e.g. PNG) # may be defined for blocked formats (e.g. PNG)
# def load_read(self, read_bytes): # def load_read(self, read_bytes: int) -> bytes:
# pass # pass
def _seek_check(self, frame): def _seek_check(self, frame):
@ -390,7 +390,7 @@ class Parser:
offset = 0 offset = 0
finished = 0 finished = 0
def reset(self): def reset(self) -> None:
""" """
(Consumer) Reset the parser. Note that you can only call this (Consumer) Reset the parser. Note that you can only call this
method immediately after you've created a parser; parser method immediately after you've created a parser; parser
@ -605,7 +605,7 @@ def _safe_read(fp, size):
class PyCodecState: class PyCodecState:
def __init__(self): def __init__(self) -> None:
self.xsize = 0 self.xsize = 0
self.ysize = 0 self.ysize = 0
self.xoff = 0 self.xoff = 0
@ -634,7 +634,7 @@ class PyCodec:
""" """
self.args = args self.args = args
def cleanup(self): def cleanup(self) -> None:
""" """
Override to perform codec specific cleanup Override to perform codec specific cleanup

View File

@ -16,11 +16,14 @@
# #
from __future__ import annotations from __future__ import annotations
import abc
import functools import functools
class Filter: class Filter:
pass @abc.abstractmethod
def filter(self, image):
pass
class MultibandFilter(Filter): class MultibandFilter(Filter):
@ -541,7 +544,7 @@ class Color3DLUT(MultibandFilter):
_copy_table=False, _copy_table=False,
) )
def __repr__(self): def __repr__(self) -> str:
r = [ r = [
f"{self.__class__.__name__} from {self.table.__class__.__name__}", f"{self.__class__.__name__} from {self.table.__class__.__name__}",
"size={:d}x{:d}x{:d}".format(*self.size), "size={:d}x{:d}x{:d}".format(*self.size),

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

@ -66,7 +66,7 @@ class ImagePalette:
def colors(self, colors): def colors(self, colors):
self._colors = colors self._colors = colors
def copy(self): def copy(self) -> ImagePalette:
new = ImagePalette() new = ImagePalette()
new.mode = self.mode new.mode = self.mode
@ -77,7 +77,7 @@ class ImagePalette:
return new return new
def getdata(self): def getdata(self) -> tuple[str, bytes]:
""" """
Get palette contents in format suitable for the low-level Get palette contents in format suitable for the low-level
``im.putpalette`` primitive. ``im.putpalette`` primitive.
@ -88,7 +88,7 @@ class ImagePalette:
return self.rawmode, self.palette return self.rawmode, self.palette
return self.mode, self.tobytes() return self.mode, self.tobytes()
def tobytes(self): def tobytes(self) -> bytes:
"""Convert palette to bytes. """Convert palette to bytes.
.. warning:: This method is experimental. .. warning:: This method is experimental.

View File

@ -128,7 +128,7 @@ class PhotoImage:
if image: if image:
self.paste(image) self.paste(image)
def __del__(self): def __del__(self) -> None:
name = self.__photo.name name = self.__photo.name
self.__photo.name = None self.__photo.name = None
try: try:
@ -136,7 +136,7 @@ class PhotoImage:
except Exception: except Exception:
pass # ignore internal errors pass # ignore internal errors
def __str__(self): def __str__(self) -> str:
""" """
Get the Tkinter photo image identifier. This method is automatically Get the Tkinter photo image identifier. This method is automatically
called by Tkinter whenever a PhotoImage object is passed to a Tkinter called by Tkinter whenever a PhotoImage object is passed to a Tkinter
@ -146,7 +146,7 @@ class PhotoImage:
""" """
return str(self.__photo) return str(self.__photo)
def width(self): def width(self) -> int:
""" """
Get the width of the image. Get the width of the image.
@ -154,7 +154,7 @@ class PhotoImage:
""" """
return self.__size[0] return self.__size[0]
def height(self): def height(self) -> int:
""" """
Get the height of the image. Get the height of the image.
@ -219,7 +219,7 @@ class BitmapImage:
kw["data"] = image.tobitmap() kw["data"] = image.tobitmap()
self.__photo = tkinter.BitmapImage(**kw) self.__photo = tkinter.BitmapImage(**kw)
def __del__(self): def __del__(self) -> None:
name = self.__photo.name name = self.__photo.name
self.__photo.name = None self.__photo.name = None
try: try:
@ -227,7 +227,7 @@ class BitmapImage:
except Exception: except Exception:
pass # ignore internal errors pass # ignore internal errors
def width(self): def width(self) -> int:
""" """
Get the width of the image. Get the width of the image.
@ -235,7 +235,7 @@ class BitmapImage:
""" """
return self.__size[0] return self.__size[0]
def height(self): def height(self) -> int:
""" """
Get the height of the image. Get the height of the image.
@ -243,7 +243,7 @@ class BitmapImage:
""" """
return self.__size[1] return self.__size[1]
def __str__(self): def __str__(self) -> str:
""" """
Get the Tkinter bitmap image identifier. This method is automatically Get the Tkinter bitmap image identifier. This method is automatically
called by Tkinter whenever a BitmapImage object is passed to a Tkinter called by Tkinter whenever a BitmapImage object is passed to a Tkinter

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
@ -204,7 +204,7 @@ class Window:
def ui_handle_damage(self, x0, y0, x1, y1): def ui_handle_damage(self, x0, y0, x1, y1):
pass pass
def ui_handle_destroy(self): def ui_handle_destroy(self) -> None:
pass pass
def ui_handle_repair(self, dc, x0, y0, x1, y1): def ui_handle_repair(self, dc, x0, y0, x1, y1):
@ -213,7 +213,7 @@ class Window:
def ui_handle_resize(self, width, height): def ui_handle_resize(self, width, height):
pass pass
def mainloop(self): def mainloop(self) -> None:
Image.core.eventloop() Image.core.eventloop()

View File

@ -57,7 +57,7 @@ def dump(c: Sequence[int | bytes]) -> None:
""".. deprecated:: 10.2.0""" """.. deprecated:: 10.2.0"""
deprecate("IptcImagePlugin.dump", 12) deprecate("IptcImagePlugin.dump", 12)
for i in c: for i in c:
print("%02x" % _i8(i), end=" ") print(f"{_i8(i):02x}", end=" ")
print() print()

View File

@ -63,12 +63,12 @@ class BoxReader:
data = self._read_bytes(size) data = self._read_bytes(size)
return struct.unpack(field_format, data) return struct.unpack(field_format, data)
def read_boxes(self): def read_boxes(self) -> BoxReader:
size = self.remaining_in_box size = self.remaining_in_box
data = self._read_bytes(size) data = self._read_bytes(size)
return BoxReader(io.BytesIO(data), size) return BoxReader(io.BytesIO(data), size)
def has_next_box(self): def has_next_box(self) -> bool:
if self.has_length: if self.has_length:
return self.fp.tell() + self.remaining_in_box < self.length return self.fp.tell() + self.remaining_in_box < self.length
else: else:
@ -215,7 +215,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
format = "JPEG2000" format = "JPEG2000"
format_description = "JPEG 2000 (ISO 15444)" format_description = "JPEG 2000 (ISO 15444)"
def _open(self): def _open(self) -> None:
sig = self.fp.read(4) sig = self.fp.read(4)
if sig == b"\xff\x4f\xff\x51": if sig == b"\xff\x4f\xff\x51":
self.codec = "j2k" self.codec = "j2k"
@ -267,7 +267,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
) )
] ]
def _parse_comment(self): def _parse_comment(self) -> None:
hdr = self.fp.read(2) hdr = self.fp.read(2)
length = _binary.i16be(hdr) length = _binary.i16be(hdr)
self.fp.seek(length - 2, os.SEEK_CUR) self.fp.seek(length - 2, os.SEEK_CUR)

View File

@ -408,7 +408,7 @@ class JpegImageFile(ImageFile.ImageFile):
msg = "no marker found" msg = "no marker found"
raise SyntaxError(msg) raise SyntaxError(msg)
def load_read(self, read_bytes): def load_read(self, read_bytes: int) -> bytes:
""" """
internal: read more image data internal: read more image data
For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker
@ -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
@ -460,7 +462,7 @@ class JpegImageFile(ImageFile.ImageFile):
box = (0, 0, original_size[0] / scale, original_size[1] / scale) box = (0, 0, original_size[0] / scale, original_size[1] / scale)
return self.mode, box return self.mode, box
def load_djpeg(self): def load_djpeg(self) -> None:
# ALTERNATIVE: handle JPEGs via the IJG command line utilities # ALTERNATIVE: handle JPEGs via the IJG command line utilities
f, path = tempfile.mkstemp() f, path = tempfile.mkstemp()

View File

@ -38,7 +38,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
format_description = "Microsoft Image Composer" format_description = "Microsoft Image Composer"
_close_exclusive_fp_after_loading = False _close_exclusive_fp_after_loading = False
def _open(self): def _open(self) -> None:
# read the OLE directory and see if this is a likely # read the OLE directory and see if this is a likely
# to be a Microsoft Image Composer file # to be a Microsoft Image Composer file
@ -88,7 +88,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
def tell(self): def tell(self):
return self.frame return self.frame
def close(self): def close(self) -> None:
self.__fp.close() self.__fp.close()
self.ole.close() self.ole.close()
super().close() super().close()

View File

@ -53,6 +53,10 @@ class BitStream:
return v return v
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"\x00\x00\x01\xb3"
## ##
# Image plugin for MPEG streams. This plugin can identify a stream, # Image plugin for MPEG streams. This plugin can identify a stream,
# but it cannot read it. # but it cannot read it.
@ -77,7 +81,7 @@ class MpegImageFile(ImageFile.ImageFile):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Registry stuff # Registry stuff
Image.register_open(MpegImageFile.format, MpegImageFile) Image.register_open(MpegImageFile.format, MpegImageFile, _accept)
Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"]) Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"])

View File

@ -100,7 +100,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
format_description = "MPO (CIPA DC-007)" format_description = "MPO (CIPA DC-007)"
_close_exclusive_fp_after_loading = False _close_exclusive_fp_after_loading = False
def _open(self): def _open(self) -> None:
self.fp.seek(0) # prep the fp in order to pass the JPEG test self.fp.seek(0) # prep the fp in order to pass the JPEG test
JpegImagePlugin.JpegImageFile._open(self) JpegImagePlugin.JpegImageFile._open(self)
self._after_jpeg_open() self._after_jpeg_open()
@ -124,10 +124,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
# for now we can only handle reading and individual frame extraction # for now we can only handle reading and individual frame extraction
self.readonly = 1 self.readonly = 1
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
self._fp.seek(pos) self._fp.seek(pos)
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
self.fp = self._fp self.fp = self._fp
@ -149,7 +149,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])] self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])]
self.__frame = frame self.__frame = frame
def tell(self): def tell(self) -> int:
return self.__frame return self.__frame
@staticmethod @staticmethod

View File

@ -54,7 +54,7 @@ class PSDraw:
self.fp.write(b"%%EndProlog\n") self.fp.write(b"%%EndProlog\n")
self.isofont = {} self.isofont = {}
def end_document(self): def end_document(self) -> None:
"""Ends printing. (Write PostScript DSC footer.)""" """Ends printing. (Write PostScript DSC footer.)"""
self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n") self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n")
if hasattr(self.fp, "flush"): if hasattr(self.fp, "flush"):

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

@ -87,10 +87,10 @@ class IndirectReferenceTuple(NamedTuple):
class IndirectReference(IndirectReferenceTuple): class IndirectReference(IndirectReferenceTuple):
def __str__(self): def __str__(self) -> str:
return f"{self.object_id} {self.generation} R" return f"{self.object_id} {self.generation} R"
def __bytes__(self): def __bytes__(self) -> bytes:
return self.__str__().encode("us-ascii") return self.__str__().encode("us-ascii")
def __eq__(self, other): def __eq__(self, other):
@ -108,7 +108,7 @@ class IndirectReference(IndirectReferenceTuple):
class IndirectObjectDef(IndirectReference): class IndirectObjectDef(IndirectReference):
def __str__(self): def __str__(self) -> str:
return f"{self.object_id} {self.generation} obj" return f"{self.object_id} {self.generation} obj"
@ -144,15 +144,13 @@ 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):
return key in self.existing_entries or key in self.new_entries return key in self.existing_entries or key in self.new_entries
def __len__(self): def __len__(self) -> int:
return len( return len(
set(self.existing_entries.keys()) set(self.existing_entries.keys())
| set(self.new_entries.keys()) | set(self.new_entries.keys())
@ -213,7 +211,7 @@ class PdfName:
else: else:
self.name = name.encode("us-ascii") self.name = name.encode("us-ascii")
def name_as_str(self): def name_as_str(self) -> str:
return self.name.decode("us-ascii") return self.name.decode("us-ascii")
def __eq__(self, other): def __eq__(self, other):
@ -224,8 +222,8 @@ class PdfName:
def __hash__(self): def __hash__(self):
return hash(self.name) return hash(self.name)
def __repr__(self): def __repr__(self) -> str:
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):
@ -233,7 +231,7 @@ class PdfName:
allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"}
def __bytes__(self): def __bytes__(self) -> bytes:
result = bytearray(b"/") result = bytearray(b"/")
for b in self.name: for b in self.name:
if b in self.allowed_chars: if b in self.allowed_chars:
@ -244,7 +242,7 @@ class PdfName:
class PdfArray(List[Any]): class PdfArray(List[Any]):
def __bytes__(self): def __bytes__(self) -> bytes:
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
@ -288,7 +286,7 @@ class PdfDict(_DictBase):
value = time.gmtime(calendar.timegm(value) + offset) value = time.gmtime(calendar.timegm(value) + offset)
return value return value
def __bytes__(self): def __bytes__(self) -> bytes:
out = bytearray(b"<<") out = bytearray(b"<<")
for key, value in self.items(): for key, value in self.items():
if value is None: if value is None:
@ -306,7 +304,7 @@ class PdfBinary:
def __init__(self, data): def __init__(self, data):
self.data = data self.data = data
def __bytes__(self): def __bytes__(self) -> bytes:
return b"<%s>" % b"".join(b"%02X" % b for b in self.data) return b"<%s>" % b"".join(b"%02X" % b for b in self.data)
@ -411,28 +409,28 @@ class PdfParser:
self.close() self.close()
return False # do not suppress exceptions return False # do not suppress exceptions
def start_writing(self): def start_writing(self) -> None:
self.close_buf() self.close_buf()
self.seek_end() self.seek_end()
def close_buf(self): def close_buf(self) -> None:
try: try:
self.buf.close() self.buf.close()
except AttributeError: except AttributeError:
pass pass
self.buf = None self.buf = None
def close(self): def close(self) -> None:
if self.should_close_buf: if self.should_close_buf:
self.close_buf() self.close_buf()
if self.f is not None and self.should_close_file: if self.f is not None and self.should_close_file:
self.f.close() self.f.close()
self.f = None self.f = None
def seek_end(self): def seek_end(self) -> None:
self.f.seek(0, os.SEEK_END) self.f.seek(0, os.SEEK_END)
def write_header(self): def write_header(self) -> None:
self.f.write(b"%PDF-1.4\n") self.f.write(b"%PDF-1.4\n")
def write_comment(self, s): def write_comment(self, s):
@ -452,7 +450,7 @@ class PdfParser:
) )
return self.root_ref return self.root_ref
def rewrite_pages(self): def rewrite_pages(self) -> None:
pages_tree_nodes_to_delete = [] pages_tree_nodes_to_delete = []
for i, page_ref in enumerate(self.orig_pages): for i, page_ref in enumerate(self.orig_pages):
page_info = self.cached_objects[page_ref] page_info = self.cached_objects[page_ref]
@ -531,7 +529,7 @@ class PdfParser:
f.write(b"endobj\n") f.write(b"endobj\n")
return ref return ref
def del_root(self): def del_root(self) -> None:
if self.root_ref is None: if self.root_ref is None:
return return
del self.xref_table[self.root_ref.object_id] del self.xref_table[self.root_ref.object_id]
@ -549,7 +547,7 @@ class PdfParser:
except ValueError: # cannot mmap an empty file except ValueError: # cannot mmap an empty file
return b"" return b""
def read_pdf_info(self): def read_pdf_info(self) -> None:
self.file_size_total = len(self.buf) self.file_size_total = len(self.buf)
self.file_size_this = self.file_size_total - self.start_offset self.file_size_this = self.file_size_total - self.start_offset
self.read_trailer() self.read_trailer()
@ -825,11 +823,10 @@ class PdfParser:
m = cls.re_stream_start.match(data, offset) m = cls.re_stream_start.match(data, offset)
if m: if m:
try: try:
stream_len = int(result[b"Length"]) stream_len_str = result.get(b"Length")
except (TypeError, KeyError, ValueError) as e: stream_len = int(stream_len_str)
msg = "bad or missing Length in stream dict (%r)" % result.get( except (TypeError, ValueError) as e:
b"Length", None msg = f"bad or missing Length in stream dict ({stream_len_str})"
)
raise PdfFormatError(msg) from e raise PdfFormatError(msg) from e
stream_data = data[m.end() : m.end() + stream_len] stream_data = data[m.end() : m.end() + stream_len]
m = cls.re_stream_end.match(data, m.end() + stream_len) m = cls.re_stream_end.match(data, m.end() + stream_len)
@ -884,7 +881,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

@ -39,6 +39,7 @@ import struct
import warnings import warnings
import zlib import zlib
from enum import IntEnum from enum import IntEnum
from typing import IO
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -149,14 +150,15 @@ def _crc32(data, seed=0):
class ChunkStream: class ChunkStream:
def __init__(self, fp): def __init__(self, fp: IO[bytes]) -> None:
self.fp = fp self.fp: IO[bytes] | None = fp
self.queue = [] self.queue: list[tuple[bytes, int, int]] | None = []
def read(self): def read(self) -> tuple[bytes, int, int]:
"""Fetch a new chunk. Returns header information.""" """Fetch a new chunk. Returns header information."""
cid = None cid = None
assert self.fp is not None
if self.queue: if self.queue:
cid, pos, length = self.queue.pop() cid, pos, length = self.queue.pop()
self.fp.seek(pos) self.fp.seek(pos)
@ -173,25 +175,26 @@ class ChunkStream:
return cid, pos, length return cid, pos, length
def __enter__(self): def __enter__(self) -> ChunkStream:
return self return self
def __exit__(self, *args): def __exit__(self, *args):
self.close() self.close()
def close(self): def close(self) -> None:
self.queue = self.fp = None self.queue = self.fp = None
def push(self, cid, pos, length): def push(self, cid: bytes, pos: int, length: int) -> None:
assert self.queue is not None
self.queue.append((cid, pos, length)) self.queue.append((cid, pos, length))
def call(self, cid, pos, length): def call(self, cid, pos, length):
"""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: bytes, data: bytes) -> None:
"""Read and verify checksum""" """Read and verify checksum"""
# Skip CRC checks for ancillary chunks if allowed to load truncated # Skip CRC checks for ancillary chunks if allowed to load truncated
@ -201,6 +204,7 @@ class ChunkStream:
self.crc_skip(cid, data) self.crc_skip(cid, data)
return return
assert self.fp is not None
try: try:
crc1 = _crc32(data, _crc32(cid)) crc1 = _crc32(data, _crc32(cid))
crc2 = i32(self.fp.read(4)) crc2 = i32(self.fp.read(4))
@ -211,12 +215,13 @@ class ChunkStream:
msg = f"broken PNG file (incomplete checksum in {repr(cid)})" msg = f"broken PNG file (incomplete checksum in {repr(cid)})"
raise SyntaxError(msg) from e raise SyntaxError(msg) from e
def crc_skip(self, cid, data): def crc_skip(self, cid: bytes, data: bytes) -> None:
"""Read checksum""" """Read checksum"""
assert self.fp is not None
self.fp.read(4) self.fp.read(4)
def verify(self, endchunk=b"IEND"): def verify(self, endchunk: bytes = b"IEND") -> list[bytes]:
# Simple approach; just calculate checksum for all remaining # Simple approach; just calculate checksum for all remaining
# blocks. Must be called directly after open. # blocks. Must be called directly after open.
@ -361,7 +366,7 @@ class PngStream(ChunkStream):
self.text_memory = 0 self.text_memory = 0
def check_text_memory(self, chunklen): def check_text_memory(self, chunklen: int) -> None:
self.text_memory += chunklen self.text_memory += chunklen
if self.text_memory > MAX_TEXT_MEMORY: if self.text_memory > MAX_TEXT_MEMORY:
msg = ( msg = (
@ -370,19 +375,19 @@ class PngStream(ChunkStream):
) )
raise ValueError(msg) raise ValueError(msg)
def save_rewind(self): def save_rewind(self) -> None:
self.rewind_state = { self.rewind_state = {
"info": self.im_info.copy(), "info": self.im_info.copy(),
"tile": self.im_tile, "tile": self.im_tile,
"seq_num": self._seq_num, "seq_num": self._seq_num,
} }
def rewind(self): def rewind(self) -> None:
self.im_info = self.rewind_state["info"].copy() self.im_info = self.rewind_state["info"].copy()
self.im_tile = self.rewind_state["tile"] self.im_tile = self.rewind_state["tile"]
self._seq_num = self.rewind_state["seq_num"] self._seq_num = self.rewind_state["seq_num"]
def chunk_iCCP(self, pos, length): def chunk_iCCP(self, pos: int, length: int) -> bytes:
# ICC profile # ICC profile
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
# according to PNG spec, the iCCP chunk contains: # according to PNG spec, the iCCP chunk contains:
@ -409,7 +414,7 @@ class PngStream(ChunkStream):
self.im_info["icc_profile"] = icc_profile self.im_info["icc_profile"] = icc_profile
return s return s
def chunk_IHDR(self, pos, length): def chunk_IHDR(self, pos: int, length: int) -> bytes:
# image header # image header
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
if length < 13: if length < 13:
@ -446,14 +451,14 @@ class PngStream(ChunkStream):
msg = "end of PNG image" msg = "end of PNG image"
raise EOFError(msg) raise EOFError(msg)
def chunk_PLTE(self, pos, length): def chunk_PLTE(self, pos: int, length: int) -> bytes:
# palette # palette
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P": if self.im_mode == "P":
self.im_palette = "RGB", s self.im_palette = "RGB", s
return s return s
def chunk_tRNS(self, pos, length): def chunk_tRNS(self, pos: int, length: int) -> bytes:
# transparency # transparency
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
if self.im_mode == "P": if self.im_mode == "P":
@ -473,13 +478,13 @@ class PngStream(ChunkStream):
self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4) self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4)
return s return s
def chunk_gAMA(self, pos, length): def chunk_gAMA(self, pos: int, length: int) -> bytes:
# gamma setting # gamma setting
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
self.im_info["gamma"] = i32(s) / 100000.0 self.im_info["gamma"] = i32(s) / 100000.0
return s return s
def chunk_cHRM(self, pos, length): def chunk_cHRM(self, pos: int, length: int) -> bytes:
# chromaticity, 8 unsigned ints, actual value is scaled by 100,000 # chromaticity, 8 unsigned ints, actual value is scaled by 100,000
# WP x,y, Red x,y, Green x,y Blue x,y # WP x,y, Red x,y, Green x,y Blue x,y
@ -488,7 +493,7 @@ class PngStream(ChunkStream):
self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals)
return s return s
def chunk_sRGB(self, pos, length): def chunk_sRGB(self, pos: int, length: int) -> bytes:
# srgb rendering intent, 1 byte # srgb rendering intent, 1 byte
# 0 perceptual # 0 perceptual
# 1 relative colorimetric # 1 relative colorimetric
@ -504,7 +509,7 @@ class PngStream(ChunkStream):
self.im_info["srgb"] = s[0] self.im_info["srgb"] = s[0]
return s return s
def chunk_pHYs(self, pos, length): def chunk_pHYs(self, pos: int, length: int) -> bytes:
# pixels per unit # pixels per unit
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
if length < 9: if length < 9:
@ -521,7 +526,7 @@ class PngStream(ChunkStream):
self.im_info["aspect"] = px, py self.im_info["aspect"] = px, py
return s return s
def chunk_tEXt(self, pos, length): def chunk_tEXt(self, pos: int, length: int) -> bytes:
# text # text
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
try: try:
@ -540,7 +545,7 @@ class PngStream(ChunkStream):
return s return s
def chunk_zTXt(self, pos, length): def chunk_zTXt(self, pos: int, length: int) -> bytes:
# compressed text # compressed text
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
try: try:
@ -574,7 +579,7 @@ class PngStream(ChunkStream):
return s return s
def chunk_iTXt(self, pos, length): def chunk_iTXt(self, pos: int, length: int) -> bytes:
# international text # international text
r = s = ImageFile._safe_read(self.fp, length) r = s = ImageFile._safe_read(self.fp, length)
try: try:
@ -614,13 +619,13 @@ class PngStream(ChunkStream):
return s return s
def chunk_eXIf(self, pos, length): def chunk_eXIf(self, pos: int, length: int) -> bytes:
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
self.im_info["exif"] = b"Exif\x00\x00" + s self.im_info["exif"] = b"Exif\x00\x00" + s
return s return s
# APNG chunks # APNG chunks
def chunk_acTL(self, pos, length): def chunk_acTL(self, pos: int, length: int) -> bytes:
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
if length < 8: if length < 8:
if ImageFile.LOAD_TRUNCATED_IMAGES: if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -640,7 +645,7 @@ class PngStream(ChunkStream):
self.im_custom_mimetype = "image/apng" self.im_custom_mimetype = "image/apng"
return s return s
def chunk_fcTL(self, pos, length): def chunk_fcTL(self, pos: int, length: int) -> bytes:
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
if length < 26: if length < 26:
if ImageFile.LOAD_TRUNCATED_IMAGES: if ImageFile.LOAD_TRUNCATED_IMAGES:
@ -669,7 +674,7 @@ class PngStream(ChunkStream):
self.im_info["blend"] = s[25] self.im_info["blend"] = s[25]
return s return s
def chunk_fdAT(self, pos, length): def chunk_fdAT(self, pos: int, length: int) -> bytes:
if length < 4: if length < 4:
if ImageFile.LOAD_TRUNCATED_IMAGES: if ImageFile.LOAD_TRUNCATED_IMAGES:
s = ImageFile._safe_read(self.fp, length) s = ImageFile._safe_read(self.fp, length)
@ -701,7 +706,7 @@ class PngImageFile(ImageFile.ImageFile):
format = "PNG" format = "PNG"
format_description = "Portable network graphics" format_description = "Portable network graphics"
def _open(self): def _open(self) -> None:
if not _accept(self.fp.read(8)): if not _accept(self.fp.read(8)):
msg = "not a PNG file" msg = "not a PNG file"
raise SyntaxError(msg) raise SyntaxError(msg)
@ -711,8 +716,8 @@ class PngImageFile(ImageFile.ImageFile):
# #
# Parse headers up to the first IDAT or fDAT chunk # Parse headers up to the first IDAT or fDAT chunk
self.private_chunks = [] self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = []
self.png = PngStream(self.fp) self.png: PngStream | None = PngStream(self.fp)
while True: while True:
# #
@ -783,7 +788,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:
@ -793,6 +798,7 @@ class PngImageFile(ImageFile.ImageFile):
# back up to beginning of IDAT block # back up to beginning of IDAT block
self.fp.seek(self.tile[0][2] - 8) self.fp.seek(self.tile[0][2] - 8)
assert self.png is not None
self.png.verify() self.png.verify()
self.png.close() self.png.close()
@ -800,7 +806,7 @@ class PngImageFile(ImageFile.ImageFile):
self.fp.close() self.fp.close()
self.fp = None self.fp = None
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if frame < self.__frame: if frame < self.__frame:
@ -909,10 +915,10 @@ class PngImageFile(ImageFile.ImageFile):
else: else:
self.dispose = None self.dispose = None
def tell(self): def tell(self) -> int:
return self.__frame return self.__frame
def load_prepare(self): def load_prepare(self) -> None:
"""internal: prepare to read PNG file""" """internal: prepare to read PNG file"""
if self.info.get("interlace"): if self.info.get("interlace"):
@ -921,9 +927,10 @@ class PngImageFile(ImageFile.ImageFile):
self.__idat = self.__prepare_idat # used by load_read() self.__idat = self.__prepare_idat # used by load_read()
ImageFile.ImageFile.load_prepare(self) ImageFile.ImageFile.load_prepare(self)
def load_read(self, read_bytes): def load_read(self, read_bytes: int) -> bytes:
"""internal: read more image data""" """internal: read more image data"""
assert self.png is not None
while self.__idat == 0: while self.__idat == 0:
# end of chunk, skip forward to next one # end of chunk, skip forward to next one
@ -954,8 +961,9 @@ class PngImageFile(ImageFile.ImageFile):
return self.fp.read(read_bytes) return self.fp.read(read_bytes)
def load_end(self): def load_end(self) -> None:
"""internal: finished reading image data""" """internal: finished reading image data"""
assert self.png is not None
if self.__idat != 0: if self.__idat != 0:
self.fp.read(self.__idat) self.fp.read(self.__idat)
while True: while True:
@ -1079,7 +1087,7 @@ class _idat:
self.fp = fp self.fp = fp
self.chunk = chunk self.chunk = chunk
def write(self, data): def write(self, data: bytes) -> None:
self.chunk(self.fp, b"IDAT", data) self.chunk(self.fp, b"IDAT", data)
@ -1091,7 +1099,7 @@ class _fdat:
self.chunk = chunk self.chunk = chunk
self.seq_num = seq_num self.seq_num = seq_num
def write(self, data): def write(self, data: bytes) -> None:
self.chunk(self.fp, b"fdAT", o32(self.seq_num), data) self.chunk(self.fp, b"fdAT", o32(self.seq_num), data)
self.seq_num += 1 self.seq_num += 1
@ -1436,10 +1444,10 @@ def getchunks(im, **params):
class collector: class collector:
data = [] data = []
def write(self, data): def write(self, data: bytes) -> None:
pass pass
def append(self, chunk): def append(self, chunk: bytes) -> None:
self.data.append(chunk) self.data.append(chunk)
def append(fp, cid, *data): def append(fp, cid, *data):

View File

@ -57,7 +57,7 @@ class PsdImageFile(ImageFile.ImageFile):
format_description = "Adobe Photoshop" format_description = "Adobe Photoshop"
_close_exclusive_fp_after_loading = False _close_exclusive_fp_after_loading = False
def _open(self): def _open(self) -> None:
read = self.fp.read read = self.fp.read
# #
@ -141,23 +141,22 @@ class PsdImageFile(ImageFile.ImageFile):
self.frame = 1 self.frame = 1
self._min_frame = 1 self._min_frame = 1
def seek(self, layer): def seek(self, layer: int) -> None:
if not self._seek_check(layer): if not self._seek_check(layer):
return return
# seek to given layer (1..max) # seek to given layer (1..max)
try: try:
name, mode, bbox, tile = self.layers[layer - 1] _, mode, _, tile = self.layers[layer - 1]
self._mode = mode self._mode = mode
self.tile = tile self.tile = tile
self.frame = layer self.frame = layer
self.fp = self._fp self.fp = self._fp
return name, bbox
except IndexError as e: except IndexError as e:
msg = "no such layer" msg = "no such layer"
raise EOFError(msg) from e raise EOFError(msg) from e
def tell(self): def tell(self) -> int:
# return layer number (0=image, 1..max=layers) # return layer number (0=image, 1..max=layers)
return self.frame return self.frame

View File

@ -70,7 +70,7 @@ class PyAccess:
# logger.debug("%s", vals) # logger.debug("%s", vals)
self._post_init() self._post_init()
def _post_init(self): def _post_init(self) -> None:
pass pass
def __setitem__(self, xy, color): def __setitem__(self, xy, color):

View File

@ -21,7 +21,7 @@ class QoiImageFile(ImageFile.ImageFile):
format = "QOI" format = "QOI"
format_description = "Quite OK Image" format_description = "Quite OK Image"
def _open(self): def _open(self) -> None:
if not _accept(self.fp.read(4)): if not _accept(self.fp.read(4)):
msg = "not a QOI file" msg = "not a QOI file"
raise SyntaxError(msg) raise SyntaxError(msg)

View File

@ -37,6 +37,7 @@ from __future__ import annotations
import os import os
import struct import struct
import sys import sys
from typing import TYPE_CHECKING
from . import Image, ImageFile from . import Image, ImageFile
@ -97,7 +98,7 @@ class SpiderImageFile(ImageFile.ImageFile):
format_description = "Spider 2D image" format_description = "Spider 2D image"
_close_exclusive_fp_after_loading = False _close_exclusive_fp_after_loading = False
def _open(self): def _open(self) -> None:
# check header # check header
n = 27 * 4 # read 27 float values n = 27 * 4 # read 27 float values
f = self.fp.read(n) f = self.fp.read(n)
@ -157,21 +158,21 @@ class SpiderImageFile(ImageFile.ImageFile):
self._fp = self.fp # FIXME: hack self._fp = self.fp # FIXME: hack
@property @property
def n_frames(self): def n_frames(self) -> int:
return self._nimages return self._nimages
@property @property
def is_animated(self): def is_animated(self) -> bool:
return self._nimages > 1 return self._nimages > 1
# 1st image index is zero (although SPIDER imgnumber starts at 1) # 1st image index is zero (although SPIDER imgnumber starts at 1)
def tell(self): def tell(self) -> int:
if self.imgnumber < 1: if self.imgnumber < 1:
return 0 return 0
else: else:
return self.imgnumber - 1 return self.imgnumber - 1
def seek(self, frame): def seek(self, frame: int) -> None:
if self.istack == 0: if self.istack == 0:
msg = "attempt to seek in a non-stack file" msg = "attempt to seek in a non-stack file"
raise EOFError(msg) raise EOFError(msg)
@ -191,8 +192,11 @@ class SpiderImageFile(ImageFile.ImageFile):
b = -m * minimum b = -m * minimum
return self.point(lambda i, m=m, b=b: i * m + b).convert("L") return self.point(lambda i, m=m, b=b: i * m + b).convert("L")
if TYPE_CHECKING:
from . import ImageTk
# returns a ImageTk.PhotoImage object, after rescaling to 0..255 # returns a ImageTk.PhotoImage object, after rescaling to 0..255
def tkPhotoImage(self): def tkPhotoImage(self) -> ImageTk.PhotoImage:
from . import ImageTk from . import ImageTk
return ImageTk.PhotoImage(self.convert2byte(), palette=256) return ImageTk.PhotoImage(self.convert2byte(), palette=256)
@ -218,7 +222,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 +303,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
@ -376,7 +381,7 @@ class IFDRational(Rational):
f = self._val.limit_denominator(max_denominator) f = self._val.limit_denominator(max_denominator)
return f.numerator, f.denominator return f.numerator, f.denominator
def __repr__(self): def __repr__(self) -> str:
return str(float(self._val)) return str(float(self._val))
def __hash__(self): def __hash__(self):
@ -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: (
@ -598,7 +603,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
self._next = None self._next = None
self._offset = None self._offset = None
def __str__(self): def __str__(self) -> str:
return str(dict(self)) return str(dict(self))
def named(self): def named(self):
@ -612,7 +617,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
for code, value in self.items() for code, value in self.items()
} }
def __len__(self): def __len__(self) -> int:
return len(set(self._tagdata) | set(self._tags_v2)) return len(set(self._tagdata) | set(self._tags_v2))
def __getitem__(self, tag): def __getitem__(self, tag):
@ -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
@ -1036,7 +1041,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
ifd.next = original.next # an indicator for multipage tiffs ifd.next = original.next # an indicator for multipage tiffs
return ifd return ifd
def to_v2(self): def to_v2(self) -> ImageFileDirectory_v2:
"""Returns an """Returns an
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
instance with the same data as is contained in the original instance with the same data as is contained in the original
@ -1056,7 +1061,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
def __contains__(self, tag): def __contains__(self, tag):
return tag in self._tags_v1 or tag in self._tagdata return tag in self._tags_v1 or tag in self._tagdata
def __len__(self): def __len__(self) -> int:
return len(set(self._tagdata) | set(self._tags_v1)) return len(set(self._tagdata) | set(self._tags_v1))
def __iter__(self): def __iter__(self):
@ -1138,7 +1143,7 @@ class TiffImageFile(ImageFile.ImageFile):
self.seek(current) self.seek(current)
return self._n_frames return self._n_frames
def seek(self, frame): def seek(self, frame: int) -> None:
"""Select a given frame as current image""" """Select a given frame as current image"""
if not self._seek_check(frame): if not self._seek_check(frame):
return return
@ -1149,7 +1154,7 @@ class TiffImageFile(ImageFile.ImageFile):
Image._decompression_bomb_check(self.size) Image._decompression_bomb_check(self.size)
self.im = Image.core.new(self.mode, self.size) self.im = Image.core.new(self.mode, self.size)
def _seek(self, frame): def _seek(self, frame: int) -> None:
self.fp = self._fp self.fp = self._fp
# reset buffered io handle in case fp # reset buffered io handle in case fp
@ -1193,7 +1198,7 @@ class TiffImageFile(ImageFile.ImageFile):
self.__frame = frame self.__frame = frame
self._setup() self._setup()
def tell(self): def tell(self) -> int:
"""Return the current frame number""" """Return the current frame number"""
return self.__frame return self.__frame
@ -1232,7 +1237,7 @@ class TiffImageFile(ImageFile.ImageFile):
return self._load_libtiff() return self._load_libtiff()
return super().load() return super().load()
def load_end(self): def load_end(self) -> None:
# allow closing if we're on the first frame, there's no next # allow closing if we're on the first frame, there's no next
# This is the ImageFile.load path only, libtiff specific below. # This is the ImageFile.load path only, libtiff specific below.
if not self.is_animated: if not self.is_animated:
@ -1937,7 +1942,7 @@ class AppendingTiffWriter:
self.beginning = self.f.tell() self.beginning = self.f.tell()
self.setup() self.setup()
def setup(self): def setup(self) -> None:
# Reset everything. # Reset everything.
self.f.seek(self.beginning, os.SEEK_SET) self.f.seek(self.beginning, os.SEEK_SET)
@ -1962,7 +1967,7 @@ class AppendingTiffWriter:
self.skipIFDs() self.skipIFDs()
self.goToEnd() self.goToEnd()
def finalize(self): def finalize(self) -> None:
if self.isFirst: if self.isFirst:
return return
@ -1985,7 +1990,7 @@ class AppendingTiffWriter:
self.f.seek(ifd_offset) self.f.seek(ifd_offset)
self.fixIFD() self.fixIFD()
def newFrame(self): def newFrame(self) -> None:
# Call this to finish a frame. # Call this to finish a frame.
self.finalize() self.finalize()
self.setup() self.setup()
@ -1998,7 +2003,7 @@ class AppendingTiffWriter:
self.close() self.close()
return False return False
def tell(self): def tell(self) -> int:
return self.f.tell() - self.offsetOfNewPage return self.f.tell() - self.offsetOfNewPage
def seek(self, offset, whence=io.SEEK_SET): def seek(self, offset, whence=io.SEEK_SET):
@ -2008,7 +2013,7 @@ class AppendingTiffWriter:
self.f.seek(offset, whence) self.f.seek(offset, whence)
return self.tell() return self.tell()
def goToEnd(self): def goToEnd(self) -> None:
self.f.seek(0, os.SEEK_END) self.f.seek(0, os.SEEK_END)
pos = self.f.tell() pos = self.f.tell()
@ -2020,11 +2025,11 @@ 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) -> None:
while True: while True:
ifd_offset = self.readLong() ifd_offset = self.readLong()
if ifd_offset == 0: if ifd_offset == 0:
@ -2079,11 +2084,11 @@ class AppendingTiffWriter:
msg = f"wrote only {bytes_written} bytes but wanted 4" msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg) raise RuntimeError(msg)
def close(self): def close(self) -> None:
self.finalize() self.finalize()
self.f.close() self.f.close()
def fixIFD(self): def fixIFD(self) -> None:
num_tags = self.readShort() num_tags = self.readShort()
for i in range(num_tags): for i in range(num_tags):

View File

@ -32,7 +32,7 @@ class WalImageFile(ImageFile.ImageFile):
format = "WAL" format = "WAL"
format_description = "Quake2 Texture" format_description = "Quake2 Texture"
def _open(self): def _open(self) -> None:
self._mode = "P" self._mode = "P"
# read header fields # read header fields

View File

@ -43,7 +43,7 @@ class WebPImageFile(ImageFile.ImageFile):
__loaded = 0 __loaded = 0
__logical_frame = 0 __logical_frame = 0
def _open(self): def _open(self) -> None:
if not _webp.HAVE_WEBPANIM: if not _webp.HAVE_WEBPANIM:
# Legacy mode # Legacy mode
data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode( data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode(
@ -109,7 +109,7 @@ class WebPImageFile(ImageFile.ImageFile):
""" """
return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {} return self._getxmp(self.info["xmp"]) if "xmp" in self.info else {}
def seek(self, frame): def seek(self, frame: int) -> None:
if not self._seek_check(frame): if not self._seek_check(frame):
return return
@ -144,7 +144,7 @@ class WebPImageFile(ImageFile.ImageFile):
timestamp -= duration timestamp -= duration
return data, timestamp, duration return data, timestamp, duration
def _seek(self, frame): def _seek(self, frame: int) -> None:
if self.__physical_frame == frame: if self.__physical_frame == frame:
return # Nothing to do return # Nothing to do
if frame < self.__physical_frame: if frame < self.__physical_frame:
@ -171,10 +171,10 @@ class WebPImageFile(ImageFile.ImageFile):
return super().load() return super().load()
def load_seek(self, pos): def load_seek(self, pos: int) -> None:
pass pass
def tell(self): def tell(self) -> int:
if not _webp.HAVE_WEBPANIM: if not _webp.HAVE_WEBPANIM:
return super().tell() return super().tell()

View File

@ -79,7 +79,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
format = "WMF" format = "WMF"
format_description = "Windows Metafile" format_description = "Windows Metafile"
def _open(self): def _open(self) -> None:
self._inch = None self._inch = None
# check placable header # check placable header

View File

@ -36,7 +36,7 @@ class XpmImageFile(ImageFile.ImageFile):
format = "XPM" format = "XPM"
format_description = "X11 Pixel Map" format_description = "X11 Pixel Map"
def _open(self): def _open(self) -> None:
if not _accept(self.fp.read(9)): if not _accept(self.fp.read(9)):
msg = "not an XPM file" msg = "not an XPM file"
raise SyntaxError(msg) raise SyntaxError(msg)
@ -103,16 +103,13 @@ class XpmImageFile(ImageFile.ImageFile):
self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))]
def load_read(self, read_bytes): def load_read(self, read_bytes: int) -> bytes:
# #
# load all image data in one chunk # load all image data in one chunk
xsize, ysize = self.size xsize, ysize = self.size
s = [None] * ysize s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)]
for i in range(ysize):
s[i] = self.fp.readline()[1 : xsize + 1].ljust(xsize)
return b"".join(s) return b"".join(s)

View File

@ -18,7 +18,7 @@ modules = {
} }
def check_module(feature): def check_module(feature: str) -> bool:
""" """
Checks if a module is available. Checks if a module is available.
@ -42,7 +42,7 @@ def check_module(feature):
return False return False
def version_module(feature): def version_module(feature: str) -> str | None:
""" """
:param feature: The module to check for. :param feature: The module to check for.
:returns: :returns:
@ -54,13 +54,10 @@ def version_module(feature):
module, ver = modules[feature] module, ver = modules[feature]
if ver is None:
return None
return getattr(__import__(module, fromlist=[ver]), ver) return getattr(__import__(module, fromlist=[ver]), ver)
def get_supported_modules(): def get_supported_modules() -> list[str]:
""" """
:returns: A list of all supported modules. :returns: A list of all supported modules.
""" """
@ -75,7 +72,7 @@ codecs = {
} }
def check_codec(feature): def check_codec(feature: str) -> bool:
""" """
Checks if a codec is available. Checks if a codec is available.
@ -89,10 +86,10 @@ 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: str) -> str | None:
""" """
:param feature: The codec to check for. :param feature: The codec to check for.
:returns: :returns:
@ -105,7 +102,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]
@ -113,7 +110,7 @@ def version_codec(feature):
return version return version
def get_supported_codecs(): def get_supported_codecs() -> list[str]:
""" """
:returns: A list of all supported codecs. :returns: A list of all supported codecs.
""" """
@ -133,7 +130,7 @@ features = {
} }
def check_feature(feature): def check_feature(feature: str) -> bool | None:
""" """
Checks if a feature is available. Checks if a feature is available.
@ -157,7 +154,7 @@ def check_feature(feature):
return None return None
def version_feature(feature): def version_feature(feature: str) -> str | None:
""" """
:param feature: The feature to check for. :param feature: The feature to check for.
:returns: The version number as a string, or ``None`` if not available. :returns: The version number as a string, or ``None`` if not available.
@ -174,14 +171,14 @@ def version_feature(feature):
return getattr(__import__(module, fromlist=[ver]), ver) return getattr(__import__(module, fromlist=[ver]), ver)
def get_supported_features(): def get_supported_features() -> list[str]:
""" """
:returns: A list of all supported features. :returns: A list of all supported features.
""" """
return [f for f in features if check_feature(f)] return [f for f in features if check_feature(f)]
def check(feature): def check(feature: str) -> bool | None:
""" """
:param feature: A module, codec, or feature name. :param feature: A module, codec, or feature name.
:returns: :returns:
@ -199,7 +196,7 @@ def check(feature):
return False return False
def version(feature): def version(feature: str) -> str | None:
""" """
:param feature: :param feature:
The module, codec, or feature to check for. The module, codec, or feature to check for.
@ -215,7 +212,7 @@ def version(feature):
return None return None
def get_supported(): def get_supported() -> list[str]:
""" """
:returns: A list of all supported modules, features, and codecs. :returns: A list of all supported modules, features, and codecs.
""" """

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