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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ jobs:
- name: Build and Run Valgrind
run: |
# The Pillow user in the docker container is UID 1000
sudo chown -R 1000 $GITHUB_WORKSPACE
# The Pillow user in the docker container is UID 1001
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 }}
sudo chown -R runner $GITHUB_WORKSPACE

View File

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

View File

@ -57,9 +57,9 @@ jobs:
- python-version: "3.10"
PYTHONOPTIMIZE: 2
# M1 only available for 3.10+
- os: "macos-latest"
- os: "macos-13"
python-version: "3.9"
- os: "macos-latest"
- os: "macos-13"
python-version: "3.8"
exclude:
- os: "macos-14"
@ -150,11 +150,12 @@ jobs:
.ci/after_success.sh
- name: Upload coverage
uses: codecov/codecov-action@v3.1.5
uses: codecov/codecov-action@v4
with:
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
gcov: true
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success:
permissions:

View File

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

View File

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

View File

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

View File

@ -5,6 +5,24 @@ Changelog (Pillow)
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
[Cirras, radarhere]

View File

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

View File

@ -273,7 +273,18 @@ def _cached_hopper(mode: str) -> Image.Image:
im = hopper("L")
else:
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:

View File

@ -37,6 +37,8 @@ def test_version() -> None:
else:
assert function(name) == version
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)
for module in features.modules:

View File

@ -336,9 +336,7 @@ def test_readline_psfile(tmp_path: Path) -> None:
strings = ["something", "else", "baz", "bif"]
def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
ending = "Failure with line ending: %s" % (
"".join("%s" % ord(s) for s in ending)
)
ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}"
assert t.readline().strip("\r\n") == "something", ending
assert t.readline().strip("\r\n") == "else", 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"
@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
# any sense, so we're running up against limits where we're asking
# libtiff to do stupid things.
@ -236,13 +238,28 @@ class TestFileLibTiff(LibTiffTestCase):
del new_ifd[338]
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
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):
value: Any
type: int
@ -281,53 +298,43 @@ class TestFileLibTiff(LibTiffTestCase):
)
}
libtiffs = [False]
if Image.core.libtiff_support_custom_tags:
libtiffs.append(True)
def check_tags(
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
) -> None:
im = hopper()
for libtiff in libtiffs:
TiffImagePlugin.WRITE_LIBTIFF = libtiff
out = str(tmp_path / "temp.tif")
im.save(out, tiffinfo=tiffinfo)
def check_tags(
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
) -> None:
im = hopper()
with Image.open(out) as reloaded:
for tag, value in tiffinfo.items():
reloaded_value = reloaded.tag_v2[tag]
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")
im.save(out, tiffinfo=tiffinfo)
assert reloaded_value == value
with Image.open(out) as reloaded:
for tag, value in tiffinfo.items():
reloaded_value = reloaded.tag_v2[tag]
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
# Test with types
ifd = TiffImagePlugin.ImageFileDirectory_v2()
for tag, tagdata in custom.items():
ifd[tag] = tagdata.value
ifd.tagtype[tag] = tagdata.type
check_tags(ifd)
assert reloaded_value == value
# Test with types
ifd = TiffImagePlugin.ImageFileDirectory_v2()
for tag, tagdata in custom.items():
ifd[tag] = tagdata.value
ifd.tagtype[tag] = tagdata.type
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
# 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
}
)
def test_osubfiletype(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
@ -343,24 +350,24 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not segfault
im.save(outfile)
def test_xmlpacket_tag(self, tmp_path: Path) -> None:
TiffImagePlugin.WRITE_LIBTIFF = True
def test_xmlpacket_tag(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
out = str(tmp_path / "temp.tif")
hopper().save(out, tiffinfo={700: b"xmlpacket tag"})
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded:
if 700 in reloaded.tag_v2:
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
im = hopper("RGB")
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out, dpi=(72, 72))
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded:
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[269][0]
def test_12bit_rawmode(self) -> None:
def test_12bit_rawmode(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Are we generating the same interpretation
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:
im.load()
TiffImagePlugin.READ_LIBTIFF = False
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", False)
# to make the target --
# convert 12bit.cropped.tif -depth 16 tmp.tif
# convert tmp.tif -evaluate RightShift 4 12in16bit2.tif
@ -514,12 +521,13 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, out)
@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")
TiffImagePlugin.WRITE_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out)
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded:
# colormap/palette tag
@ -548,9 +556,9 @@ class TestFileLibTiff(LibTiffTestCase):
with pytest.raises(OSError):
os.close(fn)
def test_multipage(self) -> None:
def test_multipage(self, monkeypatch: pytest.MonkeyPatch) -> None:
# issue #862
TiffImagePlugin.READ_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/multipage.tiff") as im:
# 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.convert("RGB").getpixel((0, 0)) == (0, 0, 255)
TiffImagePlugin.READ_LIBTIFF = False
def test_multipage_nframes(self) -> None:
def test_multipage_nframes(self, monkeypatch: pytest.MonkeyPatch) -> None:
# issue #862
TiffImagePlugin.READ_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/multipage.tiff") as im:
frames = im.n_frames
assert frames == 3
@ -582,10 +588,8 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not raise ValueError: I/O operation on closed file
im.load()
TiffImagePlugin.READ_LIBTIFF = False
def test_multipage_seek_backwards(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True
def test_multipage_seek_backwards(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/multipage.tiff") as im:
im.seek(1)
im.load()
@ -593,24 +597,21 @@ class TestFileLibTiff(LibTiffTestCase):
im.seek(0)
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
TiffImagePlugin.READ_LIBTIFF = False
def test__next(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True
def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/hopper.tif") as im:
assert not im.tag.next
im.load()
assert not im.tag.next
def test_4bit(self) -> None:
def test_4bit(self, monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange
test_file = "Tests/images/hopper_gray_4bpp.tif"
original = hopper("L")
# Act
TiffImagePlugin.READ_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open(test_file) as im:
TiffImagePlugin.READ_LIBTIFF = False
# Assert
assert im.size == (128, 128)
@ -650,12 +651,12 @@ class TestFileLibTiff(LibTiffTestCase):
assert im2.mode == "L"
assert_image_equal(im, im2)
def test_save_bytesio(self) -> None:
def test_save_bytesio(self, monkeypatch: pytest.MonkeyPatch) -> None:
# PR 1011
# Test TIFF saving to io.BytesIO() object.
TiffImagePlugin.WRITE_LIBTIFF = True
TiffImagePlugin.READ_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
# Generate test image
pilim = hopper()
@ -672,9 +673,6 @@ class TestFileLibTiff(LibTiffTestCase):
save_bytesio("packbits")
save_bytesio("tiff_lzw")
TiffImagePlugin.WRITE_LIBTIFF = False
TiffImagePlugin.READ_LIBTIFF = False
def test_save_ycbcr(self, tmp_path: Path) -> None:
im = hopper("YCbCr")
outfile = str(tmp_path / "temp.tif")
@ -694,15 +692,16 @@ class TestFileLibTiff(LibTiffTestCase):
if Image.core.libtiff_support_custom_tags:
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
with Image.open("Tests/images/rdf.tif") as im:
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
# this shouldn't crash
im.save(out, format="TIFF")
TiffImagePlugin.WRITE_LIBTIFF = False
def test_page_number_x_0(self, tmp_path: Path) -> None:
# Issue 973
@ -733,36 +732,41 @@ class TestFileLibTiff(LibTiffTestCase):
# Should not raise PermissionError.
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:
icc = img.info.get("icc_profile")
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:
icc_libtiff = img.info.get("icc_profile")
assert icc_libtiff is not None
TiffImagePlugin.READ_LIBTIFF = False
assert icc == icc_libtiff
def test_write_icc(self, tmp_path: Path) -> None:
def check_write(libtiff: bool) -> None:
TiffImagePlugin.WRITE_LIBTIFF = libtiff
@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_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:
icc_profile = img.info["icc_profile"]
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
icc_profile = img.info["icc_profile"]
out = str(tmp_path / "temp.tif")
img.save(out, icc_profile=icc_profile)
with Image.open(out) as reloaded:
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)
out = str(tmp_path / "temp.tif")
img.save(out, icc_profile=icc_profile)
with Image.open(out) as reloaded:
assert icc_profile == reloaded.info["icc_profile"]
def test_multipage_compression(self) -> None:
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")
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))
out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
im.save(out)
TiffImagePlugin.WRITE_LIBTIFF = False
with Image.open(out) as reloaded:
assert reloaded.mode == "F"
@ -1091,15 +1096,14 @@ class TestFileLibTiff(LibTiffTestCase):
with Image.open(out) as im:
im.load()
def test_realloc_overflow(self) -> None:
TiffImagePlugin.READ_LIBTIFF = True
def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im:
with pytest.raises(OSError) as e:
im.load()
# Assert that the error code is IMAGING_CODEC_MEMORY
assert str(e.value) == "-9"
TiffImagePlugin.READ_LIBTIFF = False
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
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:
# 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")

View File

@ -28,45 +28,26 @@ from .helper import (
assert_image_similar_tofile,
assert_not_all_same,
hopper,
is_big_endian,
is_win32,
mark_if_feature_version,
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:
@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:
Image.new(mode, (1, 1))
helper_image_new(mode, (1, 1))
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
def test_image_modes_fail(self, mode: str) -> None:
@ -1045,30 +1026,33 @@ class TestImage:
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:
im = hopper(mode)
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
@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:
im = hopper(mode)
source_bytes = im.tobytes()
reloaded = Image.new(mode, im.size)
reloaded = helper_image_new(mode, im.size)
reloaded.frombytes(source_bytes)
assert reloaded.tobytes() == source_bytes
@pytest.mark.parametrize(("mode", "pixelsize"), image_modes)
def test_getdata_putdata(self, mode: str, pixelsize: int) -> None:
im = Image.new(mode, (2, 2))
source_bytes = bytes(range(im.width * im.height * pixelsize))
im.frombytes(source_bytes)
reloaded = Image.new(mode, im.size)
@pytest.mark.parametrize("mode", Image.MODES + ["BGR;15", "BGR;16", "BGR;24"])
def test_getdata_putdata(self, mode: str) -> None:
if is_big_endian() and mode == "BGR;15":
pytest.xfail("Known failure of BGR;15 on big-endian")
im = hopper(mode)
reloaded = helper_image_new(mode, im.size)
reloaded.putdata(im.getdata())
assert_image_equal(im, reloaded)

View File

@ -33,7 +33,7 @@ except ImportError:
class AccessTest:
# initial value
# Initial value
_init_cffi_access = Image.USE_CFFI_ACCESS
_need_cffi_access = False
@ -138,8 +138,8 @@ class TestImageGetPixel(AccessTest):
if bands == 1:
return 1
if mode in ("BGR;15", "BGR;16"):
# These modes have less than 8 bits per band
# So (1, 2, 3) cannot be roundtripped
# These modes have less than 8 bits per band,
# so (1, 2, 3) cannot be roundtripped.
return (16, 32, 49)
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
)
# check putpixel
# Check putpixel
im = Image.new(mode, (1, 1), None)
im.putpixel((0, 0), expected_color)
actual_color = im.getpixel((0, 0))
@ -160,7 +160,7 @@ class TestImageGetPixel(AccessTest):
f"expected {expected_color} got {actual_color}"
)
# check putpixel negative index
# Check putpixel negative index
im.putpixel((-1, -1), expected_color)
actual_color = im.getpixel((-1, -1))
assert actual_color == expected_color, (
@ -168,22 +168,21 @@ class TestImageGetPixel(AccessTest):
f"expected {expected_color} got {actual_color}"
)
# Check 0
# Check 0x0 image with None initial color
im = Image.new(mode, (0, 0), None)
assert im.load() is not None
error = ValueError if self._need_cffi_access else IndexError
with pytest.raises(error):
im.putpixel((0, 0), expected_color)
with pytest.raises(error):
im.getpixel((0, 0))
# Check 0 negative index
# Check negative index
with pytest.raises(error):
im.putpixel((-1, -1), expected_color)
with pytest.raises(error):
im.getpixel((-1, -1))
# check initial color
# Check initial color
im = Image.new(mode, (1, 1), expected_color)
actual_color = im.getpixel((0, 0))
assert actual_color == expected_color, (
@ -191,46 +190,30 @@ class TestImageGetPixel(AccessTest):
f"expected {expected_color} got {actual_color}"
)
# check initial color negative index
# Check initial color negative index
actual_color = im.getpixel((-1, -1))
assert actual_color == expected_color, (
f"initial color failed with negative index for mode {mode}, "
f"expected {expected_color} got {actual_color}"
)
# Check 0
# Check 0x0 image with initial color
im = Image.new(mode, (0, 0), expected_color)
with pytest.raises(error):
im.getpixel((0, 0))
# Check 0 negative index
# Check negative index
with pytest.raises(error):
im.getpixel((-1, -1))
@pytest.mark.parametrize(
"mode",
(
"1",
"L",
"LA",
"I",
"I;16",
"I;16B",
"F",
"P",
"PA",
"BGR;15",
"BGR;16",
"BGR;24",
"RGB",
"RGBA",
"RGBX",
"CMYK",
"YCbCr",
),
)
@pytest.mark.parametrize("mode", Image.MODES)
def test_basic(self, mode: str) -> None:
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:
im = hopper()
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("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1))
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*
self.check(mode, expected_color)
@ -298,13 +281,6 @@ class TestCffi(AccessTest):
im = Image.new(mode, (10, 10), 40000)
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:
"""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("1"), 255)
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)
for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"):
im = Image.new(mode, (10, 10), 40000)
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")
def test_not_implemented(self) -> 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:
size = 10
@ -361,7 +332,7 @@ class TestCffi(AccessTest):
with pytest.warns(DeprecationWarning):
px = Image.new("L", (size, 1), 0).load()
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
@pytest.mark.parametrize("mode", ("P", "PA"))
@ -439,13 +410,14 @@ class TestEmbeddable:
from setuptools.command import build_ext
with open("embed_pil.c", "w", encoding="utf-8") as fh:
home = sys.prefix.replace("\\", "\\\\")
fh.write(
"""
f"""
#include "Python.h"
int main(int argc, char* argv[])
{
char *home = "%s";
{{
char *home = "{home}";
wchar_t *whome = Py_DecodeLocale(home, NULL);
Py_SetPythonHome(whome);
@ -460,9 +432,8 @@ int main(int argc, char* argv[])
PyMem_RawFree(whome);
return 0;
}
}}
"""
% sys.prefix.replace("\\", "\\\\")
)
compiler = getattr(build_ext, "new_compiler")()
@ -478,7 +449,7 @@ int main(int argc, char* argv[])
env = os.environ.copy()
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)
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"))
def test_mode_BGR(mode: str) -> None:
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)
assert list(im.getdata()) == data

View File

@ -47,7 +47,11 @@ def test_iterator_min_frame() -> None:
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:
for index, frame in enumerate(ImageSequence.Iterator(im)):
frame.load()
@ -55,17 +59,6 @@ def _test_multipage_tiff() -> None:
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:
with Image.open("Tests/images/multipage.tiff") as im:
first_frame = None

View File

@ -216,7 +216,10 @@ class TestLibPack:
)
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:
self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34)
@ -359,11 +362,14 @@ class TestLibUnpack:
)
def test_BGR(self) -> None:
self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8))
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))
with pytest.warns(DeprecationWarning):
self.assert_unpack(
"BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)
)
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:
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 pathlib import Path
from PIL import Image, TiffImagePlugin, features
import pytest
from PIL import Image, TiffImagePlugin
from PIL.TiffImagePlugin import IFDRational
from .helper import hopper
from .helper import hopper, skip_unless_feature
def _test_equal(num, denom, target) -> None:
@ -52,18 +54,18 @@ def test_nonetype() -> None:
assert xres and yres
def test_ifd_rational_save(tmp_path: Path) -> None:
methods = [True]
if features.check("libtiff"):
methods.append(False)
@pytest.mark.parametrize(
"libtiff", (pytest.param(True, marks=skip_unless_feature("libtiff")), 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:
TiffImagePlugin.WRITE_LIBTIFF = libtiff
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", libtiff)
im.save(out, dpi=(res, res), compression="raw")
im = hopper()
out = str(tmp_path / "temp.tiff")
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])
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
# ones.
extensions = [
"dater",
"sphinx.ext.autodoc",
"sphinx.ext.extlinks",
"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
: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
----------------

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;16B`` (16-bit big 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
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 |
+----------------------------------+----------------------------+---------------------+
| Fedora 38 | 3.11 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 39 | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Fedora 40 | 3.12 | 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 |
| | 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 |
| | 3.12, 3.13, PyPy3 | |
| +----------------------------+---------------------+
| | 3.10 | arm64v8, ppc64le, |
| | 3.10 | arm64v8 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
| | | s390x |
+----------------------------------+----------------------------+---------------------+
| 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::
:maxdepth: 2
10.4.0
10.3.0
10.2.0
10.1.0

View File

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

View File

@ -253,7 +253,7 @@ class BlpImageFile(ImageFile.ImageFile):
format = "BLP"
format_description = "Blizzard Mipmap Format"
def _open(self):
def _open(self) -> None:
self.magic = self.fp.read(4)
self.fp.seek(5, os.SEEK_CUR)
@ -333,7 +333,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
class BLP1Decoder(_BLPBaseDecoder):
def _load(self):
def _load(self) -> None:
if self._blp_compression == Format.JPEG:
self._decode_jpeg_stream()
@ -418,7 +418,7 @@ class BLP2Decoder(_BLPBaseDecoder):
class BLPEncoder(ImageFile.PyEncoder):
_pushes_fd = True
def _write_palette(self):
def _write_palette(self) -> bytes:
data = b""
palette = self.im.getpalette("RGBA", "RGBA")
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"""
# read 14 bytes: magic number, filesize, reserved, header final offset
head_data = self.fp.read(14)
@ -379,7 +379,7 @@ class DibImageFile(BmpImageFile):
format = "DIB"
format_description = "Windows Bitmap"
def _open(self):
def _open(self) -> None:
self._bitmap()

View File

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

View File

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

View File

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

View File

@ -271,16 +271,16 @@ class D3DFMT(IntEnum):
module = sys.modules[__name__]
for item in DDSD:
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:
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:
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:
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_RGB = DDPF.RGB
@ -331,7 +331,7 @@ class DdsImageFile(ImageFile.ImageFile):
format = "DDS"
format_description = "DirectDraw Surface"
def _open(self):
def _open(self) -> None:
if not _accept(self.fp.read(4)):
msg = "not a DDS file"
raise SyntaxError(msg)
@ -472,7 +472,7 @@ class DdsImageFile(ImageFile.ImageFile):
else:
self.tile = [ImageFile._Tile("raw", extents, 0, rawmode or self.mode)]
def load_seek(self, pos):
def load_seek(self, pos: int) -> None:
pass

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -76,19 +76,19 @@ class GifImageFile(ImageFile.ImageFile):
global_palette = None
def data(self):
def data(self) -> bytes | None:
s = self.fp.read(1)
if s and s[0]:
return self.fp.read(s[0])
return None
def _is_palette_needed(self, p):
def _is_palette_needed(self, p: bytes) -> bool:
for i in range(0, len(p), 3):
if not (i // 3 == p[i] == p[i + 1] == p[i + 2]):
return True
return False
def _open(self):
def _open(self) -> None:
# Screen
s = self.fp.read(13)
if not _accept(s):
@ -147,7 +147,7 @@ class GifImageFile(ImageFile.ImageFile):
self.seek(current)
return self._is_animated
def seek(self, frame):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
if frame < self.__frame:
@ -417,7 +417,7 @@ class GifImageFile(ImageFile.ImageFile):
elif k in self.info:
del self.info[k]
def load_prepare(self):
def load_prepare(self) -> None:
temp_mode = "P" if self._frame_palette else "L"
self._prev_im = None
if self.__frame == 0:
@ -437,7 +437,7 @@ class GifImageFile(ImageFile.ImageFile):
super().load_prepare()
def load_end(self):
def load_end(self) -> None:
if self.__frame == 0:
if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS:
if self._frame_transparency is not None:
@ -463,7 +463,7 @@ class GifImageFile(ImageFile.ImageFile):
else:
self.im.paste(frame_im, self.dispose_extent)
def tell(self):
def tell(self) -> int:
return self.__frame
@ -474,7 +474,7 @@ class GifImageFile(ImageFile.ImageFile):
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
for saving in a Gif.
@ -887,7 +887,7 @@ def _get_optimize(im, info):
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
if not palette_bytes:
return 0
@ -897,7 +897,7 @@ def _get_color_table_size(palette_bytes):
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
suitable for direct inclusion in the GIF header
@ -915,7 +915,7 @@ def _get_header_palette(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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ import warnings
from collections.abc import Callable, MutableMapping
from enum import IntEnum
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.
# PILLOW_VERSION was removed in Pillow 9.0.0.
@ -55,6 +55,7 @@ from . import (
_plugins,
)
from ._binary import i32le, o32be, o32le
from ._deprecate import deprecate
from ._typing import StrOrBytesPath, TypeGuard
from ._util import DeferredError, is_path
@ -248,7 +249,28 @@ def _conv_type_shape(im):
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
# may have to modify the stride calculation in map.c too!
@ -404,7 +426,7 @@ def _getdecoder(mode, decoder_name, args, extra=()):
try:
# get decoder
decoder = getattr(core, decoder_name + "_decoder")
decoder = getattr(core, f"{decoder_name}_decoder")
except AttributeError as e:
msg = f"decoder {decoder_name} not available"
raise OSError(msg) from e
@ -427,7 +449,7 @@ def _getencoder(mode, encoder_name, args, extra=()):
try:
# get encoder
encoder = getattr(core, encoder_name + "_encoder")
encoder = getattr(core, f"{encoder_name}_encoder")
except AttributeError as e:
msg = f"encoder {encoder_name} not available"
raise OSError(msg) from e
@ -602,7 +624,7 @@ class Image:
) -> str:
suffix = ""
if format:
suffix = "." + format
suffix = f".{format}"
if not file:
f, filename = tempfile.mkstemp(suffix)
@ -876,7 +898,7 @@ class Image:
return self.pyaccess
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
method attempts to determine if the file is broken, without
@ -939,6 +961,9 @@ class Image:
:returns: An :py:class:`~PIL.Image.Image` object.
"""
if mode in ("BGR;15", "BGR;16", "BGR;24"):
deprecate(mode, 12)
self.load()
has_transparency = "transparency" in self.info
@ -1263,7 +1288,9 @@ class Image:
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
image that as closely as possible matches the given mode and
@ -1286,13 +1313,16 @@ class Image:
"""
pass
def _expand(self, xmargin, ymargin=None):
def _expand(self, xmargin: int, ymargin: int | None = None) -> Image:
if ymargin is None:
ymargin = xmargin
self.load()
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
available filters, see the :py:mod:`~PIL.ImageFilter` module.
@ -1304,7 +1334,7 @@ class Image:
self.load()
if isinstance(filter, Callable):
if callable(filter):
filter = filter()
if not hasattr(filter, "filter"):
msg = "filter argument should be ImageFilter.Filter instance or class"
@ -2174,7 +2204,7 @@ class Image:
(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)
if reducing_gap is not None and reducing_gap < 1.0:
@ -2819,7 +2849,7 @@ class Image:
(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)
image.load()
@ -2956,6 +2986,9 @@ def new(
:returns: An :py:class:`~PIL.Image.Image` object.
"""
if mode in ("BGR;15", "BGR;16", "BGR;24"):
deprecate(mode, 12)
_check_size(size)
if color is None:
@ -3214,8 +3247,8 @@ _fromarray_typemap = {
((1, 1, 3), "|u1"): ("RGB", "RGB"),
((1, 1, 4), "|u1"): ("RGBA", "RGBA"),
# shortcuts:
((1, 1), _ENDIAN + "i4"): ("I", "I"),
((1, 1), _ENDIAN + "f4"): ("F", "F"),
((1, 1), f"{_ENDIAN}i4"): ("I", "I"),
((1, 1), f"{_ENDIAN}f4"): ("F", "F"),
}
@ -3443,7 +3476,7 @@ def eval(image, *args):
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.

View File

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

View File

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

View File

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

View File

@ -16,11 +16,14 @@
#
from __future__ import annotations
import abc
import functools
class Filter:
pass
@abc.abstractmethod
def filter(self, image):
pass
class MultibandFilter(Filter):
@ -541,7 +544,7 @@ class Color3DLUT(MultibandFilter):
_copy_table=False,
)
def __repr__(self):
def __repr__(self) -> str:
r = [
f"{self.__class__.__name__} from {self.table.__class__.__name__}",
"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)
im_1.load()
try:
op = getattr(_imagingmath, op + "_" + im_1.mode)
op = getattr(_imagingmath, f"{op}_{im_1.mode}")
except AttributeError as e:
msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e
@ -89,7 +89,7 @@ class _Operand:
im_1.load()
im_2.load()
try:
op = getattr(_imagingmath, op + "_" + im_1.mode)
op = getattr(_imagingmath, f"{op}_{im_1.mode}")
except AttributeError as e:
msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e

View File

@ -18,6 +18,8 @@ import sys
from functools import lru_cache
from typing import NamedTuple
from ._deprecate import deprecate
class ModeDescriptor(NamedTuple):
"""Wrapper for mode strings."""
@ -42,8 +44,8 @@ def getmode(mode: str) -> ModeDescriptor:
# Bits need to be extended to bytes
"1": ("L", "L", ("1",), "|b1"),
"L": ("L", "L", ("L",), "|u1"),
"I": ("L", "I", ("I",), endian + "i4"),
"F": ("L", "F", ("F",), endian + "f4"),
"I": ("L", "I", ("I",), f"{endian}i4"),
"F": ("L", "F", ("F",), f"{endian}f4"),
"P": ("P", "L", ("P",), "|u1"),
"RGB": ("RGB", "L", ("R", "G", "B"), "|u1"),
"RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"),
@ -63,6 +65,8 @@ def getmode(mode: str) -> ModeDescriptor:
"PA": ("RGB", "L", ("P", "A"), "|u1"),
}
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]
return ModeDescriptor(mode, bands, base_mode, base_type, type_str)
@ -74,8 +78,8 @@ def getmode(mode: str) -> ModeDescriptor:
"I;16LS": "<i2",
"I;16B": ">u2",
"I;16BS": ">i2",
"I;16N": endian + "u2",
"I;16NS": endian + "i2",
"I;16N": f"{endian}u2",
"I;16NS": f"{endian}i2",
"I;32": "<u4",
"I;32B": ">u4",
"I;32L": "<u4",

View File

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

View File

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

View File

@ -128,7 +128,7 @@ class PhotoImage:
if image:
self.paste(image)
def __del__(self):
def __del__(self) -> None:
name = self.__photo.name
self.__photo.name = None
try:
@ -136,7 +136,7 @@ class PhotoImage:
except Exception:
pass # ignore internal errors
def __str__(self):
def __str__(self) -> str:
"""
Get the Tkinter photo image identifier. This method is automatically
called by Tkinter whenever a PhotoImage object is passed to a Tkinter
@ -146,7 +146,7 @@ class PhotoImage:
"""
return str(self.__photo)
def width(self):
def width(self) -> int:
"""
Get the width of the image.
@ -154,7 +154,7 @@ class PhotoImage:
"""
return self.__size[0]
def height(self):
def height(self) -> int:
"""
Get the height of the image.
@ -219,7 +219,7 @@ class BitmapImage:
kw["data"] = image.tobitmap()
self.__photo = tkinter.BitmapImage(**kw)
def __del__(self):
def __del__(self) -> None:
name = self.__photo.name
self.__photo.name = None
try:
@ -227,7 +227,7 @@ class BitmapImage:
except Exception:
pass # ignore internal errors
def width(self):
def width(self) -> int:
"""
Get the width of the image.
@ -235,7 +235,7 @@ class BitmapImage:
"""
return self.__size[0]
def height(self):
def height(self) -> int:
"""
Get the height of the image.
@ -243,7 +243,7 @@ class BitmapImage:
"""
return self.__size[1]
def __str__(self):
def __str__(self) -> str:
"""
Get the Tkinter bitmap image identifier. This method is automatically
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):
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):
pass
@ -204,7 +204,7 @@ class Window:
def ui_handle_damage(self, x0, y0, x1, y1):
pass
def ui_handle_destroy(self):
def ui_handle_destroy(self) -> None:
pass
def ui_handle_repair(self, dc, x0, y0, x1, y1):
@ -213,7 +213,7 @@ class Window:
def ui_handle_resize(self, width, height):
pass
def mainloop(self):
def mainloop(self) -> None:
Image.core.eventloop()

View File

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

View File

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

View File

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

View File

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

View File

@ -53,6 +53,10 @@ class BitStream:
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,
# but it cannot read it.
@ -77,7 +81,7 @@ class MpegImageFile(ImageFile.ImageFile):
# --------------------------------------------------------------------
# Registry stuff
Image.register_open(MpegImageFile.format, MpegImageFile)
Image.register_open(MpegImageFile.format, MpegImageFile, _accept)
Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"])

View File

@ -100,7 +100,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
format_description = "MPO (CIPA DC-007)"
_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
JpegImagePlugin.JpegImageFile._open(self)
self._after_jpeg_open()
@ -124,10 +124,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
# for now we can only handle reading and individual frame extraction
self.readonly = 1
def load_seek(self, pos):
def load_seek(self, pos: int) -> None:
self._fp.seek(pos)
def seek(self, frame):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
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.__frame = frame
def tell(self):
def tell(self) -> int:
return self.__frame
@staticmethod

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,6 +37,7 @@ from __future__ import annotations
import os
import struct
import sys
from typing import TYPE_CHECKING
from . import Image, ImageFile
@ -97,7 +98,7 @@ class SpiderImageFile(ImageFile.ImageFile):
format_description = "Spider 2D image"
_close_exclusive_fp_after_loading = False
def _open(self):
def _open(self) -> None:
# check header
n = 27 * 4 # read 27 float values
f = self.fp.read(n)
@ -157,21 +158,21 @@ class SpiderImageFile(ImageFile.ImageFile):
self._fp = self.fp # FIXME: hack
@property
def n_frames(self):
def n_frames(self) -> int:
return self._nimages
@property
def is_animated(self):
def is_animated(self) -> bool:
return self._nimages > 1
# 1st image index is zero (although SPIDER imgnumber starts at 1)
def tell(self):
def tell(self) -> int:
if self.imgnumber < 1:
return 0
else:
return self.imgnumber - 1
def seek(self, frame):
def seek(self, frame: int) -> None:
if self.istack == 0:
msg = "attempt to seek in a non-stack file"
raise EOFError(msg)
@ -191,8 +192,11 @@ class SpiderImageFile(ImageFile.ImageFile):
b = -m * minimum
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
def tkPhotoImage(self):
def tkPhotoImage(self) -> ImageTk.PhotoImage:
from . import ImageTk
return ImageTk.PhotoImage(self.convert2byte(), palette=256)
@ -218,7 +222,7 @@ def loadImageSeries(filelist=None):
im = im.convert2byte()
except Exception:
if not isSpiderImage(img):
print(img + " is not a Spider image file")
print(f"{img} is not a Spider image file")
continue
im.info["filename"] = img
imglist.append(im)
@ -299,10 +303,10 @@ if __name__ == "__main__":
sys.exit()
with Image.open(filename) as im:
print("image: " + str(im))
print("format: " + str(im.format))
print("size: " + str(im.size))
print("mode: " + str(im.mode))
print(f"image: {im}")
print(f"format: {im.format}")
print(f"size: {im.size}")
print(f"mode: {im.mode}")
print("max, min: ", end=" ")
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 i32be as i32
from ._binary import o8
from ._deprecate import deprecate
from .TiffTags import TYPES
logger = logging.getLogger(__name__)
@ -244,6 +245,7 @@ OPEN_INFO = {
(MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
(II, 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"),
(MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
(II, 3, (1,), 2, (8,), ()): ("P", "P;R"),
@ -276,6 +278,9 @@ PREFIXES = [
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:
return prefix[:4] in PREFIXES
@ -376,7 +381,7 @@ class IFDRational(Rational):
f = self._val.limit_denominator(max_denominator)
return f.numerator, f.denominator
def __repr__(self):
def __repr__(self) -> str:
return str(float(self._val))
def __hash__(self):
@ -464,7 +469,7 @@ def _register_basic(idx_fmt_name):
idx, fmt, name = idx_fmt_name
TYPES[idx] = name
size = struct.calcsize("=" + fmt)
size = struct.calcsize(f"={fmt}")
_load_dispatch[idx] = ( # noqa: F821
size,
lambda self, data, legacy_api=True: (
@ -598,7 +603,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
self._next = None
self._offset = None
def __str__(self):
def __str__(self) -> str:
return str(dict(self))
def named(self):
@ -612,7 +617,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
for code, value in self.items()
}
def __len__(self):
def __len__(self) -> int:
return len(set(self._tagdata) | set(self._tags_v2))
def __getitem__(self, tag):
@ -982,8 +987,8 @@ ImageFileDirectory_v2._load_dispatch = _load_dispatch
ImageFileDirectory_v2._write_dispatch = _write_dispatch
for idx, name in TYPES.items():
name = name.replace(" ", "_")
setattr(ImageFileDirectory_v2, "load_" + name, _load_dispatch[idx][1])
setattr(ImageFileDirectory_v2, "write_" + name, _write_dispatch[idx])
setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1])
setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx])
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
return ifd
def to_v2(self):
def to_v2(self) -> ImageFileDirectory_v2:
"""Returns an
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
instance with the same data as is contained in the original
@ -1056,7 +1061,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
def __contains__(self, tag):
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))
def __iter__(self):
@ -1138,7 +1143,7 @@ class TiffImageFile(ImageFile.ImageFile):
self.seek(current)
return self._n_frames
def seek(self, frame):
def seek(self, frame: int) -> None:
"""Select a given frame as current image"""
if not self._seek_check(frame):
return
@ -1149,7 +1154,7 @@ class TiffImageFile(ImageFile.ImageFile):
Image._decompression_bomb_check(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
# reset buffered io handle in case fp
@ -1193,7 +1198,7 @@ class TiffImageFile(ImageFile.ImageFile):
self.__frame = frame
self._setup()
def tell(self):
def tell(self) -> int:
"""Return the current frame number"""
return self.__frame
@ -1232,7 +1237,7 @@ class TiffImageFile(ImageFile.ImageFile):
return self._load_libtiff()
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
# This is the ImageFile.load path only, libtiff specific below.
if not self.is_animated:
@ -1937,7 +1942,7 @@ class AppendingTiffWriter:
self.beginning = self.f.tell()
self.setup()
def setup(self):
def setup(self) -> None:
# Reset everything.
self.f.seek(self.beginning, os.SEEK_SET)
@ -1962,7 +1967,7 @@ class AppendingTiffWriter:
self.skipIFDs()
self.goToEnd()
def finalize(self):
def finalize(self) -> None:
if self.isFirst:
return
@ -1985,7 +1990,7 @@ class AppendingTiffWriter:
self.f.seek(ifd_offset)
self.fixIFD()
def newFrame(self):
def newFrame(self) -> None:
# Call this to finish a frame.
self.finalize()
self.setup()
@ -1998,7 +2003,7 @@ class AppendingTiffWriter:
self.close()
return False
def tell(self):
def tell(self) -> int:
return self.f.tell() - self.offsetOfNewPage
def seek(self, offset, whence=io.SEEK_SET):
@ -2008,7 +2013,7 @@ class AppendingTiffWriter:
self.f.seek(offset, whence)
return self.tell()
def goToEnd(self):
def goToEnd(self) -> None:
self.f.seek(0, os.SEEK_END)
pos = self.f.tell()
@ -2020,11 +2025,11 @@ class AppendingTiffWriter:
def setEndian(self, endian):
self.endian = endian
self.longFmt = self.endian + "L"
self.shortFmt = self.endian + "H"
self.tagFormat = self.endian + "HHL"
self.longFmt = f"{self.endian}L"
self.shortFmt = f"{self.endian}H"
self.tagFormat = f"{self.endian}HHL"
def skipIFDs(self):
def skipIFDs(self) -> None:
while True:
ifd_offset = self.readLong()
if ifd_offset == 0:
@ -2079,11 +2084,11 @@ class AppendingTiffWriter:
msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg)
def close(self):
def close(self) -> None:
self.finalize()
self.f.close()
def fixIFD(self):
def fixIFD(self) -> None:
num_tags = self.readShort()
for i in range(num_tags):

View File

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

View File

@ -43,7 +43,7 @@ class WebPImageFile(ImageFile.ImageFile):
__loaded = 0
__logical_frame = 0
def _open(self):
def _open(self) -> None:
if not _webp.HAVE_WEBPANIM:
# Legacy mode
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 {}
def seek(self, frame):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
@ -144,7 +144,7 @@ class WebPImageFile(ImageFile.ImageFile):
timestamp -= duration
return data, timestamp, duration
def _seek(self, frame):
def _seek(self, frame: int) -> None:
if self.__physical_frame == frame:
return # Nothing to do
if frame < self.__physical_frame:
@ -171,10 +171,10 @@ class WebPImageFile(ImageFile.ImageFile):
return super().load()
def load_seek(self, pos):
def load_seek(self, pos: int) -> None:
pass
def tell(self):
def tell(self) -> int:
if not _webp.HAVE_WEBPANIM:
return super().tell()

View File

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

View File

@ -36,7 +36,7 @@ class XpmImageFile(ImageFile.ImageFile):
format = "XPM"
format_description = "X11 Pixel Map"
def _open(self):
def _open(self) -> None:
if not _accept(self.fp.read(9)):
msg = "not an XPM file"
raise SyntaxError(msg)
@ -103,16 +103,13 @@ class XpmImageFile(ImageFile.ImageFile):
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
xsize, ysize = self.size
s = [None] * ysize
for i in range(ysize):
s[i] = self.fp.readline()[1 : xsize + 1].ljust(xsize)
s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)]
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.
@ -42,7 +42,7 @@ def check_module(feature):
return False
def version_module(feature):
def version_module(feature: str) -> str | None:
"""
:param feature: The module to check for.
:returns:
@ -54,13 +54,10 @@ def version_module(feature):
module, ver = modules[feature]
if ver is None:
return None
return getattr(__import__(module, fromlist=[ver]), ver)
def get_supported_modules():
def get_supported_modules() -> list[str]:
"""
: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.
@ -89,10 +86,10 @@ def check_codec(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.
:returns:
@ -105,7 +102,7 @@ def version_codec(feature):
codec, lib = codecs[feature]
version = getattr(Image.core, lib + "_version")
version = getattr(Image.core, f"{lib}_version")
if feature == "libtiff":
return version.split("\n")[0].split("Version ")[1]
@ -113,7 +110,7 @@ def version_codec(feature):
return version
def get_supported_codecs():
def get_supported_codecs() -> list[str]:
"""
: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.
@ -157,7 +154,7 @@ def check_feature(feature):
return None
def version_feature(feature):
def version_feature(feature: str) -> str | None:
"""
:param feature: The feature to check for.
: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)
def get_supported_features():
def get_supported_features() -> list[str]:
"""
:returns: A list of all supported features.
"""
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.
:returns:
@ -199,7 +196,7 @@ def check(feature):
return False
def version(feature):
def version(feature: str) -> str | None:
"""
:param feature:
The module, codec, or feature to check for.
@ -215,7 +212,7 @@ def version(feature):
return None
def get_supported():
def get_supported() -> list[str]:
"""
: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,
"image",
self->image->image);
};
}
static struct PyGetSetDef getsetters[] = {
{"mode", (getter)_getattr_mode},

View File

@ -213,34 +213,37 @@ cms_transform_dealloc(CmsTransformObject *self) {
static cmsUInt32Number
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;
} else if (strcmp(PILmode, "RGBA") == 0) {
return TYPE_RGBA_8;
} else if (strcmp(PILmode, "RGBX") == 0) {
return TYPE_RGBA_8;
} else if (strcmp(PILmode, "RGBA;16B") == 0) {
}
if (strcmp(PILmode, "RGBA;16B") == 0) {
return TYPE_RGBA_16;
} else if (strcmp(PILmode, "CMYK") == 0) {
}
if (strcmp(PILmode, "CMYK") == 0) {
return TYPE_CMYK_8;
} else if (strcmp(PILmode, "L") == 0) {
return TYPE_GRAY_8;
} else if (strcmp(PILmode, "L;16") == 0) {
}
if (strcmp(PILmode, "L;16") == 0) {
return TYPE_GRAY_16;
} else if (strcmp(PILmode, "L;16B") == 0) {
}
if (strcmp(PILmode, "L;16B") == 0) {
return TYPE_GRAY_16_SE;
} else if (strcmp(PILmode, "YCCA") == 0) {
}
if (
strcmp(PILmode, "YCCA") == 0 ||
strcmp(PILmode, "YCC") == 0
) {
return TYPE_YCbCr_8;
} else if (strcmp(PILmode, "YCC") == 0) {
return TYPE_YCbCr_8;
} else if (strcmp(PILmode, "LAB") == 0) {
}
if (strcmp(PILmode, "LAB") == 0) {
// 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));
}
else {
/* take a wild guess... */
return TYPE_GRAY_8;
}
/* presume "L" by default */
return TYPE_GRAY_8;
}
#define Cms_Min(a, b) ((a) < (b) ? (a) : (b))

View File

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

View File

@ -81,12 +81,6 @@ get_pixel_16B(Imaging im, int x, int y, void *color) {
#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
get_pixel_BGR15(Imaging im, int x, int y, void *color) {
UINT8 *in = (UINT8 *)&im->image8[y][x * 2];
@ -207,7 +201,11 @@ ImagingAccessInit() {
ADD("I;16", 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;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;32B", get_pixel_32B, put_pixel_32B);
ADD("F", get_pixel_32, put_pixel_32);

View File

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

View File

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

View File

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