Merge branch 'main' into convert_mode

This commit is contained in:
Andrew Murray 2024-02-11 21:55:07 +11:00 committed by GitHub
commit e617e604b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 660 additions and 458 deletions

View File

@ -10,6 +10,8 @@ exclude_also =
if DEBUG: if DEBUG:
# Don't complain about compatibility code for missing optional dependencies # Don't complain about compatibility code for missing optional dependencies
except ImportError except ImportError
if TYPE_CHECKING:
@abc.abstractmethod
[run] [run]
omit = omit =

View File

@ -23,6 +23,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# Drafts your next release notes as pull requests are merged into "main" # Drafts your next release notes as pull requests are merged into "main"
- uses: release-drafter/release-drafter@v5 - uses: release-drafter/release-drafter@v6
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -6,6 +6,7 @@ This sort of info is missing from GitHub Actions.
Requested here: Requested here:
https://github.com/actions/virtual-environments/issues/79 https://github.com/actions/virtual-environments/issues/79
""" """
from __future__ import annotations from __future__ import annotations
import os import os

View File

@ -49,7 +49,6 @@ jobs:
- name: Install Cygwin - name: Install Cygwin
uses: egor-tensin/setup-cygwin@v4 uses: egor-tensin/setup-cygwin@v4
with: with:
platform: x86_64
packages: > packages: >
gcc-g++ gcc-g++
ghostscript ghostscript
@ -81,7 +80,7 @@ jobs:
zlib-devel zlib-devel
- name: Add Lapack to PATH - name: Add Lapack to PATH
uses: egor-tensin/cleanup-path@v3 uses: egor-tensin/cleanup-path@v4
with: with:
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
@ -142,7 +141,7 @@ jobs:
bash.exe .ci/after_success.sh bash.exe .ci/after_success.sh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3.1.5
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Cygwin flags: GHA_Cygwin

View File

@ -101,7 +101,7 @@ jobs:
MATRIX_DOCKER: ${{ matrix.docker }} MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3.1.5
with: with:
flags: GHA_Docker flags: GHA_Docker
name: ${{ matrix.docker }} name: ${{ matrix.docker }}

View File

@ -82,7 +82,7 @@ jobs:
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3.1.5
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows

View File

@ -202,7 +202,7 @@ jobs:
shell: pwsh shell: pwsh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3.1.5
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows

View File

@ -149,7 +149,7 @@ jobs:
.ci/after_success.sh .ci/after_success.sh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3.1.5
with: with:
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}

View File

@ -72,14 +72,12 @@ function build {
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then if [ -n "$IS_MACOS" ]; then
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then
cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
fi fi
fi
else else
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
fi fi
@ -131,13 +129,13 @@ untar pillow-depends-main.zip
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then
# webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb # webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
# libxdmcp causes an issue on macOS < 11 # libxau and libxdmcp cause an issue on macOS < 11
# if php is installed, brew tries to reinstall these after installing openblas # if php is installed, brew tries to reinstall these after installing openblas
# remove cairo to fix building harfbuzz on arm64 # remove cairo to fix building harfbuzz on arm64
# remove lcms2 and libpng to fix building openjpeg on arm64 # remove lcms2 and libpng to fix building openjpeg on arm64
# remove zstd to avoid inclusion on x86_64 # remove zstd to avoid inclusion on x86_64
# curl from brew requires zstd, use system curl # curl from brew requires zstd, use system curl
brew remove --ignore-dependencies webp libpng libtiff libxcb libxdmcp curl php cairo lcms2 ghostscript zstd brew remove --ignore-dependencies webp libpng libtiff libxcb libxau libxdmcp curl php cairo lcms2 ghostscript zstd
brew install pkg-config brew install pkg-config
fi fi

View File

@ -1,17 +1,17 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.9 rev: v0.2.0
hooks: hooks:
- id: ruff - id: ruff
args: [--fix, --exit-non-zero-on-fix] args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.12.1 rev: 24.1.1
hooks: hooks:
- id: black - id: black
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.7.6 rev: 1.7.7
hooks: hooks:
- id: bandit - id: bandit
args: [--severity-level=high] args: [--severity-level=high]
@ -48,12 +48,12 @@ repos:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: 1.5.3 rev: 1.7.0
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.15 rev: v0.16
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject

View File

@ -5,6 +5,12 @@ Changelog (Pillow)
10.3.0 (unreleased) 10.3.0 (unreleased)
------------------- -------------------
- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745
[nik012003, radarhere]
- Remove execute bit from ``setup.py`` #7760
[hugovk]
- Do not support using test-image-results to upload images after test failures #7739 - Do not support using test-image-results to upload images after test failures #7739
[radarhere] [radarhere]

View File

@ -1,6 +1,7 @@
""" """
Helper functions. Helper functions.
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging

View File

@ -10,7 +10,7 @@ from .helper import assert_image_similar
base = os.path.join("Tests", "images", "bmp") base = os.path.join("Tests", "images", "bmp")
def get_files(d, ext: str = ".bmp"): def get_files(d: str, ext: str = ".bmp") -> list[str]:
return [ return [
os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f
] ]
@ -29,7 +29,7 @@ def test_bad() -> None:
pass pass
def test_questionable(): def test_questionable() -> None:
"""These shouldn't crash/dos, but it's not well defined that these """These shouldn't crash/dos, but it's not well defined that these
are in spec""" are in spec"""
supported = [ supported = [
@ -80,7 +80,7 @@ def test_good() -> None:
"rgb32bf.bmp": "rgb24.png", "rgb32bf.bmp": "rgb24.png",
} }
def get_compare(f): def get_compare(f: str) -> str:
name = os.path.split(f)[1] name = os.path.split(f)[1]
if name in file_map: if name in file_map:
return os.path.join(base, "html", file_map[name]) return os.path.join(base, "html", file_map[name])

View File

@ -23,11 +23,11 @@ def test_imageops_box_blur() -> None:
assert isinstance(i, Image.Image) assert isinstance(i, Image.Image)
def box_blur(image, radius: int = 1, n: int = 1): def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image:
return image._new(image.im.box_blur((radius, radius), n)) return image._new(image.im.box_blur((radius, radius), n))
def assert_image(im, data, delta: int = 0) -> None: def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None:
it = iter(im.getdata()) it = iter(im.getdata())
for data_row in data: for data_row in data:
im_row = [next(it) for _ in range(im.size[0])] im_row = [next(it) for _ in range(im.size[0])]
@ -37,7 +37,13 @@ def assert_image(im, data, delta: int = 0) -> None:
next(it) next(it)
def assert_blur(im, radius, data, passes: int = 1, delta: int = 0) -> None: def assert_blur(
im: Image.Image,
radius: float,
data: list[list[int]],
passes: int = 1,
delta: int = 0,
) -> None:
# check grayscale image # check grayscale image
assert_image(box_blur(im, radius, passes), data, delta) assert_image(box_blur(im, radius, passes), data, delta)
rgba = Image.merge("RGBA", (im, im, im, im)) rgba = Image.merge("RGBA", (im, im, im, im))

View File

@ -15,7 +15,9 @@ except ImportError:
class TestColorLut3DCoreAPI: class TestColorLut3DCoreAPI:
def generate_identity_table(self, channels, size): def generate_identity_table(
self, channels: int, size: int | tuple[int, int, int]
) -> tuple[int, int, int, int, list[float]]:
if isinstance(size, tuple): if isinstance(size, tuple):
size_1d, size_2d, size_3d = size size_1d, size_2d, size_3d = size
else: else:

View File

@ -47,7 +47,7 @@ def test_apng_basic() -> None:
"filename", "filename",
("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"),
) )
def test_apng_fdat(filename) -> None: def test_apng_fdat(filename: str) -> None:
with Image.open(filename) as im: with Image.open(filename) as im:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((0, 0)) == (0, 255, 0, 255)
@ -338,7 +338,7 @@ def test_apng_syntax_errors() -> None:
"sequence_fdat_fctl.png", "sequence_fdat_fctl.png",
), ),
) )
def test_apng_sequence_errors(test_file) -> None: def test_apng_sequence_errors(test_file: str) -> None:
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
with Image.open(f"Tests/images/apng/{test_file}") as im: with Image.open(f"Tests/images/apng/{test_file}") as im:
im.seek(im.n_frames - 1) im.seek(im.n_frames - 1)
@ -681,7 +681,7 @@ def test_seek_after_close() -> None:
@pytest.mark.parametrize("default_image", (True, False)) @pytest.mark.parametrize("default_image", (True, False))
@pytest.mark.parametrize("duplicate", (True, False)) @pytest.mark.parametrize("duplicate", (True, False))
def test_different_modes_in_later_frames( def test_different_modes_in_later_frames(
mode, default_image, duplicate, tmp_path: Path mode: str, default_image: bool, duplicate: bool, tmp_path: Path
) -> None: ) -> None:
test_file = str(tmp_path / "temp.png") test_file = str(tmp_path / "temp.png")

View File

@ -64,7 +64,7 @@ def test_seek_mode_2() -> None:
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_read_n0(bytesmode) -> None: def test_read_n0(bytesmode: bool) -> None:
# Arrange # Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh: with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
@ -80,7 +80,7 @@ def test_read_n0(bytesmode) -> None:
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_read_n(bytesmode) -> None: def test_read_n(bytesmode: bool) -> None:
# Arrange # Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh: with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
@ -96,7 +96,7 @@ def test_read_n(bytesmode) -> None:
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_read_eof(bytesmode) -> None: def test_read_eof(bytesmode: bool) -> None:
# Arrange # Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh: with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100) container = ContainerIO.ContainerIO(fh, 22, 100)
@ -112,7 +112,7 @@ def test_read_eof(bytesmode) -> None:
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_readline(bytesmode) -> None: def test_readline(bytesmode: bool) -> None:
# Arrange # Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh: with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120) container = ContainerIO.ContainerIO(fh, 0, 120)
@ -127,7 +127,7 @@ def test_readline(bytesmode) -> None:
@pytest.mark.parametrize("bytesmode", (True, False)) @pytest.mark.parametrize("bytesmode", (True, False))
def test_readlines(bytesmode) -> None: def test_readlines(bytesmode: bool) -> None:
# Arrange # Arrange
expected = [ expected = [
"This is line 1\n", "This is line 1\n",

View File

@ -1,4 +1,5 @@
"""Test DdsImagePlugin""" """Test DdsImagePlugin"""
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO

View File

@ -84,7 +84,7 @@ simple_eps_file_with_long_binary_data = (
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
) )
@pytest.mark.parametrize("scale", (1, 2)) @pytest.mark.parametrize("scale", (1, 2))
def test_sanity(filename, size, scale) -> None: def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
expected_size = tuple(s * scale for s in size) expected_size = tuple(s * scale for s in size)
with Image.open(filename) as image: with Image.open(filename) as image:
image.load(scale=scale) image.load(scale=scale)
@ -129,28 +129,28 @@ def test_binary_header_only() -> None:
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_missing_version_comment(prefix) -> None: def test_missing_version_comment(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version))
with pytest.raises(SyntaxError): with pytest.raises(SyntaxError):
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_missing_boundingbox_comment(prefix) -> None: def test_missing_boundingbox_comment(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox))
with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'):
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_invalid_boundingbox_comment(prefix) -> None: def test_invalid_boundingbox_comment(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox))
with pytest.raises(OSError, match="cannot determine EPS bounding box"): with pytest.raises(OSError, match="cannot determine EPS bounding box"):
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix) -> None: def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None:
data = io.BytesIO( data = io.BytesIO(
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata)
) )
@ -161,21 +161,21 @@ def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix) -> None:
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_ascii_comment_too_long(prefix) -> None: def test_ascii_comment_too_long(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment))
with pytest.raises(SyntaxError, match="not an EPS file"): with pytest.raises(SyntaxError, match="not an EPS file"):
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_long_binary_data(prefix) -> None: def test_long_binary_data(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
EpsImagePlugin.EpsImageFile(data) EpsImagePlugin.EpsImageFile(data)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_load_long_binary_data(prefix) -> None: def test_load_long_binary_data(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
with Image.open(data) as img: with Image.open(data) as img:
img.load() img.load()
@ -305,7 +305,7 @@ def test_render_scale2() -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) @pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps"))
def test_resize(filename) -> None: def test_resize(filename: str) -> None:
with Image.open(filename) as im: with Image.open(filename) as im:
new_size = (100, 100) new_size = (100, 100)
im = im.resize(new_size) im = im.resize(new_size)
@ -314,7 +314,7 @@ def test_resize(filename) -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize("filename", (FILE1, FILE2)) @pytest.mark.parametrize("filename", (FILE1, FILE2))
def test_thumbnail(filename) -> None: def test_thumbnail(filename: str) -> None:
# Issue #619 # Issue #619
with Image.open(filename) as im: with Image.open(filename) as im:
new_size = (100, 100) new_size = (100, 100)
@ -335,7 +335,7 @@ def test_readline_psfile(tmp_path: Path) -> None:
line_endings = ["\r\n", "\n", "\n\r", "\r"] line_endings = ["\r\n", "\n", "\n\r", "\r"]
strings = ["something", "else", "baz", "bif"] strings = ["something", "else", "baz", "bif"]
def _test_readline(t, ending) -> None: def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
ending = "Failure with line ending: %s" % ( ending = "Failure with line ending: %s" % (
"".join("%s" % ord(s) for s in ending) "".join("%s" % ord(s) for s in ending)
) )
@ -344,13 +344,13 @@ def test_readline_psfile(tmp_path: Path) -> None:
assert t.readline().strip("\r\n") == "baz", ending assert t.readline().strip("\r\n") == "baz", ending
assert t.readline().strip("\r\n") == "bif", ending assert t.readline().strip("\r\n") == "bif", ending
def _test_readline_io_psfile(test_string, ending) -> None: def _test_readline_io_psfile(test_string: str, ending: str) -> None:
f = io.BytesIO(test_string.encode("latin-1")) f = io.BytesIO(test_string.encode("latin-1"))
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
t = EpsImagePlugin.PSFile(f) t = EpsImagePlugin.PSFile(f)
_test_readline(t, ending) _test_readline(t, ending)
def _test_readline_file_psfile(test_string, ending) -> None: def _test_readline_file_psfile(test_string: str, ending: str) -> None:
f = str(tmp_path / "temp.txt") f = str(tmp_path / "temp.txt")
with open(f, "wb") as w: with open(f, "wb") as w:
w.write(test_string.encode("latin-1")) w.write(test_string.encode("latin-1"))
@ -376,7 +376,7 @@ def test_psfile_deprecation() -> None:
"line_ending", "line_ending",
(b"\r\n", b"\n", b"\n\r", b"\r"), (b"\r\n", b"\n", b"\n\r", b"\r"),
) )
def test_readline(prefix, line_ending) -> None: def test_readline(prefix: bytes, line_ending: bytes) -> None:
simple_file = prefix + line_ending.join(simple_eps_file_with_comments) simple_file = prefix + line_ending.join(simple_eps_file_with_comments)
data = io.BytesIO(simple_file) data = io.BytesIO(simple_file)
test_file = EpsImagePlugin.EpsImageFile(data) test_file = EpsImagePlugin.EpsImageFile(data)
@ -394,7 +394,7 @@ def test_readline(prefix, line_ending) -> None:
"Tests/images/illuCS6_preview.eps", "Tests/images/illuCS6_preview.eps",
), ),
) )
def test_open_eps(filename) -> None: def test_open_eps(filename: str) -> None:
# https://github.com/python-pillow/Pillow/issues/1104 # https://github.com/python-pillow/Pillow/issues/1104
with Image.open(filename) as img: with Image.open(filename) as img:
assert img.mode == "RGB" assert img.mode == "RGB"
@ -417,7 +417,7 @@ def test_emptyline() -> None:
"test_file", "test_file",
["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
) )
def test_timeout(test_file) -> None: def test_timeout(test_file: str) -> None:
with open(test_file, "rb") as f: with open(test_file, "rb") as f:
with pytest.raises(Image.UnidentifiedImageError): with pytest.raises(Image.UnidentifiedImageError):
with Image.open(f): with Image.open(f):

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import warnings import warnings
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Generator
import pytest import pytest
@ -144,13 +145,13 @@ def test_strategy() -> None:
def test_optimize() -> None: def test_optimize() -> None:
def test_grayscale(optimize): def test_grayscale(optimize: int) -> int:
im = Image.new("L", (1, 1), 0) im = Image.new("L", (1, 1), 0)
filename = BytesIO() filename = BytesIO()
im.save(filename, "GIF", optimize=optimize) im.save(filename, "GIF", optimize=optimize)
return len(filename.getvalue()) return len(filename.getvalue())
def test_bilevel(optimize): def test_bilevel(optimize: int) -> int:
im = Image.new("1", (1, 1), 0) im = Image.new("1", (1, 1), 0)
test_file = BytesIO() test_file = BytesIO()
im.save(test_file, "GIF", optimize=optimize) im.save(test_file, "GIF", optimize=optimize)
@ -178,7 +179,9 @@ def test_optimize() -> None:
(4, 513, 256), (4, 513, 256),
), ),
) )
def test_optimize_correctness(colors, size, expected_palette_length) -> None: def test_optimize_correctness(
colors: int, size: int, expected_palette_length: int
) -> None:
# 256 color Palette image, posterize to > 128 and < 128 levels. # 256 color Palette image, posterize to > 128 and < 128 levels.
# Size bigger and smaller than 512x512. # Size bigger and smaller than 512x512.
# Check the palette for number of colors allocated. # Check the palette for number of colors allocated.
@ -297,7 +300,7 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None:
("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"),
), ),
) )
def test_loading_multiple_palettes(path, mode) -> None: def test_loading_multiple_palettes(path: str, mode: str) -> None:
with Image.open(path) as im: with Image.open(path) as im:
assert im.mode == "P" assert im.mode == "P"
first_frame_colors = im.palette.colors.keys() first_frame_colors = im.palette.colors.keys()
@ -347,9 +350,9 @@ def test_palette_handling(tmp_path: Path) -> None:
def test_palette_434(tmp_path: Path) -> None: def test_palette_434(tmp_path: Path) -> None:
# see https://github.com/python-pillow/Pillow/issues/434 # see https://github.com/python-pillow/Pillow/issues/434
def roundtrip(im, *args, **kwargs): def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
im.copy().save(out, *args, **kwargs) im.copy().save(out, **kwargs)
reloaded = Image.open(out) reloaded = Image.open(out)
return reloaded return reloaded
@ -429,7 +432,7 @@ def test_seek_rewind() -> None:
("Tests/images/iss634.gif", 42), ("Tests/images/iss634.gif", 42),
), ),
) )
def test_n_frames(path, n_frames) -> None: def test_n_frames(path: str, n_frames: int) -> None:
# Test is_animated before n_frames # Test is_animated before n_frames
with Image.open(path) as im: with Image.open(path) as im:
assert im.is_animated == (n_frames != 1) assert im.is_animated == (n_frames != 1)
@ -541,7 +544,10 @@ def test_dispose_background_transparency() -> None:
), ),
), ),
) )
def test_transparent_dispose(loading_strategy, expected_colors) -> None: def test_transparent_dispose(
loading_strategy: GifImagePlugin.LoadingStrategy,
expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]],
) -> None:
GifImagePlugin.LOADING_STRATEGY = loading_strategy GifImagePlugin.LOADING_STRATEGY = loading_strategy
try: try:
with Image.open("Tests/images/transparent_dispose.gif") as img: with Image.open("Tests/images/transparent_dispose.gif") as img:
@ -889,7 +895,9 @@ def test_identical_frames(tmp_path: Path) -> None:
1500, 1500,
), ),
) )
def test_identical_frames_to_single_frame(duration, tmp_path: Path) -> None: def test_identical_frames_to_single_frame(
duration: int | list[int], tmp_path: Path
) -> None:
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
im_list = [ im_list = [
Image.new("L", (100, 100), "#000"), Image.new("L", (100, 100), "#000"),
@ -1049,7 +1057,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None:
def test_version(tmp_path: Path) -> None: def test_version(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
def assert_version_after_save(im, version) -> None: def assert_version_after_save(im: Image.Image, version: bytes) -> None:
im.save(out) im.save(out)
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["version"] == version assert reread.info["version"] == version
@ -1088,7 +1096,7 @@ def test_append_images(tmp_path: Path) -> None:
assert reread.n_frames == 3 assert reread.n_frames == 3
# Tests appending using a generator # Tests appending using a generator
def im_generator(ims): def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
yield from ims yield from ims
im.save(out, save_all=True, append_images=im_generator(ims)) im.save(out, save_all=True, append_images=im_generator(ims))

View File

@ -5,6 +5,7 @@ import re
import warnings import warnings
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any
import pytest import pytest
@ -42,7 +43,7 @@ TEST_FILE = "Tests/images/hopper.jpg"
@skip_unless_feature("jpg") @skip_unless_feature("jpg")
class TestFileJpeg: class TestFileJpeg:
def roundtrip(self, im, **options): def roundtrip(self, im: Image.Image, **options: Any) -> Image.Image:
out = BytesIO() out = BytesIO()
im.save(out, "JPEG", **options) im.save(out, "JPEG", **options)
test_bytes = out.tell() test_bytes = out.tell()
@ -51,7 +52,7 @@ class TestFileJpeg:
im.bytes = test_bytes # for testing only im.bytes = test_bytes # for testing only
return im return im
def gen_random_image(self, size, mode: str = "RGB"): def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image:
"""Generates a very hard to compress file """Generates a very hard to compress file
:param size: tuple :param size: tuple
:param mode: optional image mode :param mode: optional image mode
@ -71,7 +72,7 @@ class TestFileJpeg:
assert im.get_format_mimetype() == "image/jpeg" assert im.get_format_mimetype() == "image/jpeg"
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
def test_zero(self, size, tmp_path: Path) -> None: def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg") f = str(tmp_path / "temp.jpg")
im = Image.new("RGB", size) im = Image.new("RGB", size)
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -108,13 +109,11 @@ class TestFileJpeg:
assert "comment" not in reloaded.info assert "comment" not in reloaded.info
# Test that a comment argument overrides the default comment # Test that a comment argument overrides the default comment
for comment in ("Test comment text", b"Text comment text"): for comment in ("Test comment text", b"Test comment text"):
out = BytesIO() out = BytesIO()
im.save(out, format="JPEG", comment=comment) im.save(out, format="JPEG", comment=comment)
with Image.open(out) as reloaded: with Image.open(out) as reloaded:
if not isinstance(comment, bytes): assert reloaded.info["comment"] == b"Test comment text"
comment = comment.encode()
assert reloaded.info["comment"] == comment
def test_cmyk(self) -> None: def test_cmyk(self) -> None:
# Test CMYK handling. Thanks to Tim and Charlie for test data, # Test CMYK handling. Thanks to Tim and Charlie for test data,
@ -145,7 +144,7 @@ class TestFileJpeg:
assert k > 0.9 assert k > 0.9
def test_rgb(self) -> None: def test_rgb(self) -> None:
def getchannels(im): def getchannels(im: Image.Image) -> tuple[int, int, int]:
return tuple(v[0] for v in im.layer) return tuple(v[0] for v in im.layer)
im = hopper() im = hopper()
@ -161,8 +160,8 @@ class TestFileJpeg:
"test_image_path", "test_image_path",
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
) )
def test_dpi(self, test_image_path) -> None: def test_dpi(self, test_image_path: str) -> None:
def test(xdpi, ydpi=None): def test(xdpi: int, ydpi: int | None = None):
with Image.open(test_image_path) as im: with Image.open(test_image_path) as im:
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
return im.info.get("dpi") return im.info.get("dpi")
@ -207,7 +206,7 @@ class TestFileJpeg:
ImageFile.MAXBLOCK * 4 + 3, # large block ImageFile.MAXBLOCK * 4 + 3, # large block
), ),
) )
def test_icc_big(self, n) -> None: def test_icc_big(self, n: int) -> None:
# Make sure that the "extra" support handles large blocks # Make sure that the "extra" support handles large blocks
# The ICC APP marker can store 65519 bytes per marker, so # The ICC APP marker can store 65519 bytes per marker, so
# using a 4-byte test code should allow us to detect out of # using a 4-byte test code should allow us to detect out of
@ -433,7 +432,7 @@ class TestFileJpeg:
assert_image(im1, im2.mode, im2.size) assert_image(im1, im2.mode, im2.size)
def test_subsampling(self) -> None: def test_subsampling(self) -> None:
def getsampling(im): def getsampling(im: Image.Image):
layer = im.layer layer = im.layer
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
@ -530,7 +529,7 @@ class TestFileJpeg:
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
) )
def test_qtables(self, tmp_path: Path) -> None: def test_qtables(self, tmp_path: Path) -> None:
def _n_qtables_helper(n, test_file) -> None: def _n_qtables_helper(n: int, test_file: str) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
f = str(tmp_path / "temp.jpg") f = str(tmp_path / "temp.jpg")
im.save(f, qtables=[[n] * 64] * n) im.save(f, qtables=[[n] * 64] * n)
@ -666,7 +665,7 @@ class TestFileJpeg:
"blocks, rows, markers", "blocks, rows, markers",
((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)), ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)),
) )
def test_restart_markers(self, blocks, rows, markers) -> None: def test_restart_markers(self, blocks: int, rows: int, markers: int) -> None:
im = Image.new("RGB", (32, 32)) # 16 MCUs im = Image.new("RGB", (32, 32)) # 16 MCUs
out = BytesIO() out = BytesIO()
im.save( im.save(
@ -724,7 +723,7 @@ class TestFileJpeg:
assert im.format == "JPEG" assert im.format == "JPEG"
@pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr"))
def test_save_correct_modes(self, mode) -> None: def test_save_correct_modes(self, mode: str) -> None:
out = BytesIO() out = BytesIO()
img = Image.new(mode, (20, 20)) img = Image.new(mode, (20, 20))
img.save(out, "JPEG") img.save(out, "JPEG")
@ -993,12 +992,12 @@ class TestFileJpeg:
# Even though this decoder never says that it is finished # Even though this decoder never says that it is finished
# the image should still end when there is no new data # the image should still end when there is no new data
class InfiniteMockPyDecoder(ImageFile.PyDecoder): class InfiniteMockPyDecoder(ImageFile.PyDecoder):
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
return 0, 0 return 0, 0
decoder = InfiniteMockPyDecoder(None) decoder = InfiniteMockPyDecoder(None)
def closure(mode, *args): def closure(mode: str, *args) -> InfiniteMockPyDecoder:
decoder.__init__(mode, *args) decoder.__init__(mode, *args)
return decoder return decoder

View File

@ -4,6 +4,7 @@ import os
import re import re
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any
import pytest import pytest
@ -36,7 +37,7 @@ test_card.load()
# 'Not enough memory to handle tile data' # 'Not enough memory to handle tile data'
def roundtrip(im, **options): def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
out = BytesIO() out = BytesIO()
im.save(out, "JPEG2000", **options) im.save(out, "JPEG2000", **options)
test_bytes = out.tell() test_bytes = out.tell()
@ -138,7 +139,7 @@ def test_prog_res_rt() -> None:
@pytest.mark.parametrize("num_resolutions", range(2, 6)) @pytest.mark.parametrize("num_resolutions", range(2, 6))
def test_default_num_resolutions(num_resolutions) -> None: def test_default_num_resolutions(num_resolutions: int) -> None:
d = 1 << (num_resolutions - 1) d = 1 << (num_resolutions - 1)
im = test_card.resize((d - 1, d - 1)) im = test_card.resize((d - 1, d - 1))
with pytest.raises(OSError): with pytest.raises(OSError):
@ -198,9 +199,9 @@ def test_layers_type(tmp_path: Path) -> None:
for quality_layers in [[100, 50, 10], (100, 50, 10), None]: for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
test_card.save(outfile, quality_layers=quality_layers) test_card.save(outfile, quality_layers=quality_layers)
for quality_layers in ["quality_layers", ("100", "50", "10")]: for quality_layers_str in ["quality_layers", ("100", "50", "10")]:
with pytest.raises(ValueError): with pytest.raises(ValueError):
test_card.save(outfile, quality_layers=quality_layers) test_card.save(outfile, quality_layers=quality_layers_str)
def test_layers() -> None: def test_layers() -> None:
@ -233,7 +234,7 @@ def test_layers() -> None:
("foo.jp2", {"no_jp2": False}, 4, b"jP"), ("foo.jp2", {"no_jp2": False}, 4, b"jP"),
), ),
) )
def test_no_jp2(name, args, offset, data) -> None: def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None:
out = BytesIO() out = BytesIO()
if name: if name:
out.name = name out.name = name
@ -278,7 +279,7 @@ def test_sgnd(tmp_path: Path) -> None:
@pytest.mark.parametrize("ext", (".j2k", ".jp2")) @pytest.mark.parametrize("ext", (".j2k", ".jp2"))
def test_rgba(ext) -> None: def test_rgba(ext: str) -> None:
# Arrange # Arrange
with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im:
# Act # Act
@ -289,7 +290,7 @@ def test_rgba(ext) -> None:
@pytest.mark.parametrize("ext", (".j2k", ".jp2")) @pytest.mark.parametrize("ext", (".j2k", ".jp2"))
def test_16bit_monochrome_has_correct_mode(ext) -> None: def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
with Image.open("Tests/images/16bit.cropped" + ext) as im: with Image.open("Tests/images/16bit.cropped" + ext) as im:
im.load() im.load()
assert im.mode == "I;16" assert im.mode == "I;16"
@ -346,12 +347,12 @@ def test_parser_feed() -> None:
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
) )
@pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) @pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2"))
def test_subsampling_decode(name) -> None: def test_subsampling_decode(name: str) -> None:
test = f"{EXTRA_DIR}/{name}.jp2" test = f"{EXTRA_DIR}/{name}.jp2"
reference = f"{EXTRA_DIR}/{name}.ppm" reference = f"{EXTRA_DIR}/{name}.ppm"
with Image.open(test) as im: with Image.open(test) as im:
epsilon = 3 # for YCbCr images epsilon = 3.0 # for YCbCr images
with Image.open(reference) as im2: with Image.open(reference) as im2:
width, height = im2.size width, height = im2.size
if name[-1] == "2": if name[-1] == "2":
@ -400,7 +401,7 @@ def test_save_comment() -> None:
"Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k",
], ],
) )
def test_crashes(test_file) -> None: def test_crashes(test_file: str) -> None:
with open(test_file, "rb") as f: with open(test_file, "rb") as f:
with Image.open(f) as im: with Image.open(f) as im:
# Valgrind should not complain here # Valgrind should not complain here

View File

@ -27,7 +27,7 @@ from .helper import (
@skip_unless_feature("libtiff") @skip_unless_feature("libtiff")
class LibTiffTestCase: class LibTiffTestCase:
def _assert_noerr(self, tmp_path: Path, im) -> None: def _assert_noerr(self, tmp_path: Path, im: Image.Image) -> None:
"""Helper tests that assert basic sanity about the g4 tiff reading""" """Helper tests that assert basic sanity about the g4 tiff reading"""
# 1 bit # 1 bit
assert im.mode == "1" assert im.mode == "1"
@ -140,7 +140,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
@pytest.mark.parametrize("legacy_api", (False, True)) @pytest.mark.parametrize("legacy_api", (False, True))
def test_write_metadata(self, legacy_api, tmp_path: Path) -> None: def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None:
"""Test metadata writing through libtiff""" """Test metadata writing through libtiff"""
f = str(tmp_path / "temp.tiff") f = str(tmp_path / "temp.tiff")
with Image.open("Tests/images/hopper_g4.tif") as img: with Image.open("Tests/images/hopper_g4.tif") as img:
@ -243,7 +243,7 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.WRITE_LIBTIFF = False
def test_custom_metadata(self, tmp_path: Path) -> None: def test_custom_metadata(self, tmp_path: Path) -> None:
tc = namedtuple("test_case", "value,type,supported_by_default") tc = namedtuple("tc", "value,type,supported_by_default")
custom = { custom = {
37000 + k: v 37000 + k: v
for k, v in enumerate( for k, v in enumerate(
@ -284,7 +284,9 @@ class TestFileLibTiff(LibTiffTestCase):
for libtiff in libtiffs: for libtiff in libtiffs:
TiffImagePlugin.WRITE_LIBTIFF = libtiff TiffImagePlugin.WRITE_LIBTIFF = libtiff
def check_tags(tiffinfo) -> None: def check_tags(
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
) -> None:
im = hopper() im = hopper()
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
@ -502,7 +504,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, out) assert_image_equal_tofile(im, out)
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
def test_palette_save(self, im, tmp_path: Path) -> None: def test_palette_save(self, im: Image.Image, tmp_path: Path) -> None:
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
TiffImagePlugin.WRITE_LIBTIFF = True TiffImagePlugin.WRITE_LIBTIFF = True
@ -514,7 +516,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert len(reloaded.tag_v2[320]) == 768 assert len(reloaded.tag_v2[320]) == 768
@pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
def test_bw_compression_w_rgb(self, compression, tmp_path: Path) -> None: def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None:
im = hopper("RGB") im = hopper("RGB")
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
@ -647,7 +649,7 @@ class TestFileLibTiff(LibTiffTestCase):
# Generate test image # Generate test image
pilim = hopper() pilim = hopper()
def save_bytesio(compression=None) -> None: def save_bytesio(compression: str | None = None) -> None:
buffer_io = io.BytesIO() buffer_io = io.BytesIO()
pilim.save(buffer_io, format="tiff", compression=compression) pilim.save(buffer_io, format="tiff", compression=compression)
buffer_io.seek(0) buffer_io.seek(0)
@ -731,7 +733,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert icc == icc_libtiff assert icc == icc_libtiff
def test_write_icc(self, tmp_path: Path) -> None: def test_write_icc(self, tmp_path: Path) -> None:
def check_write(libtiff) -> None: def check_write(libtiff: bool) -> None:
TiffImagePlugin.WRITE_LIBTIFF = libtiff TiffImagePlugin.WRITE_LIBTIFF = libtiff
with Image.open("Tests/images/hopper.iccprofile.tif") as img: with Image.open("Tests/images/hopper.iccprofile.tif") as img:
@ -837,7 +839,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.mode == "F" assert reloaded.mode == "F"
assert reloaded.getexif()[SAMPLEFORMAT] == 3 assert reloaded.getexif()[SAMPLEFORMAT] == 3
def test_lzma(self, capfd): def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None:
try: try:
with Image.open("Tests/images/hopper_lzma.tif") as im: with Image.open("Tests/images/hopper_lzma.tif") as im:
assert im.mode == "RGB" assert im.mode == "RGB"
@ -853,7 +855,7 @@ class TestFileLibTiff(LibTiffTestCase):
sys.stderr.write(captured.err) sys.stderr.write(captured.err)
raise raise
def test_webp(self, capfd): def test_webp(self, capfd: pytest.CaptureFixture[str]) -> None:
try: try:
with Image.open("Tests/images/hopper_webp.tif") as im: with Image.open("Tests/images/hopper_webp.tif") as im:
assert im.mode == "RGB" assert im.mode == "RGB"
@ -971,7 +973,7 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png")
@pytest.mark.parametrize("compression", (None, "jpeg")) @pytest.mark.parametrize("compression", (None, "jpeg"))
def test_block_tile_tags(self, compression, tmp_path: Path) -> None: def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None:
im = hopper() im = hopper()
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
@ -1020,7 +1022,9 @@ class TestFileLibTiff(LibTiffTestCase):
), ),
], ],
) )
def test_wrong_bits_per_sample(self, file_name, mode, size, tile) -> None: def test_wrong_bits_per_sample(
self, file_name: str, mode: str, size: tuple[int, int], tile
) -> None:
with Image.open("Tests/images/" + file_name) as im: with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode assert im.mode == mode
assert im.size == size assert im.size == size
@ -1086,7 +1090,7 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.READ_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
def test_save_multistrip(self, compression, tmp_path: Path) -> None: def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
im = hopper("RGB").resize((256, 256)) im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
im.save(out, compression=compression) im.save(out, compression=compression)
@ -1096,14 +1100,14 @@ class TestFileLibTiff(LibTiffTestCase):
assert len(im.tag_v2[STRIPOFFSETS]) > 1 assert len(im.tag_v2[STRIPOFFSETS]) > 1
@pytest.mark.parametrize("argument", (True, False)) @pytest.mark.parametrize("argument", (True, False))
def test_save_single_strip(self, argument, tmp_path: Path) -> None: def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None:
im = hopper("RGB").resize((256, 256)) im = hopper("RGB").resize((256, 256))
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
if not argument: if not argument:
TiffImagePlugin.STRIP_SIZE = 2**18 TiffImagePlugin.STRIP_SIZE = 2**18
try: try:
arguments = {"compression": "tiff_adobe_deflate"} arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument: if argument:
arguments["strip_size"] = 2**18 arguments["strip_size"] = 2**18
im.save(out, **arguments) im.save(out, **arguments)
@ -1114,7 +1118,7 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.STRIP_SIZE = 65536 TiffImagePlugin.STRIP_SIZE = 65536
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None))
def test_save_zero(self, compression, tmp_path: Path) -> None: def test_save_zero(self, compression: str | None, tmp_path: Path) -> None:
im = Image.new("RGB", (0, 0)) im = Image.new("RGB", (0, 0))
out = str(tmp_path / "temp.tif") out = str(tmp_path / "temp.tif")
with pytest.raises(SystemError): with pytest.raises(SystemError):
@ -1134,7 +1138,7 @@ class TestFileLibTiff(LibTiffTestCase):
("Tests/images/child_ifd_jpeg.tiff", (20,)), ("Tests/images/child_ifd_jpeg.tiff", (20,)),
), ),
) )
def test_get_child_images(self, path, sizes) -> None: def test_get_child_images(self, path: str, sizes: tuple[int, ...]) -> None:
with Image.open(path) as im: with Image.open(path) as im:
ims = im.get_child_images() ims = im.get_child_images()

View File

@ -9,7 +9,6 @@ from .test_file_libtiff import LibTiffTestCase
class TestFileLibTiffSmall(LibTiffTestCase): class TestFileLibTiffSmall(LibTiffTestCase):
"""The small lena image was failing on open in the libtiff """The small lena image was failing on open in the libtiff
decoder because the file pointer was set to the wrong place decoder because the file pointer was set to the wrong place
by a spurious seek. It wasn't failing with the byteio method. by a spurious seek. It wasn't failing with the byteio method.

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import warnings import warnings
from io import BytesIO from io import BytesIO
from typing import Any
import pytest import pytest
@ -19,7 +20,7 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
pytestmark = skip_unless_feature("jpg") pytestmark = skip_unless_feature("jpg")
def roundtrip(im, **options): def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
out = BytesIO() out = BytesIO()
im.save(out, "MPO", **options) im.save(out, "MPO", **options)
test_bytes = out.tell() test_bytes = out.tell()
@ -30,7 +31,7 @@ def roundtrip(im, **options):
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
def test_sanity(test_file) -> None: def test_sanity(test_file: str) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
im.load() im.load()
assert im.mode == "RGB" assert im.mode == "RGB"
@ -70,7 +71,7 @@ def test_context_manager() -> None:
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
def test_app(test_file) -> None: def test_app(test_file: str) -> None:
# Test APP/COM reader (@PIL135) # Test APP/COM reader (@PIL135)
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.applist[0][0] == "APP1" assert im.applist[0][0] == "APP1"
@ -82,7 +83,7 @@ def test_app(test_file) -> None:
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
def test_exif(test_file) -> None: def test_exif(test_file: str) -> None:
with Image.open(test_file) as im_original: with Image.open(test_file) as im_original:
im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif())
@ -143,7 +144,7 @@ def test_reload_exif_after_seek() -> None:
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
def test_mp(test_file) -> None: def test_mp(test_file: str) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
assert mpinfo[45056] == b"0100" assert mpinfo[45056] == b"0100"
@ -168,7 +169,7 @@ def test_mp_no_data() -> None:
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
def test_mp_attribute(test_file) -> None: def test_mp_attribute(test_file: str) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
mpinfo = im._getmp() mpinfo = im._getmp()
for frame_number, mpentry in enumerate(mpinfo[0xB002]): for frame_number, mpentry in enumerate(mpinfo[0xB002]):
@ -185,7 +186,7 @@ def test_mp_attribute(test_file) -> None:
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
def test_seek(test_file) -> None: def test_seek(test_file: str) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.tell() == 0 assert im.tell() == 0
# prior to first image raises an error, both blatant and borderline # prior to first image raises an error, both blatant and borderline
@ -229,7 +230,7 @@ def test_eoferror() -> None:
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
def test_image_grab(test_file) -> None: def test_image_grab(test_file: str) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.tell() == 0 assert im.tell() == 0
im0 = im.tobytes() im0 = im.tobytes()
@ -244,7 +245,7 @@ def test_image_grab(test_file) -> None:
@pytest.mark.parametrize("test_file", test_files) @pytest.mark.parametrize("test_file", test_files)
def test_save(test_file) -> None: def test_save(test_file: str) -> None:
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.tell() == 0 assert im.tell() == 0
jpg0 = roundtrip(im) jpg0 = roundtrip(im)

View File

@ -6,6 +6,7 @@ import warnings
import zlib import zlib
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any
import pytest import pytest
@ -36,7 +37,7 @@ TEST_PNG_FILE = "Tests/images/hopper.png"
MAGIC = PngImagePlugin._MAGIC MAGIC = PngImagePlugin._MAGIC
def chunk(cid, *data): def chunk(cid: bytes, *data: bytes) -> bytes:
test_file = BytesIO() test_file = BytesIO()
PngImagePlugin.putchunk(*(test_file, cid) + data) PngImagePlugin.putchunk(*(test_file, cid) + data)
return test_file.getvalue() return test_file.getvalue()
@ -52,11 +53,11 @@ HEAD = MAGIC + IHDR
TAIL = IDAT + IEND TAIL = IDAT + IEND
def load(data): def load(data: bytes) -> Image.Image:
return Image.open(BytesIO(data)) return Image.open(BytesIO(data))
def roundtrip(im, **options): def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
out = BytesIO() out = BytesIO()
im.save(out, "PNG", **options) im.save(out, "PNG", **options)
out.seek(0) out.seek(0)
@ -65,7 +66,7 @@ def roundtrip(im, **options):
@skip_unless_feature("zlib") @skip_unless_feature("zlib")
class TestFilePng: class TestFilePng:
def get_chunks(self, filename): def get_chunks(self, filename: str) -> list[bytes]:
chunks = [] chunks = []
with open(filename, "rb") as fp: with open(filename, "rb") as fp:
fp.read(8) fp.read(8)
@ -444,7 +445,7 @@ class TestFilePng:
def test_unicode_text(self) -> None: def test_unicode_text(self) -> None:
# Check preservation of non-ASCII characters # Check preservation of non-ASCII characters
def rt_text(value) -> None: def rt_text(value: str) -> None:
im = Image.new("RGB", (32, 32)) im = Image.new("RGB", (32, 32))
info = PngImagePlugin.PngInfo() info = PngImagePlugin.PngInfo()
info.add_text("Text", value) info.add_text("Text", value)
@ -644,7 +645,7 @@ class TestFilePng:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT")
) )
def test_truncated_chunks(self, cid) -> None: def test_truncated_chunks(self, cid: bytes) -> None:
fp = BytesIO() fp = BytesIO()
with PngImagePlugin.PngStream(fp) as png: with PngImagePlugin.PngStream(fp) as png:
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -763,7 +764,7 @@ class TestFilePng:
im.seek(1) im.seek(1)
@pytest.mark.parametrize("buffer", (True, False)) @pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(self, buffer) -> None: def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout old_stdout = sys.stdout
if buffer: if buffer:

View File

@ -70,7 +70,9 @@ def test_sanity() -> None:
), ),
), ),
) )
def test_arbitrary_maxval(data, mode, pixels) -> None: def test_arbitrary_maxval(
data: bytes, mode: str, pixels: tuple[int | tuple[int, int, int], ...]
) -> None:
fp = BytesIO(data) fp = BytesIO(data)
with Image.open(fp) as im: with Image.open(fp) as im:
assert im.size == (3, 1) assert im.size == (3, 1)
@ -139,7 +141,7 @@ def test_pfm_big_endian(tmp_path: Path) -> None:
b"Pf 1 1 -0.0 \0\0\0\0", b"Pf 1 1 -0.0 \0\0\0\0",
], ],
) )
def test_pfm_invalid(data) -> None: def test_pfm_invalid(data: bytes) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
with Image.open(BytesIO(data)): with Image.open(BytesIO(data)):
pass pass
@ -162,7 +164,7 @@ def test_pfm_invalid(data) -> None:
), ),
), ),
) )
def test_plain(plain_path, raw_path) -> None: def test_plain(plain_path: str, raw_path: str) -> None:
with Image.open(plain_path) as im: with Image.open(plain_path) as im:
assert_image_equal_tofile(im, raw_path) assert_image_equal_tofile(im, raw_path)
@ -186,7 +188,9 @@ def test_16bit_plain_pgm() -> None:
(b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6), (b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6),
), ),
) )
def test_plain_data_with_comment(tmp_path: Path, header, data, comment_count) -> None: def test_plain_data_with_comment(
tmp_path: Path, header: bytes, data: bytes, comment_count: int
) -> None:
path1 = str(tmp_path / "temp1.ppm") path1 = str(tmp_path / "temp1.ppm")
path2 = str(tmp_path / "temp2.ppm") path2 = str(tmp_path / "temp2.ppm")
comment = b"# comment" * comment_count comment = b"# comment" * comment_count
@ -199,7 +203,7 @@ def test_plain_data_with_comment(tmp_path: Path, header, data, comment_count) ->
@pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n"))
def test_plain_truncated_data(tmp_path: Path, data) -> None: def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None:
path = str(tmp_path / "temp.ppm") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(data) f.write(data)
@ -210,7 +214,7 @@ def test_plain_truncated_data(tmp_path: Path, data) -> None:
@pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A"))
def test_plain_invalid_data(tmp_path: Path, data) -> None: def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None:
path = str(tmp_path / "temp.ppm") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(data) f.write(data)
@ -227,7 +231,7 @@ def test_plain_invalid_data(tmp_path: Path, data) -> None:
b"P3\n128 128\n255\n012345678910 0", # token too long b"P3\n128 128\n255\n012345678910 0", # token too long
), ),
) )
def test_plain_ppm_token_too_long(tmp_path: Path, data) -> None: def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:
path = str(tmp_path / "temp.ppm") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(data) f.write(data)
@ -313,7 +317,7 @@ def test_not_enough_image_data(tmp_path: Path) -> None:
@pytest.mark.parametrize("maxval", (b"0", b"65536")) @pytest.mark.parametrize("maxval", (b"0", b"65536"))
def test_invalid_maxval(maxval, tmp_path: Path) -> None: def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None:
path = str(tmp_path / "temp.ppm") path = str(tmp_path / "temp.ppm")
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(b"P6\n3 1 " + maxval) f.write(b"P6\n3 1 " + maxval)
@ -351,7 +355,7 @@ def test_mimetypes(tmp_path: Path) -> None:
@pytest.mark.parametrize("buffer", (True, False)) @pytest.mark.parametrize("buffer", (True, False))
def test_save_stdout(buffer) -> None: def test_save_stdout(buffer: bool) -> None:
old_stdout = sys.stdout old_stdout = sys.stdout
if buffer: if buffer:

View File

@ -72,7 +72,7 @@ def test_invalid_file() -> None:
def test_write(tmp_path: Path) -> None: def test_write(tmp_path: Path) -> None:
def roundtrip(img) -> None: def roundtrip(img: Image.Image) -> None:
out = str(tmp_path / "temp.sgi") out = str(tmp_path / "temp.sgi")
img.save(out, format="sgi") img.save(out, format="sgi")
assert_image_equal_tofile(img, out) assert_image_equal_tofile(img, out)

View File

@ -230,9 +230,7 @@ class TestImageGetPixel(AccessTest):
assert im.getpixel([0, 0]) == (20, 20, 70) assert im.getpixel([0, 0]) == (20, 20, 70)
@pytest.mark.parametrize("mode", ("I;16", "I;16B")) @pytest.mark.parametrize("mode", ("I;16", "I;16B"))
@pytest.mark.parametrize( @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1))
"expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)
)
def test_signedness(self, mode, expected_color) -> None: def test_signedness(self, mode, expected_color) -> None:
# see https://github.com/python-pillow/Pillow/issues/452 # see https://github.com/python-pillow/Pillow/issues/452
# pixelaccess is using signed int* instead of uint* # pixelaccess is using signed int* instead of uint*

View File

@ -8,7 +8,7 @@ from .helper import assert_image_equal, hopper
@pytest.mark.parametrize("data_type", ("bytes", "memoryview")) @pytest.mark.parametrize("data_type", ("bytes", "memoryview"))
def test_sanity(data_type) -> None: def test_sanity(data_type: str) -> None:
im1 = hopper() im1 = hopper()
data = im1.tobytes() data = im1.tobytes()

View File

@ -26,7 +26,7 @@ def test_close() -> None:
im.getpixel((0, 0)) im.getpixel((0, 0))
def test_close_after_load(caplog) -> None: def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None:
im = Image.open("Tests/images/hopper.gif") im = Image.open("Tests/images/hopper.gif")
im.load() im.load()
with caplog.at_level(logging.DEBUG): with caplog.at_level(logging.DEBUG):

View File

@ -11,10 +11,9 @@ class TestImagingPaste:
masks = {} masks = {}
size = 128 size = 128
def assert_9points_image(self, im, expected) -> None: def assert_9points_image(
expected = [ self, im: Image.Image, expected: list[tuple[int, int, int, int]]
point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected ) -> None:
]
px = im.load() px = im.load()
actual = [ actual = [
px[0, 0], px[0, 0],
@ -27,9 +26,17 @@ class TestImagingPaste:
px[self.size // 2, self.size - 1], px[self.size // 2, self.size - 1],
px[self.size - 1, self.size - 1], px[self.size - 1, self.size - 1],
] ]
assert actual == expected assert actual == [
point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected
]
def assert_9points_paste(self, im, im2, mask, expected) -> None: def assert_9points_paste(
self,
im: Image.Image,
im2: Image.Image,
mask: Image.Image,
expected: list[tuple[int, int, int, int]],
) -> None:
im3 = im.copy() im3 = im.copy()
im3.paste(im2, (0, 0), mask) im3.paste(im2, (0, 0), mask)
self.assert_9points_image(im3, expected) self.assert_9points_image(im3, expected)
@ -106,7 +113,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_solid(self, mode) -> None: def test_image_solid(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "red") im = Image.new(mode, (200, 200), "red")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -116,7 +123,7 @@ class TestImagingPaste:
assert_image_equal(im, im2) assert_image_equal(im, im2)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_mask_1(self, mode) -> None: def test_image_mask_1(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -138,7 +145,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_mask_L(self, mode) -> None: def test_image_mask_L(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -160,7 +167,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_mask_LA(self, mode) -> None: def test_image_mask_LA(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -182,7 +189,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_mask_RGBA(self, mode) -> None: def test_image_mask_RGBA(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -204,7 +211,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_mask_RGBa(self, mode) -> None: def test_image_mask_RGBa(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -226,7 +233,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_color_solid(self, mode) -> None: def test_color_solid(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "black") im = Image.new(mode, (200, 200), "black")
rect = (12, 23, 128 + 12, 128 + 23) rect = (12, 23, 128 + 12, 128 + 23)
@ -239,7 +246,7 @@ class TestImagingPaste:
assert sum(head[:255]) == 0 assert sum(head[:255]) == 0
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_color_mask_1(self, mode) -> None: def test_color_mask_1(self, mode: str) -> None:
im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)])
color = (10, 20, 30, 40)[: len(mode)] color = (10, 20, 30, 40)[: len(mode)]
@ -261,7 +268,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_color_mask_L(self, mode) -> None: def test_color_mask_L(self, mode: str) -> None:
im = getattr(self, "gradient_" + mode).copy() im = getattr(self, "gradient_" + mode).copy()
color = "white" color = "white"
@ -283,7 +290,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_color_mask_RGBA(self, mode) -> None: def test_color_mask_RGBA(self, mode: str) -> None:
im = getattr(self, "gradient_" + mode).copy() im = getattr(self, "gradient_" + mode).copy()
color = "white" color = "white"
@ -305,7 +312,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_color_mask_RGBa(self, mode) -> None: def test_color_mask_RGBa(self, mode: str) -> None:
im = getattr(self, "gradient_" + mode).copy() im = getattr(self, "gradient_" + mode).copy()
color = "white" color = "white"

View File

@ -48,7 +48,7 @@ gradients_image.load()
((1, 3), (10, 4)), ((1, 3), (10, 4)),
), ),
) )
def test_args_factor(size, expected) -> None: def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) -> None:
im = Image.new("L", (10, 10)) im = Image.new("L", (10, 10))
assert expected == im.reduce(size).size assert expected == im.reduce(size).size
@ -56,7 +56,7 @@ def test_args_factor(size, expected) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError))
) )
def test_args_factor_error(size, expected_error) -> None: def test_args_factor_error(size: float | tuple[int, int], expected_error) -> None:
im = Image.new("L", (10, 10)) im = Image.new("L", (10, 10))
with pytest.raises(expected_error): with pytest.raises(expected_error):
im.reduce(size) im.reduce(size)
@ -69,7 +69,7 @@ def test_args_factor_error(size, expected_error) -> None:
((5, 5, 6, 6), (1, 1)), ((5, 5, 6, 6), (1, 1)),
), ),
) )
def test_args_box(size, expected) -> None: def test_args_box(size: tuple[int, int, int, int], expected: tuple[int, int]) -> None:
im = Image.new("L", (10, 10)) im = Image.new("L", (10, 10))
assert expected == im.reduce(2, size).size assert expected == im.reduce(2, size).size
@ -86,20 +86,20 @@ def test_args_box(size, expected) -> None:
((5, 0, 5, 10), ValueError), ((5, 0, 5, 10), ValueError),
), ),
) )
def test_args_box_error(size, expected_error) -> None: def test_args_box_error(size: str | tuple[int, int, int, int], expected_error) -> None:
im = Image.new("L", (10, 10)) im = Image.new("L", (10, 10))
with pytest.raises(expected_error): with pytest.raises(expected_error):
im.reduce(2, size).size im.reduce(2, size).size
@pytest.mark.parametrize("mode", ("P", "1", "I;16")) @pytest.mark.parametrize("mode", ("P", "1", "I;16"))
def test_unsupported_modes(mode) -> None: def test_unsupported_modes(mode: str) -> None:
im = Image.new("P", (10, 10)) im = Image.new("P", (10, 10))
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.reduce(3) im.reduce(3)
def get_image(mode): def get_image(mode: str) -> Image.Image:
mode_info = ImageMode.getmode(mode) mode_info = ImageMode.getmode(mode)
if mode_info.basetype == "L": if mode_info.basetype == "L":
bands = [gradients_image] bands = [gradients_image]
@ -119,7 +119,7 @@ def get_image(mode):
return im.crop((0, 0, im.width, im.height - 5)) return im.crop((0, 0, im.width, im.height - 5))
def compare_reduce_with_box(im, factor) -> None: def compare_reduce_with_box(im: Image.Image, factor: int | tuple[int, int]) -> None:
box = (11, 13, 146, 164) box = (11, 13, 146, 164)
reduced = im.reduce(factor, box=box) reduced = im.reduce(factor, box=box)
reference = im.crop(box).reduce(factor) reference = im.crop(box).reduce(factor)
@ -127,7 +127,10 @@ def compare_reduce_with_box(im, factor) -> None:
def compare_reduce_with_reference( def compare_reduce_with_reference(
im, factor, average_diff: float = 0.4, max_diff: int = 1 im: Image.Image,
factor: int | tuple[int, int],
average_diff: float = 0.4,
max_diff: int = 1,
) -> None: ) -> None:
"""Image.reduce() should look very similar to Image.resize(BOX). """Image.reduce() should look very similar to Image.resize(BOX).
@ -173,7 +176,9 @@ def compare_reduce_with_reference(
assert_compare_images(reduced, reference, average_diff, max_diff) assert_compare_images(reduced, reference, average_diff, max_diff)
def assert_compare_images(a, b, max_average_diff, max_diff: int = 255) -> None: def assert_compare_images(
a: Image.Image, b: Image.Image, max_average_diff: float, max_diff: int = 255
) -> None:
assert a.mode == b.mode, f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.mode == b.mode, f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
assert a.size == b.size, f"got size {repr(a.size)}, expected {repr(b.size)}" assert a.size == b.size, f"got size {repr(a.size)}, expected {repr(b.size)}"
@ -201,20 +206,20 @@ def assert_compare_images(a, b, max_average_diff, max_diff: int = 255) -> None:
@pytest.mark.parametrize("factor", remarkable_factors) @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_L(factor) -> None: def test_mode_L(factor: int | tuple[int, int]) -> None:
im = get_image("L") im = get_image("L")
compare_reduce_with_reference(im, factor) compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)
@pytest.mark.parametrize("factor", remarkable_factors) @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_LA(factor) -> None: def test_mode_LA(factor: int | tuple[int, int]) -> None:
im = get_image("LA") im = get_image("LA")
compare_reduce_with_reference(im, factor, 0.8, 5) compare_reduce_with_reference(im, factor, 0.8, 5)
@pytest.mark.parametrize("factor", remarkable_factors) @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_LA_opaque(factor) -> None: def test_mode_LA_opaque(factor: int | tuple[int, int]) -> None:
im = get_image("LA") im = get_image("LA")
# With opaque alpha, an error should be way smaller. # With opaque alpha, an error should be way smaller.
im.putalpha(Image.new("L", im.size, 255)) im.putalpha(Image.new("L", im.size, 255))
@ -223,27 +228,27 @@ def test_mode_LA_opaque(factor) -> None:
@pytest.mark.parametrize("factor", remarkable_factors) @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_La(factor) -> None: def test_mode_La(factor: int | tuple[int, int]) -> None:
im = get_image("La") im = get_image("La")
compare_reduce_with_reference(im, factor) compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)
@pytest.mark.parametrize("factor", remarkable_factors) @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_RGB(factor) -> None: def test_mode_RGB(factor: int | tuple[int, int]) -> None:
im = get_image("RGB") im = get_image("RGB")
compare_reduce_with_reference(im, factor) compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)
@pytest.mark.parametrize("factor", remarkable_factors) @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_RGBA(factor) -> None: def test_mode_RGBA(factor: int | tuple[int, int]) -> None:
im = get_image("RGBA") im = get_image("RGBA")
compare_reduce_with_reference(im, factor, 0.8, 5) compare_reduce_with_reference(im, factor, 0.8, 5)
@pytest.mark.parametrize("factor", remarkable_factors) @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_RGBA_opaque(factor) -> None: def test_mode_RGBA_opaque(factor: int | tuple[int, int]) -> None:
im = get_image("RGBA") im = get_image("RGBA")
# With opaque alpha, an error should be way smaller. # With opaque alpha, an error should be way smaller.
im.putalpha(Image.new("L", im.size, 255)) im.putalpha(Image.new("L", im.size, 255))
@ -252,21 +257,21 @@ def test_mode_RGBA_opaque(factor) -> None:
@pytest.mark.parametrize("factor", remarkable_factors) @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_RGBa(factor) -> None: def test_mode_RGBa(factor: int | tuple[int, int]) -> None:
im = get_image("RGBa") im = get_image("RGBa")
compare_reduce_with_reference(im, factor) compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)
@pytest.mark.parametrize("factor", remarkable_factors) @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_I(factor) -> None: def test_mode_I(factor: int | tuple[int, int]) -> None:
im = get_image("I") im = get_image("I")
compare_reduce_with_reference(im, factor) compare_reduce_with_reference(im, factor)
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)
@pytest.mark.parametrize("factor", remarkable_factors) @pytest.mark.parametrize("factor", remarkable_factors)
def test_mode_F(factor) -> None: def test_mode_F(factor: int | tuple[int, int]) -> None:
im = get_image("F") im = get_image("F")
compare_reduce_with_reference(im, factor, 0, 0) compare_reduce_with_reference(im, factor, 0, 0)
compare_reduce_with_box(im, factor) compare_reduce_with_box(im, factor)

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from contextlib import contextmanager from contextlib import contextmanager
from typing import Generator
import pytest import pytest
@ -51,7 +52,7 @@ class TestImagingResampleVulnerability:
class TestImagingCoreResampleAccuracy: class TestImagingCoreResampleAccuracy:
def make_case(self, mode, size, color): def make_case(self, mode: str, size: tuple[int, int], color: int) -> Image.Image:
"""Makes a sample image with two dark and two bright squares. """Makes a sample image with two dark and two bright squares.
For example: For example:
e0 e0 1f 1f e0 e0 1f 1f
@ -66,7 +67,7 @@ class TestImagingCoreResampleAccuracy:
return Image.merge(mode, [case] * len(mode)) return Image.merge(mode, [case] * len(mode))
def make_sample(self, data, size): def make_sample(self, data: str, size: tuple[int, int]) -> Image.Image:
"""Restores a sample image from given data string which contains """Restores a sample image from given data string which contains
hex-encoded pixels from the top left fourth of a sample. hex-encoded pixels from the top left fourth of a sample.
""" """
@ -83,7 +84,7 @@ class TestImagingCoreResampleAccuracy:
s_px[size[0] - x - 1, y] = 255 - val s_px[size[0] - x - 1, y] = 255 - val
return sample return sample
def check_case(self, case, sample) -> None: def check_case(self, case: Image.Image, sample: Image.Image) -> None:
s_px = sample.load() s_px = sample.load()
c_px = case.load() c_px = case.load()
for y in range(case.size[1]): for y in range(case.size[1]):
@ -95,7 +96,7 @@ class TestImagingCoreResampleAccuracy:
) )
assert s_px[x, y] == c_px[x, y], message assert s_px[x, y] == c_px[x, y], message
def serialize_image(self, image): def serialize_image(self, image: Image.Image) -> str:
s_px = image.load() s_px = image.load()
return "\n".join( return "\n".join(
" ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0])) " ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0]))
@ -103,7 +104,7 @@ class TestImagingCoreResampleAccuracy:
) )
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
def test_reduce_box(self, mode) -> None: def test_reduce_box(self, mode: str) -> None:
case = self.make_case(mode, (8, 8), 0xE1) case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.Resampling.BOX) case = case.resize((4, 4), Image.Resampling.BOX)
# fmt: off # fmt: off
@ -114,7 +115,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
def test_reduce_bilinear(self, mode) -> None: def test_reduce_bilinear(self, mode: str) -> None:
case = self.make_case(mode, (8, 8), 0xE1) case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.Resampling.BILINEAR) case = case.resize((4, 4), Image.Resampling.BILINEAR)
# fmt: off # fmt: off
@ -125,7 +126,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
def test_reduce_hamming(self, mode) -> None: def test_reduce_hamming(self, mode: str) -> None:
case = self.make_case(mode, (8, 8), 0xE1) case = self.make_case(mode, (8, 8), 0xE1)
case = case.resize((4, 4), Image.Resampling.HAMMING) case = case.resize((4, 4), Image.Resampling.HAMMING)
# fmt: off # fmt: off
@ -136,7 +137,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
def test_reduce_bicubic(self, mode) -> None: def test_reduce_bicubic(self, mode: str) -> None:
case = self.make_case(mode, (12, 12), 0xE1) case = self.make_case(mode, (12, 12), 0xE1)
case = case.resize((6, 6), Image.Resampling.BICUBIC) case = case.resize((6, 6), Image.Resampling.BICUBIC)
# fmt: off # fmt: off
@ -148,7 +149,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (6, 6))) self.check_case(channel, self.make_sample(data, (6, 6)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
def test_reduce_lanczos(self, mode) -> None: def test_reduce_lanczos(self, mode: str) -> None:
case = self.make_case(mode, (16, 16), 0xE1) case = self.make_case(mode, (16, 16), 0xE1)
case = case.resize((8, 8), Image.Resampling.LANCZOS) case = case.resize((8, 8), Image.Resampling.LANCZOS)
# fmt: off # fmt: off
@ -161,7 +162,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (8, 8))) self.check_case(channel, self.make_sample(data, (8, 8)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
def test_enlarge_box(self, mode) -> None: def test_enlarge_box(self, mode: str) -> None:
case = self.make_case(mode, (2, 2), 0xE1) case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.Resampling.BOX) case = case.resize((4, 4), Image.Resampling.BOX)
# fmt: off # fmt: off
@ -172,7 +173,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
def test_enlarge_bilinear(self, mode) -> None: def test_enlarge_bilinear(self, mode: str) -> None:
case = self.make_case(mode, (2, 2), 0xE1) case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.Resampling.BILINEAR) case = case.resize((4, 4), Image.Resampling.BILINEAR)
# fmt: off # fmt: off
@ -183,7 +184,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
def test_enlarge_hamming(self, mode) -> None: def test_enlarge_hamming(self, mode: str) -> None:
case = self.make_case(mode, (2, 2), 0xE1) case = self.make_case(mode, (2, 2), 0xE1)
case = case.resize((4, 4), Image.Resampling.HAMMING) case = case.resize((4, 4), Image.Resampling.HAMMING)
# fmt: off # fmt: off
@ -194,7 +195,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (4, 4))) self.check_case(channel, self.make_sample(data, (4, 4)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
def test_enlarge_bicubic(self, mode) -> None: def test_enlarge_bicubic(self, mode: str) -> None:
case = self.make_case(mode, (4, 4), 0xE1) case = self.make_case(mode, (4, 4), 0xE1)
case = case.resize((8, 8), Image.Resampling.BICUBIC) case = case.resize((8, 8), Image.Resampling.BICUBIC)
# fmt: off # fmt: off
@ -207,7 +208,7 @@ class TestImagingCoreResampleAccuracy:
self.check_case(channel, self.make_sample(data, (8, 8))) self.check_case(channel, self.make_sample(data, (8, 8)))
@pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L"))
def test_enlarge_lanczos(self, mode) -> None: def test_enlarge_lanczos(self, mode: str) -> None:
case = self.make_case(mode, (6, 6), 0xE1) case = self.make_case(mode, (6, 6), 0xE1)
case = case.resize((12, 12), Image.Resampling.LANCZOS) case = case.resize((12, 12), Image.Resampling.LANCZOS)
data = ( data = (
@ -230,7 +231,7 @@ class TestImagingCoreResampleAccuracy:
class TestCoreResampleConsistency: class TestCoreResampleConsistency:
def make_case(self, mode, fill): def make_case(self, mode: str, fill: tuple[int, int, int] | float):
im = Image.new(mode, (512, 9), fill) im = Image.new(mode, (512, 9), fill)
return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0]
@ -265,7 +266,7 @@ class TestCoreResampleConsistency:
class TestCoreResampleAlphaCorrect: class TestCoreResampleAlphaCorrect:
def make_levels_case(self, mode): def make_levels_case(self, mode: str) -> Image.Image:
i = Image.new(mode, (256, 16)) i = Image.new(mode, (256, 16))
px = i.load() px = i.load()
for y in range(i.size[1]): for y in range(i.size[1]):
@ -275,7 +276,7 @@ class TestCoreResampleAlphaCorrect:
px[x, y] = tuple(pix) px[x, y] = tuple(pix)
return i return i
def run_levels_case(self, i) -> None: def run_levels_case(self, i: Image.Image) -> None:
px = i.load() px = i.load()
for y in range(i.size[1]): for y in range(i.size[1]):
used_colors = {px[x, y][0] for x in range(i.size[0])} used_colors = {px[x, y][0] for x in range(i.size[0])}
@ -302,7 +303,9 @@ class TestCoreResampleAlphaCorrect:
self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC))
self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS))
def make_dirty_case(self, mode, clean_pixel, dirty_pixel): def make_dirty_case(
self, mode: str, clean_pixel: tuple[int, ...], dirty_pixel: tuple[int, ...]
) -> Image.Image:
i = Image.new(mode, (64, 64), dirty_pixel) i = Image.new(mode, (64, 64), dirty_pixel)
px = i.load() px = i.load()
xdiv4 = i.size[0] // 4 xdiv4 = i.size[0] // 4
@ -312,7 +315,7 @@ class TestCoreResampleAlphaCorrect:
px[x + xdiv4, y + ydiv4] = clean_pixel px[x + xdiv4, y + ydiv4] = clean_pixel
return i return i
def run_dirty_case(self, i, clean_pixel) -> None: def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None:
px = i.load() px = i.load()
for y in range(i.size[1]): for y in range(i.size[1]):
for x in range(i.size[0]): for x in range(i.size[0]):
@ -432,7 +435,7 @@ class TestCoreResampleBox:
Image.Resampling.LANCZOS, Image.Resampling.LANCZOS,
), ),
) )
def test_wrong_arguments(self, resample) -> None: def test_wrong_arguments(self, resample: Image.Resampling) -> None:
im = hopper() im = hopper()
im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (0, 0, im.width, im.height))
im.resize((32, 32), resample, (20, 20, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height))
@ -459,8 +462,12 @@ class TestCoreResampleBox:
with pytest.raises(ValueError, match="can't exceed"): with pytest.raises(ValueError, match="can't exceed"):
im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) im.resize((32, 32), resample, (0, 0, im.width, im.height + 1))
def resize_tiled(self, im, dst_size, xtiles, ytiles): def resize_tiled(
def split_range(size, tiles): self, im: Image.Image, dst_size: tuple[int, int], xtiles: int, ytiles: int
) -> Image.Image:
def split_range(
size: int, tiles: int
) -> Generator[tuple[int, int], None, None]:
scale = size / tiles scale = size / tiles
for i in range(tiles): for i in range(tiles):
yield int(round(scale * i)), int(round(scale * (i + 1))) yield int(round(scale * i)), int(round(scale * (i + 1)))
@ -518,7 +525,7 @@ class TestCoreResampleBox:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR)
) )
def test_formats(self, mode, resample) -> None: def test_formats(self, mode: str, resample: Image.Resampling) -> None:
im = hopper(mode) im = hopper(mode)
box = (20, 20, im.size[0] - 20, im.size[1] - 20) box = (20, 20, im.size[0] - 20, im.size[1] - 20)
with_box = im.resize((32, 32), resample, box) with_box = im.resize((32, 32), resample, box)
@ -558,7 +565,7 @@ class TestCoreResampleBox:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC)
) )
def test_skip_horizontal(self, flt) -> None: def test_skip_horizontal(self, flt: Image.Resampling) -> None:
# Can skip resize for one dimension # Can skip resize for one dimension
im = hopper() im = hopper()
@ -581,7 +588,7 @@ class TestCoreResampleBox:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC)
) )
def test_skip_vertical(self, flt) -> None: def test_skip_vertical(self, flt: Image.Resampling) -> None:
# Can skip resize for one dimension # Can skip resize for one dimension
im = hopper() im = hopper()

View File

@ -1,6 +1,7 @@
""" """
Tests for resize functionality. Tests for resize functionality.
""" """
from __future__ import annotations from __future__ import annotations
from itertools import permutations from itertools import permutations

View File

@ -342,9 +342,11 @@ def test_extended_information() -> None:
def truncate_tuple(tuple_or_float): def truncate_tuple(tuple_or_float):
return tuple( return tuple(
(
truncate_tuple(val) truncate_tuple(val)
if isinstance(val, tuple) if isinstance(val, tuple)
else int(val * power) / power else int(val * power) / power
)
for val in tuple_or_float for val in tuple_or_float
) )

View File

@ -6,6 +6,7 @@ import os.path
import pytest import pytest
from PIL import Image, ImageColor, ImageDraw, ImageFont, features from PIL import Image, ImageColor, ImageDraw, ImageFont, features
from PIL._typing import Coords
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -74,7 +75,7 @@ def test_mode_mismatch() -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4)))
def test_arc(bbox, start, end) -> None: def test_arc(bbox: Coords, start: float, end: float) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -87,7 +88,7 @@ def test_arc(bbox, start, end) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_arc_end_le_start(bbox) -> None: def test_arc_end_le_start(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -102,7 +103,7 @@ def test_arc_end_le_start(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_arc_no_loops(bbox) -> None: def test_arc_no_loops(bbox: Coords) -> None:
# No need to go in loops # No need to go in loops
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
@ -118,7 +119,7 @@ def test_arc_no_loops(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_arc_width(bbox) -> None: def test_arc_width(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -131,7 +132,7 @@ def test_arc_width(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_arc_width_pieslice_large(bbox) -> None: def test_arc_width_pieslice_large(bbox: Coords) -> None:
# Tests an arc with a large enough width that it is a pieslice # Tests an arc with a large enough width that it is a pieslice
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
@ -145,7 +146,7 @@ def test_arc_width_pieslice_large(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_arc_width_fill(bbox) -> None: def test_arc_width_fill(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -158,7 +159,7 @@ def test_arc_width_fill(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_arc_width_non_whole_angle(bbox) -> None: def test_arc_width_non_whole_angle(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -200,7 +201,7 @@ def test_bitmap() -> None:
@pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_chord(mode, bbox) -> None: def test_chord(mode: str, bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new(mode, (W, H)) im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -214,7 +215,7 @@ def test_chord(mode, bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_chord_width(bbox) -> None: def test_chord_width(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -227,7 +228,7 @@ def test_chord_width(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_chord_width_fill(bbox) -> None: def test_chord_width_fill(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -240,7 +241,7 @@ def test_chord_width_fill(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_chord_zero_width(bbox) -> None: def test_chord_zero_width(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -266,7 +267,7 @@ def test_chord_too_fat() -> None:
@pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_ellipse(mode, bbox) -> None: def test_ellipse(mode: str, bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new(mode, (W, H)) im = Image.new(mode, (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -280,7 +281,7 @@ def test_ellipse(mode, bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_ellipse_translucent(bbox) -> None: def test_ellipse_translucent(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA") draw = ImageDraw.Draw(im, "RGBA")
@ -317,7 +318,7 @@ def test_ellipse_symmetric() -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_ellipse_width(bbox) -> None: def test_ellipse_width(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -342,7 +343,7 @@ def test_ellipse_width_large() -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_ellipse_width_fill(bbox) -> None: def test_ellipse_width_fill(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -355,7 +356,7 @@ def test_ellipse_width_fill(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_ellipse_zero_width(bbox) -> None: def test_ellipse_zero_width(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -367,7 +368,7 @@ def test_ellipse_zero_width(bbox) -> None:
assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png")
def ellipse_various_sizes_helper(filled): def ellipse_various_sizes_helper(filled: bool) -> Image.Image:
ellipse_sizes = range(32) ellipse_sizes = range(32)
image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1 image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1
im = Image.new("RGB", (image_size, image_size)) im = Image.new("RGB", (image_size, image_size))
@ -409,7 +410,7 @@ def test_ellipse_various_sizes_filled() -> None:
@pytest.mark.parametrize("points", POINTS) @pytest.mark.parametrize("points", POINTS)
def test_line(points) -> None: def test_line(points: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -482,7 +483,7 @@ def test_transform() -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
@pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2)))
def test_pieslice(bbox, start, end) -> None: def test_pieslice(bbox: Coords, start: float, end: float) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -495,7 +496,7 @@ def test_pieslice(bbox, start, end) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_pieslice_width(bbox) -> None: def test_pieslice_width(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -508,7 +509,7 @@ def test_pieslice_width(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_pieslice_width_fill(bbox) -> None: def test_pieslice_width_fill(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -522,7 +523,7 @@ def test_pieslice_width_fill(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_pieslice_zero_width(bbox) -> None: def test_pieslice_zero_width(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -577,7 +578,7 @@ def test_pieslice_no_spikes() -> None:
@pytest.mark.parametrize("points", POINTS) @pytest.mark.parametrize("points", POINTS)
def test_point(points) -> None: def test_point(points: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -602,7 +603,7 @@ def test_point_I16() -> None:
@pytest.mark.parametrize("points", POINTS) @pytest.mark.parametrize("points", POINTS)
def test_polygon(points) -> None: def test_polygon(points: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -616,7 +617,9 @@ def test_polygon(points) -> None:
@pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("mode", ("RGB", "L"))
@pytest.mark.parametrize("kite_points", KITE_POINTS) @pytest.mark.parametrize("kite_points", KITE_POINTS)
def test_polygon_kite(mode, kite_points) -> None: def test_polygon_kite(
mode: str, kite_points: tuple[tuple[int, int], ...] | list[tuple[int, int]]
) -> None:
# Test drawing lines of different gradients (dx>dy, dy>dx) and # Test drawing lines of different gradients (dx>dy, dy>dx) and
# vertical (dx==0) and horizontal (dy==0) lines # vertical (dx==0) and horizontal (dy==0) lines
# Arrange # Arrange
@ -673,7 +676,7 @@ def test_polygon_translucent() -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_rectangle(bbox) -> None: def test_rectangle(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -700,7 +703,7 @@ def test_big_rectangle() -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_width(bbox) -> None: def test_rectangle_width(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -714,7 +717,7 @@ def test_rectangle_width(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_width_fill(bbox) -> None: def test_rectangle_width_fill(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -728,7 +731,7 @@ def test_rectangle_width_fill(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_zero_width(bbox) -> None: def test_rectangle_zero_width(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -741,7 +744,7 @@ def test_rectangle_zero_width(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_I16(bbox) -> None: def test_rectangle_I16(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("I;16", (W, H)) im = Image.new("I;16", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -754,7 +757,7 @@ def test_rectangle_I16(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_rectangle_translucent_outline(bbox) -> None: def test_rectangle_translucent_outline(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA") draw = ImageDraw.Draw(im, "RGBA")
@ -772,7 +775,13 @@ def test_rectangle_translucent_outline(bbox) -> None:
"xy", "xy",
[(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))],
) )
def test_rounded_rectangle(xy) -> None: def test_rounded_rectangle(
xy: (
tuple[int, int, int, int]
| tuple[list[int]]
| tuple[tuple[int, int], tuple[int, int]]
)
) -> None:
# Arrange # Arrange
im = Image.new("RGB", (200, 200)) im = Image.new("RGB", (200, 200))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -789,7 +798,7 @@ def test_rounded_rectangle(xy) -> None:
@pytest.mark.parametrize("bottom_right", (True, False)) @pytest.mark.parametrize("bottom_right", (True, False))
@pytest.mark.parametrize("bottom_left", (True, False)) @pytest.mark.parametrize("bottom_left", (True, False))
def test_rounded_rectangle_corners( def test_rounded_rectangle_corners(
top_left, top_right, bottom_right, bottom_left top_left: bool, top_right: bool, bottom_right: bool, bottom_left: bool
) -> None: ) -> None:
corners = (top_left, top_right, bottom_right, bottom_left) corners = (top_left, top_right, bottom_right, bottom_left)
@ -824,7 +833,9 @@ def test_rounded_rectangle_corners(
((10, 20, 190, 181), 85, "height"), ((10, 20, 190, 181), 85, "height"),
], ],
) )
def test_rounded_rectangle_non_integer_radius(xy, radius, type) -> None: def test_rounded_rectangle_non_integer_radius(
xy: tuple[int, int, int, int], radius: float, type: str
) -> None:
# Arrange # Arrange
im = Image.new("RGB", (200, 200)) im = Image.new("RGB", (200, 200))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -840,7 +851,7 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_rounded_rectangle_zero_radius(bbox) -> None: def test_rounded_rectangle_zero_radius(bbox: Coords) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -862,7 +873,9 @@ def test_rounded_rectangle_zero_radius(bbox) -> None:
((20, 20, 80, 80), "both"), ((20, 20, 80, 80), "both"),
], ],
) )
def test_rounded_rectangle_translucent(xy, suffix) -> None: def test_rounded_rectangle_translucent(
xy: tuple[int, int, int, int], suffix: str
) -> None:
# Arrange # Arrange
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im, "RGBA") draw = ImageDraw.Draw(im, "RGBA")
@ -879,7 +892,7 @@ def test_rounded_rectangle_translucent(xy, suffix) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_floodfill(bbox) -> None: def test_floodfill(bbox: Coords) -> None:
red = ImageColor.getrgb("red") red = ImageColor.getrgb("red")
for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]:
@ -912,7 +925,7 @@ def test_floodfill(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_floodfill_border(bbox) -> None: def test_floodfill_border(bbox: Coords) -> None:
# floodfill() is experimental # floodfill() is experimental
# Arrange # Arrange
@ -934,7 +947,7 @@ def test_floodfill_border(bbox) -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_floodfill_thresh(bbox) -> None: def test_floodfill_thresh(bbox: Coords) -> None:
# floodfill() is experimental # floodfill() is experimental
# Arrange # Arrange
@ -968,8 +981,11 @@ def test_floodfill_not_negative() -> None:
def create_base_image_draw( def create_base_image_draw(
size, mode=DEFAULT_MODE, background1=WHITE, background2=GRAY size: tuple[int, int],
): mode: str = DEFAULT_MODE,
background1: tuple[int, int, int] = WHITE,
background2: tuple[int, int, int] = GRAY,
) -> tuple[Image.Image, ImageDraw.ImageDraw]:
img = Image.new(mode, size, background1) img = Image.new(mode, size, background1)
for x in range(0, size[0]): for x in range(0, size[0]):
for y in range(0, size[1]): for y in range(0, size[1]):
@ -1003,7 +1019,7 @@ def test_triangle_right() -> None:
"fill, suffix", "fill, suffix",
((BLACK, "width"), (None, "width_no_fill")), ((BLACK, "width"), (None, "width_no_fill")),
) )
def test_triangle_right_width(fill, suffix) -> None: def test_triangle_right_width(fill: tuple[int, int, int] | None, suffix: str) -> None:
img, draw = create_base_image_draw((100, 100)) img, draw = create_base_image_draw((100, 100))
draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5) draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5)
assert_image_equal_tofile( assert_image_equal_tofile(
@ -1235,7 +1251,7 @@ def test_wide_line_larger_than_int() -> None:
], ],
], ],
) )
def test_line_joint(xy) -> None: def test_line_joint(xy: list[tuple[int, int]] | tuple[int, ...] | list[int]) -> None:
im = Image.new("RGB", (500, 325)) im = Image.new("RGB", (500, 325))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -1388,7 +1404,7 @@ def test_default_font_size() -> None:
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)
def test_same_color_outline(bbox) -> None: def test_same_color_outline(bbox: Coords) -> None:
# Prepare shape # Prepare shape
x0, y0 = 5, 5 x0, y0 = 5, 5
x1, y1 = 5, 50 x1, y1 = 5, 50
@ -1402,7 +1418,8 @@ def test_same_color_outline(bbox) -> None:
# Begin # Begin
for mode in ["RGB", "L"]: for mode in ["RGB", "L"]:
for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: fill = "red"
for outline in [None, "red", "#f00"]:
for operation, args in { for operation, args in {
"chord": [bbox, 0, 180], "chord": [bbox, 0, 180],
"ellipse": [bbox], "ellipse": [bbox],
@ -1417,6 +1434,7 @@ def test_same_color_outline(bbox) -> None:
# Act # Act
draw_method = getattr(draw, operation) draw_method = getattr(draw, operation)
assert isinstance(args, list)
args += [fill, outline] args += [fill, outline]
draw_method(*args) draw_method(*args)
@ -1434,7 +1452,9 @@ def test_same_color_outline(bbox) -> None:
(3, "triangle_width", {"width": 5, "outline": "yellow"}), (3, "triangle_width", {"width": 5, "outline": "yellow"}),
], ],
) )
def test_draw_regular_polygon(n_sides, polygon_name, args) -> None: def test_draw_regular_polygon(
n_sides: int, polygon_name: str, args: dict[str, int | str]
) -> None:
im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0))
filename = f"Tests/images/imagedraw_{polygon_name}.png" filename = f"Tests/images/imagedraw_{polygon_name}.png"
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
@ -1471,7 +1491,9 @@ def test_draw_regular_polygon(n_sides, polygon_name, args) -> None:
), ),
], ],
) )
def test_compute_regular_polygon_vertices(n_sides, expected_vertices) -> None: def test_compute_regular_polygon_vertices(
n_sides: int, expected_vertices: list[tuple[float, float]]
) -> None:
bounding_circle = (W // 2, H // 2, 25) bounding_circle = (W // 2, H // 2, 25)
vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0)
assert vertices == expected_vertices assert vertices == expected_vertices
@ -1482,7 +1504,7 @@ def test_compute_regular_polygon_vertices(n_sides, expected_vertices) -> None:
[ [
(None, (50, 50, 25), 0, TypeError, "n_sides should be an int"), (None, (50, 50, 25), 0, TypeError, "n_sides should be an int"),
(1, (50, 50, 25), 0, ValueError, "n_sides should be an int > 2"), (1, (50, 50, 25), 0, ValueError, "n_sides should be an int > 2"),
(3, 50, 0, TypeError, "bounding_circle should be a tuple"), (3, 50, 0, TypeError, "bounding_circle should be a sequence"),
( (
3, 3,
(50, 50, 100, 100), (50, 50, 100, 100),
@ -1569,7 +1591,7 @@ def test_polygon2() -> None:
@pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) @pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0)))
def test_incorrectly_ordered_coordinates(xy) -> None: def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None:
im = Image.new("RGB", (W, H)) im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)
with pytest.raises(ValueError): with pytest.raises(ValueError):

View File

@ -119,3 +119,15 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
subprocess.call(["wl-copy"], stdin=fp) subprocess.call(["wl-copy"], stdin=fp)
im = ImageGrab.grabclipboard() im = ImageGrab.grabclipboard()
assert_image_equal_tofile(im, image_path) assert_image_equal_tofile(im, image_path)
@pytest.mark.skipif(
(
sys.platform != "linux"
or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy"))
),
reason="Linux with wl-clipboard only",
)
@pytest.mark.parametrize("arg", ("text", "--clear"))
def test_grabclipboard_wl_clipboard_errors(self, arg):
subprocess.call(["wl-copy", arg])
assert ImageGrab.grabclipboard() is None

View File

@ -5,12 +5,12 @@ import pytest
from PIL import Image, ImageMath from PIL import Image, ImageMath
def pixel(im): def pixel(im: Image.Image | int) -> str | int:
if hasattr(im, "im"):
return f"{im.mode} {repr(im.getpixel((0, 0)))}"
if isinstance(im, int): if isinstance(im, int):
return int(im) # hack to deal with booleans return int(im) # hack to deal with booleans
return f"{im.mode} {repr(im.getpixel((0, 0)))}"
A = Image.new("L", (1, 1), 1) A = Image.new("L", (1, 1), 1)
B = Image.new("L", (1, 1), 2) B = Image.new("L", (1, 1), 2)
@ -60,7 +60,7 @@ def test_ops() -> None:
"(lambda: (lambda: exec('pass'))())()", "(lambda: (lambda: exec('pass'))())()",
), ),
) )
def test_prevent_exec(expression) -> None: def test_prevent_exec(expression: str) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageMath.eval(expression) ImageMath.eval(expression)

View File

@ -57,7 +57,7 @@ def test_kw() -> None:
@pytest.mark.parametrize("mode", TK_MODES) @pytest.mark.parametrize("mode", TK_MODES)
def test_photoimage(mode) -> None: def test_photoimage(mode: str) -> None:
# test as image: # test as image:
im = hopper(mode) im = hopper(mode)
@ -79,7 +79,7 @@ def test_photoimage_apply_transparency() -> None:
@pytest.mark.parametrize("mode", TK_MODES) @pytest.mark.parametrize("mode", TK_MODES)
def test_photoimage_blank(mode) -> None: def test_photoimage_blank(mode: str) -> None:
# test a image using mode/size: # test a image using mode/size:
im_tk = ImageTk.PhotoImage(mode, (100, 100)) im_tk = ImageTk.PhotoImage(mode, (100, 100))

View File

@ -10,7 +10,13 @@ X = 255
class TestLibPack: class TestLibPack:
def assert_pack(self, mode, rawmode, data, *pixels) -> None: def assert_pack(
self,
mode: str,
rawmode: str,
data: int | bytes,
*pixels: int | float | tuple[int, ...],
) -> None:
""" """
data - either raw bytes with data or just number of bytes in rawmode. data - either raw bytes with data or just number of bytes in rawmode.
""" """
@ -228,7 +234,13 @@ class TestLibPack:
class TestLibUnpack: class TestLibUnpack:
def assert_unpack(self, mode, rawmode, data, *pixels) -> None: def assert_unpack(
self,
mode: str,
rawmode: str,
data: int | bytes,
*pixels: int | float | tuple[int, ...],
) -> None:
""" """
data - either raw bytes with data or just number of bytes in rawmode. data - either raw bytes with data or just number of bytes in rawmode.
""" """

View File

@ -11,7 +11,7 @@ from .helper import hopper
original = hopper().resize((32, 32)).convert("I") original = hopper().resize((32, 32)).convert("I")
def verify(im1) -> None: def verify(im1: Image.Image) -> None:
im2 = original.copy() im2 = original.copy()
assert im1.size == im2.size assert im1.size == im2.size
pix1 = im1.load() pix1 = im1.load()
@ -27,7 +27,7 @@ def verify(im1) -> None:
@pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I")) @pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I"))
def test_basic(tmp_path: Path, mode) -> None: def test_basic(tmp_path: Path, mode: str) -> None:
# PIL 1.1 has limited support for 16-bit image data. Check that # PIL 1.1 has limited support for 16-bit image data. Check that
# create/copy/transform and save works as expected. # create/copy/transform and save works as expected.
@ -78,7 +78,7 @@ def test_basic(tmp_path: Path, mode) -> None:
def test_tobytes() -> None: def test_tobytes() -> None:
def tobytes(mode): def tobytes(mode: str) -> Image.Image:
return Image.new(mode, (1, 1), 1).tobytes() return Image.new(mode, (1, 1), 1).tobytes()
order = 1 if Image._ENDIAN == "<" else -1 order = 1 if Image._ENDIAN == "<" else -1

View File

@ -9,6 +9,7 @@ The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license: Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/ https://creativecommons.org/publicdomain/zero/1.0/
""" """
from __future__ import annotations from __future__ import annotations
import struct import struct

View File

@ -96,7 +96,7 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
test-command = "cd {project} && .github/workflows/wheels-test.sh" test-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests" test-extras = "tests"
[tool.ruff] [tool.ruff.lint]
select = [ select = [
"C4", # flake8-comprehensions "C4", # flake8-comprehensions
"E", # pycodestyle errors "E", # pycodestyle errors
@ -104,25 +104,25 @@ select = [
"F", # pyflakes errors "F", # pyflakes errors
"I", # isort "I", # isort
"ISC", # flake8-implicit-str-concat "ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"PGH", # pygrep-hooks "PGH", # pygrep-hooks
"RUF100", # unused noqa (yesqa) "RUF100", # unused noqa (yesqa)
"UP", # pyupgrade "UP", # pyupgrade
"W", # pycodestyle warnings "W", # pycodestyle warnings
"YTT", # flake8-2020 "YTT", # flake8-2020
# "LOG", # TODO: enable flake8-logging when it's not in preview anymore
] ]
extend-ignore = [ ignore = [
"E203", # Whitespace before ':' "E203", # Whitespace before ':'
"E221", # Multiple spaces before operator "E221", # Multiple spaces before operator
"E226", # Missing whitespace around arithmetic operator "E226", # Missing whitespace around arithmetic operator
"E241", # Multiple spaces after ',' "E241", # Multiple spaces after ','
] ]
[tool.ruff.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"Tests/oss-fuzz/fuzz_font.py" = ["I002"] "Tests/oss-fuzz/fuzz_font.py" = ["I002"]
"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] "Tests/oss-fuzz/fuzz_pillow.py" = ["I002"]
[tool.ruff.isort] [tool.ruff.lint.isort]
known-first-party = ["PIL"] known-first-party = ["PIL"]
required-imports = ["from __future__ import annotations"] required-imports = ["from __future__ import annotations"]
@ -141,16 +141,6 @@ warn_redundant_casts = true
warn_unreachable = true warn_unreachable = true
warn_unused_ignores = true warn_unused_ignores = true
exclude = [ exclude = [
'^src/PIL/_tkinter_finder.py$',
'^src/PIL/DdsImagePlugin.py$',
'^src/PIL/FpxImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$',
'^src/PIL/Image.py$',
'^src/PIL/ImageQt.py$',
'^src/PIL/ImImagePlugin.py$',
'^src/PIL/MicImagePlugin.py$', '^src/PIL/MicImagePlugin.py$',
'^src/PIL/PdfParser.py$',
'^src/PIL/PyAccess.py$',
'^src/PIL/TiffImagePlugin.py$',
'^src/PIL/TiffTags.py$',
'^src/PIL/WebPImagePlugin.py$',
] ]

View File

@ -28,6 +28,7 @@ BLP files come in many different flavours:
- DXT3 compression is used if alpha_encoding == 1. - DXT3 compression is used if alpha_encoding == 1.
- DXT5 compression is used if alpha_encoding == 7. - DXT5 compression is used if alpha_encoding == 7.
""" """
from __future__ import annotations from __future__ import annotations
import os import os

View File

@ -9,6 +9,7 @@ The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license: Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/ https://creativecommons.org/publicdomain/zero/1.0/
""" """
from __future__ import annotations from __future__ import annotations
import io import io
@ -269,13 +270,17 @@ class D3DFMT(IntEnum):
# Backward compatibility layer # Backward compatibility layer
module = sys.modules[__name__] module = sys.modules[__name__]
for item in DDSD: for item in DDSD:
assert item.name is not None
setattr(module, "DDSD_" + item.name, item.value) setattr(module, "DDSD_" + item.name, item.value)
for item in DDSCAPS: for item1 in DDSCAPS:
setattr(module, "DDSCAPS_" + item.name, item.value) assert item1.name is not None
for item in DDSCAPS2: setattr(module, "DDSCAPS_" + item1.name, item1.value)
setattr(module, "DDSCAPS2_" + item.name, item.value) for item2 in DDSCAPS2:
for item in DDPF: assert item2.name is not None
setattr(module, "DDPF_" + item.name, item.value) setattr(module, "DDSCAPS2_" + item2.name, item2.value)
for item3 in DDPF:
assert item3.name is not None
setattr(module, "DDPF_" + item3.name, item3.value)
DDS_FOURCC = DDPF.FOURCC DDS_FOURCC = DDPF.FOURCC
DDS_RGB = DDPF.RGB DDS_RGB = DDPF.RGB

View File

@ -50,9 +50,7 @@ class FontFile:
| None | None
] = [None] * 256 ] = [None] * 256
def __getitem__( def __getitem__(self, ix: int) -> (
self, ix: int
) -> (
tuple[ tuple[
tuple[int, int], tuple[int, int],
tuple[int, int, int, int], tuple[int, int, int, int],

View File

@ -50,6 +50,7 @@ bytes for that mipmap level.
Note: All data is stored in little-Endian (Intel) byte order. Note: All data is stored in little-Endian (Intel) byte order.
""" """
from __future__ import annotations from __future__ import annotations
import struct import struct

View File

@ -641,9 +641,9 @@ def _write_multiple_frames(im, fp, palette):
if encoderinfo.get("optimize") and im_frame.mode != "1": if encoderinfo.get("optimize") and im_frame.mode != "1":
if "transparency" not in encoderinfo: if "transparency" not in encoderinfo:
try: try:
encoderinfo[ encoderinfo["transparency"] = (
"transparency" im_frame.palette._new_color_index(im_frame)
] = im_frame.palette._new_color_index(im_frame) )
except ValueError: except ValueError:
pass pass
if "transparency" in encoderinfo: if "transparency" in encoderinfo:

View File

@ -93,8 +93,8 @@ for i in ["16", "16L", "16B"]:
for i in ["32S"]: for i in ["32S"]:
OPEN[f"L {i} image"] = ("I", f"I;{i}") OPEN[f"L {i} image"] = ("I", f"I;{i}")
OPEN[f"L*{i} image"] = ("I", f"I;{i}") OPEN[f"L*{i} image"] = ("I", f"I;{i}")
for i in range(2, 33): for j in range(2, 33):
OPEN[f"L*{i} image"] = ("F", f"F;{i}") OPEN[f"L*{j} image"] = ("F", f"F;{j}")
# -------------------------------------------------------------------- # --------------------------------------------------------------------

View File

@ -26,6 +26,7 @@
from __future__ import annotations from __future__ import annotations
import abc
import atexit import atexit
import builtins import builtins
import io import io
@ -40,11 +41,8 @@ import warnings
from collections.abc import Callable, MutableMapping from collections.abc import Callable, MutableMapping
from enum import IntEnum from enum import IntEnum
from pathlib import Path from pathlib import Path
from types import ModuleType
try: from typing import IO, TYPE_CHECKING, Any
from defusedxml import ElementTree
except ImportError:
ElementTree = None
# VERSION was removed in Pillow 6.0.0. # VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0.
@ -60,6 +58,12 @@ from . import (
from ._binary import i32le, o32be, o32le from ._binary import i32le, o32be, o32le
from ._util import DeferredError, is_path from ._util import DeferredError, is_path
ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
ElementTree = None
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -110,6 +114,7 @@ except ImportError as v:
USE_CFFI_ACCESS = False USE_CFFI_ACCESS = False
cffi: ModuleType | None
try: try:
import cffi import cffi
except ImportError: except ImportError:
@ -211,14 +216,22 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Registries # Registries
ID = [] if TYPE_CHECKING:
OPEN = {} from . import ImageFile
MIME = {} ID: list[str] = []
SAVE = {} OPEN: dict[
SAVE_ALL = {} str,
EXTENSION = {} tuple[
DECODERS = {} Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
ENCODERS = {} Callable[[bytes], bool] | None,
],
] = {}
MIME: dict[str, str] = {}
SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {}
SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {}
EXTENSION: dict[str, str] = {}
DECODERS: dict[str, object] = {}
ENCODERS: dict[str, object] = {}
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Modes # Modes
@ -571,7 +584,7 @@ class Image:
# object is gone. # object is gone.
self.im = DeferredError(ValueError("Operation on closed image")) self.im = DeferredError(ValueError("Operation on closed image"))
def _copy(self): def _copy(self) -> None:
self.load() self.load()
self.im = self.im.copy() self.im = self.im.copy()
self.pyaccess = None self.pyaccess = None
@ -2383,12 +2396,12 @@ class Image:
may have been created, and may contain partial data. may have been created, and may contain partial data.
""" """
filename = "" filename: str | bytes = ""
open_fp = False open_fp = False
if isinstance(fp, Path): if isinstance(fp, Path):
filename = str(fp) filename = str(fp)
open_fp = True open_fp = True
elif is_path(fp): elif isinstance(fp, (str, bytes)):
filename = fp filename = fp
open_fp = True open_fp = True
elif fp == sys.stdout: elif fp == sys.stdout:
@ -2398,14 +2411,15 @@ class Image:
pass pass
if not filename and hasattr(fp, "name") and is_path(fp.name): if not filename and hasattr(fp, "name") and is_path(fp.name):
# only set the name for metadata purposes # only set the name for metadata purposes
filename = fp.name filename = os.path.realpath(os.fspath(fp.name))
# may mutate self! # may mutate self!
self._ensure_mutable() self._ensure_mutable()
preinit() preinit()
ext = os.path.splitext(filename)[1].lower() filename_ext = os.path.splitext(filename)[1].lower()
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
if not format: if not format:
if ext not in EXTENSION: if ext not in EXTENSION:
@ -2514,7 +2528,7 @@ class Image:
elif new_mode: elif new_mode:
return self.convert(new_mode) return self.convert(new_mode)
def seek(self, frame) -> Image: def seek(self, frame) -> None:
""" """
Seeks to the given frame in this sequence file. If you seek Seeks to the given frame in this sequence file. If you seek
beyond the end of the sequence, the method raises an beyond the end of the sequence, the method raises an
@ -2574,10 +2588,8 @@ class Image:
self.load() self.load()
if self.im.bands == 1: if self.im.bands == 1:
ims = [self.copy()] return (self.copy(),)
else: return tuple(map(self._new, self.im.split()))
ims = map(self._new, self.im.split())
return tuple(ims)
def getchannel(self, channel): def getchannel(self, channel):
""" """
@ -2934,6 +2946,13 @@ class ImageTransformHandler:
(for use with :py:meth:`~PIL.Image.Image.transform`) (for use with :py:meth:`~PIL.Image.Image.transform`)
""" """
@abc.abstractmethod
def transform(
self,
size: tuple[int, int],
image: Image,
**options: dict[str, str | int | tuple[int, ...] | list[int]],
) -> Image:
pass pass
@ -3306,11 +3325,9 @@ def open(fp, mode="r", formats=None) -> Image:
raise TypeError(msg) raise TypeError(msg)
exclusive_fp = False exclusive_fp = False
filename = "" filename: str | bytes = ""
if isinstance(fp, Path): if is_path(fp):
filename = str(fp.resolve()) filename = os.path.realpath(os.fspath(fp))
elif is_path(fp):
filename = fp
if filename: if filename:
fp = builtins.open(filename, "rb") fp = builtins.open(filename, "rb")
@ -3484,7 +3501,11 @@ def merge(mode, bands):
# Plugin registry # Plugin registry
def register_open(id, factory, accept=None) -> None: def register_open(
id,
factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
accept: Callable[[bytes], bool] | None = None,
) -> None:
""" """
Register an image file plugin. This function should not be used Register an image file plugin. This function should not be used
in application code. in application code.
@ -3694,7 +3715,13 @@ _apply_env_variables()
atexit.register(core.clear_cache) atexit.register(core.clear_cache)
class Exif(MutableMapping): if TYPE_CHECKING:
_ExifBase = MutableMapping[int, Any]
else:
_ExifBase = MutableMapping
class Exif(_ExifBase):
""" """
This class provides read and write access to EXIF image data:: This class provides read and write access to EXIF image data::

View File

@ -281,7 +281,6 @@ class ImageCmsProfile:
class ImageCmsTransform(Image.ImagePointHandler): class ImageCmsTransform(Image.ImagePointHandler):
""" """
Transform. This can be used with the procedural API, or with the standard Transform. This can be used with the procedural API, or with the standard
:py:func:`~PIL.Image.Image.point` method. :py:func:`~PIL.Image.Image.point` method.
@ -369,7 +368,6 @@ def get_display_profile(handle=None):
class PyCMSError(Exception): class PyCMSError(Exception):
"""(pyCMS) Exception class. """(pyCMS) Exception class.
This is used for all errors in the pyCMS API.""" This is used for all errors in the pyCMS API."""

View File

@ -34,8 +34,10 @@ from __future__ import annotations
import math import math
import numbers import numbers
import struct import struct
from typing import Sequence, cast
from . import Image, ImageColor from . import Image, ImageColor
from ._typing import Coords
""" """
A simple 2D drawing interface for PIL images. A simple 2D drawing interface for PIL images.
@ -48,7 +50,7 @@ directly.
class ImageDraw: class ImageDraw:
font = None font = None
def __init__(self, im, mode=None): def __init__(self, im: Image.Image, mode: str | None = None) -> None:
""" """
Create a drawing instance. Create a drawing instance.
@ -115,7 +117,7 @@ class ImageDraw:
self.font = ImageFont.load_default() self.font = ImageFont.load_default()
return self.font return self.font
def _getfont(self, font_size): def _getfont(self, font_size: float | None):
if font_size is not None: if font_size is not None:
from . import ImageFont from . import ImageFont
@ -124,7 +126,7 @@ class ImageDraw:
font = self.getfont() font = self.getfont()
return font return font
def _getink(self, ink, fill=None): def _getink(self, ink, fill=None) -> tuple[int | None, int | None]:
if ink is None and fill is None: if ink is None and fill is None:
if self.fill: if self.fill:
fill = self.ink fill = self.ink
@ -145,13 +147,13 @@ class ImageDraw:
fill = self.draw.draw_ink(fill) fill = self.draw.draw_ink(fill)
return ink, fill return ink, fill
def arc(self, xy, start, end, fill=None, width=1): def arc(self, xy: Coords, start, end, fill=None, width=1) -> None:
"""Draw an arc.""" """Draw an arc."""
ink, fill = self._getink(fill) ink, fill = self._getink(fill)
if ink is not None: if ink is not None:
self.draw.draw_arc(xy, start, end, ink, width) self.draw.draw_arc(xy, start, end, ink, width)
def bitmap(self, xy, bitmap, fill=None): def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None:
"""Draw a bitmap.""" """Draw a bitmap."""
bitmap.load() bitmap.load()
ink, fill = self._getink(fill) ink, fill = self._getink(fill)
@ -160,7 +162,7 @@ class ImageDraw:
if ink is not None: if ink is not None:
self.draw.draw_bitmap(xy, bitmap.im, ink) self.draw.draw_bitmap(xy, bitmap.im, ink)
def chord(self, xy, start, end, fill=None, outline=None, width=1): def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None:
"""Draw a chord.""" """Draw a chord."""
ink, fill = self._getink(outline, fill) ink, fill = self._getink(outline, fill)
if fill is not None: if fill is not None:
@ -168,7 +170,7 @@ class ImageDraw:
if ink is not None and ink != fill and width != 0: if ink is not None and ink != fill and width != 0:
self.draw.draw_chord(xy, start, end, ink, 0, width) self.draw.draw_chord(xy, start, end, ink, 0, width)
def ellipse(self, xy, fill=None, outline=None, width=1): def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None:
"""Draw an ellipse.""" """Draw an ellipse."""
ink, fill = self._getink(outline, fill) ink, fill = self._getink(outline, fill)
if fill is not None: if fill is not None:
@ -176,20 +178,29 @@ class ImageDraw:
if ink is not None and ink != fill and width != 0: if ink is not None and ink != fill and width != 0:
self.draw.draw_ellipse(xy, ink, 0, width) self.draw.draw_ellipse(xy, ink, 0, width)
def line(self, xy, fill=None, width=0, joint=None): def line(self, xy: Coords, fill=None, width=0, joint=None) -> None:
"""Draw a line, or a connected sequence of line segments.""" """Draw a line, or a connected sequence of line segments."""
ink = self._getink(fill)[0] ink = self._getink(fill)[0]
if ink is not None: if ink is not None:
self.draw.draw_lines(xy, ink, width) self.draw.draw_lines(xy, ink, width)
if joint == "curve" and width > 4: if joint == "curve" and width > 4:
if not isinstance(xy[0], (list, tuple)): points: Sequence[Sequence[float]]
xy = [tuple(xy[i : i + 2]) for i in range(0, len(xy), 2)] if isinstance(xy[0], (list, tuple)):
for i in range(1, len(xy) - 1): points = cast(Sequence[Sequence[float]], xy)
point = xy[i] else:
points = [
cast(Sequence[float], tuple(xy[i : i + 2]))
for i in range(0, len(xy), 2)
]
for i in range(1, len(points) - 1):
point = points[i]
angles = [ angles = [
math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) math.degrees(math.atan2(end[0] - start[0], start[1] - end[1]))
% 360 % 360
for start, end in ((xy[i - 1], point), (point, xy[i + 1])) for start, end in (
(points[i - 1], point),
(point, points[i + 1]),
)
] ]
if angles[0] == angles[1]: if angles[0] == angles[1]:
# This is a straight line, so no joint is required # This is a straight line, so no joint is required
@ -236,7 +247,7 @@ class ImageDraw:
] ]
self.line(gap_coords, fill, width=3) self.line(gap_coords, fill, width=3)
def shape(self, shape, fill=None, outline=None): def shape(self, shape, fill=None, outline=None) -> None:
"""(Experimental) Draw a shape.""" """(Experimental) Draw a shape."""
shape.close() shape.close()
ink, fill = self._getink(outline, fill) ink, fill = self._getink(outline, fill)
@ -245,7 +256,9 @@ class ImageDraw:
if ink is not None and ink != fill: if ink is not None and ink != fill:
self.draw.draw_outline(shape, ink, 0) self.draw.draw_outline(shape, ink, 0)
def pieslice(self, xy, start, end, fill=None, outline=None, width=1): def pieslice(
self, xy: Coords, start, end, fill=None, outline=None, width=1
) -> None:
"""Draw a pieslice.""" """Draw a pieslice."""
ink, fill = self._getink(outline, fill) ink, fill = self._getink(outline, fill)
if fill is not None: if fill is not None:
@ -253,13 +266,13 @@ class ImageDraw:
if ink is not None and ink != fill and width != 0: if ink is not None and ink != fill and width != 0:
self.draw.draw_pieslice(xy, start, end, ink, 0, width) self.draw.draw_pieslice(xy, start, end, ink, 0, width)
def point(self, xy, fill=None): def point(self, xy: Coords, fill=None) -> None:
"""Draw one or more individual pixels.""" """Draw one or more individual pixels."""
ink, fill = self._getink(fill) ink, fill = self._getink(fill)
if ink is not None: if ink is not None:
self.draw.draw_points(xy, ink) self.draw.draw_points(xy, ink)
def polygon(self, xy, fill=None, outline=None, width=1): def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None:
"""Draw a polygon.""" """Draw a polygon."""
ink, fill = self._getink(outline, fill) ink, fill = self._getink(outline, fill)
if fill is not None: if fill is not None:
@ -267,7 +280,7 @@ class ImageDraw:
if ink is not None and ink != fill and width != 0: if ink is not None and ink != fill and width != 0:
if width == 1: if width == 1:
self.draw.draw_polygon(xy, ink, 0, width) self.draw.draw_polygon(xy, ink, 0, width)
else: elif self.im is not None:
# To avoid expanding the polygon outwards, # To avoid expanding the polygon outwards,
# use the fill as a mask # use the fill as a mask
mask = Image.new("1", self.im.size) mask = Image.new("1", self.im.size)
@ -291,12 +304,12 @@ class ImageDraw:
def regular_polygon( def regular_polygon(
self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1
): ) -> None:
"""Draw a regular polygon.""" """Draw a regular polygon."""
xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
self.polygon(xy, fill, outline, width) self.polygon(xy, fill, outline, width)
def rectangle(self, xy, fill=None, outline=None, width=1): def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None:
"""Draw a rectangle.""" """Draw a rectangle."""
ink, fill = self._getink(outline, fill) ink, fill = self._getink(outline, fill)
if fill is not None: if fill is not None:
@ -305,13 +318,13 @@ class ImageDraw:
self.draw.draw_rectangle(xy, ink, 0, width) self.draw.draw_rectangle(xy, ink, 0, width)
def rounded_rectangle( def rounded_rectangle(
self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None
): ) -> None:
"""Draw a rounded rectangle.""" """Draw a rounded rectangle."""
if isinstance(xy[0], (list, tuple)): if isinstance(xy[0], (list, tuple)):
(x0, y0), (x1, y1) = xy (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy)
else: else:
x0, y0, x1, y1 = xy x0, y0, x1, y1 = cast(Sequence[float], xy)
if x1 < x0: if x1 < x0:
msg = "x1 must be greater than or equal to x0" msg = "x1 must be greater than or equal to x0"
raise ValueError(msg) raise ValueError(msg)
@ -346,7 +359,8 @@ class ImageDraw:
r = d // 2 r = d // 2
ink, fill = self._getink(outline, fill) ink, fill = self._getink(outline, fill)
def draw_corners(pieslice): def draw_corners(pieslice) -> None:
parts: tuple[tuple[tuple[float, float, float, float], int, int], ...]
if full_x: if full_x:
# Draw top and bottom halves # Draw top and bottom halves
parts = ( parts = (
@ -361,7 +375,8 @@ class ImageDraw:
) )
else: else:
# Draw four separate corners # Draw four separate corners
parts = [] parts = tuple(
part
for i, part in enumerate( for i, part in enumerate(
( (
((x0, y0, x0 + d, y0 + d), 180, 270), ((x0, y0, x0 + d, y0 + d), 180, 270),
@ -369,9 +384,9 @@ class ImageDraw:
((x1 - d, y1 - d, x1, y1), 0, 90), ((x1 - d, y1 - d, x1, y1), 0, 90),
((x0, y1 - d, x0 + d, y1), 90, 180), ((x0, y1 - d, x0 + d, y1), 90, 180),
) )
): )
if corners[i]: if corners[i]
parts.append(part) )
for part in parts: for part in parts:
if pieslice: if pieslice:
self.draw.draw_pieslice(*(part + (fill, 1))) self.draw.draw_pieslice(*(part + (fill, 1)))
@ -431,12 +446,12 @@ class ImageDraw:
right[3] -= r + 1 right[3] -= r + 1
self.draw.draw_rectangle(right, ink, 1) self.draw.draw_rectangle(right, ink, 1)
def _multiline_check(self, text): def _multiline_check(self, text) -> bool:
split_character = "\n" if isinstance(text, str) else b"\n" split_character = "\n" if isinstance(text, str) else b"\n"
return split_character in text return split_character in text
def _multiline_split(self, text): def _multiline_split(self, text) -> list[str | bytes]:
split_character = "\n" if isinstance(text, str) else b"\n" split_character = "\n" if isinstance(text, str) else b"\n"
return text.split(split_character) return text.split(split_character)
@ -465,7 +480,7 @@ class ImageDraw:
embedded_color=False, embedded_color=False,
*args, *args,
**kwargs, **kwargs,
): ) -> None:
"""Draw text.""" """Draw text."""
if embedded_color and self.mode not in ("RGB", "RGBA"): if embedded_color and self.mode not in ("RGB", "RGBA"):
msg = "Embedded color supported only in RGB and RGBA modes" msg = "Embedded color supported only in RGB and RGBA modes"
@ -497,7 +512,7 @@ class ImageDraw:
return fill return fill
return ink return ink
def draw_text(ink, stroke_width=0, stroke_offset=None): def draw_text(ink, stroke_width=0, stroke_offset=None) -> None:
mode = self.fontmode mode = self.fontmode
if stroke_width == 0 and embedded_color: if stroke_width == 0 and embedded_color:
mode = "RGBA" mode = "RGBA"
@ -520,7 +535,7 @@ class ImageDraw:
*args, *args,
**kwargs, **kwargs,
) )
coord = coord[0] + offset[0], coord[1] + offset[1] coord = [coord[0] + offset[0], coord[1] + offset[1]]
except AttributeError: except AttributeError:
try: try:
mask = font.getmask( mask = font.getmask(
@ -539,7 +554,7 @@ class ImageDraw:
except TypeError: except TypeError:
mask = font.getmask(text) mask = font.getmask(text)
if stroke_offset: if stroke_offset:
coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] coord = [coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]]
if mode == "RGBA": if mode == "RGBA":
# font.getmask2(mode="RGBA") returns color in RGB bands and mask in A # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A
# extract mask and set text alpha # extract mask and set text alpha
@ -547,7 +562,10 @@ class ImageDraw:
ink_alpha = struct.pack("i", ink)[3] ink_alpha = struct.pack("i", ink)[3]
color.fillband(3, ink_alpha) color.fillband(3, ink_alpha)
x, y = coord x, y = coord
self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) if self.im is not None:
self.im.paste(
color, (x, y, x + mask.size[0], y + mask.size[1]), mask
)
else: else:
self.draw.draw_bitmap(coord, mask, ink) self.draw.draw_bitmap(coord, mask, ink)
@ -584,7 +602,7 @@ class ImageDraw:
embedded_color=False, embedded_color=False,
*, *,
font_size=None, font_size=None,
): ) -> None:
if direction == "ttb": if direction == "ttb":
msg = "ttb direction is unsupported for multiline text" msg = "ttb direction is unsupported for multiline text"
raise ValueError(msg) raise ValueError(msg)
@ -693,7 +711,7 @@ class ImageDraw:
embedded_color=False, embedded_color=False,
*, *,
font_size=None, font_size=None,
): ) -> tuple[int, int, int, int]:
"""Get the bounding box of a given string, in pixels.""" """Get the bounding box of a given string, in pixels."""
if embedded_color and self.mode not in ("RGB", "RGBA"): if embedded_color and self.mode not in ("RGB", "RGBA"):
msg = "Embedded color supported only in RGB and RGBA modes" msg = "Embedded color supported only in RGB and RGBA modes"
@ -738,7 +756,7 @@ class ImageDraw:
embedded_color=False, embedded_color=False,
*, *,
font_size=None, font_size=None,
): ) -> tuple[int, int, int, int]:
if direction == "ttb": if direction == "ttb":
msg = "ttb direction is unsupported for multiline text" msg = "ttb direction is unsupported for multiline text"
raise ValueError(msg) raise ValueError(msg)
@ -777,7 +795,7 @@ class ImageDraw:
elif anchor[1] == "d": elif anchor[1] == "d":
top -= (len(lines) - 1) * line_spacing top -= (len(lines) - 1) * line_spacing
bbox = None bbox: tuple[int, int, int, int] | None = None
for idx, line in enumerate(lines): for idx, line in enumerate(lines):
left = xy[0] left = xy[0]
@ -828,7 +846,7 @@ class ImageDraw:
return bbox return bbox
def Draw(im, mode=None): def Draw(im, mode: str | None = None) -> ImageDraw:
""" """
A simple 2D drawing interface for PIL images. A simple 2D drawing interface for PIL images.
@ -876,7 +894,7 @@ def getdraw(im=None, hints=None):
return im, handler return im, handler
def floodfill(image, xy, value, border=None, thresh=0): def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None:
""" """
(experimental) Fills a bounded region with a given color. (experimental) Fills a bounded region with a given color.
@ -932,7 +950,9 @@ def floodfill(image, xy, value, border=None, thresh=0):
edge = new_edge edge = new_edge
def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): def _compute_regular_polygon_vertices(
bounding_circle, n_sides, rotation
) -> list[tuple[float, float]]:
""" """
Generate a list of vertices for a 2D regular polygon. Generate a list of vertices for a 2D regular polygon.
@ -982,7 +1002,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
# 1.2 Check `bounding_circle` has an appropriate value # 1.2 Check `bounding_circle` has an appropriate value
if not isinstance(bounding_circle, (list, tuple)): if not isinstance(bounding_circle, (list, tuple)):
msg = "bounding_circle should be a tuple" msg = "bounding_circle should be a sequence"
raise TypeError(msg) raise TypeError(msg)
if len(bounding_circle) == 3: if len(bounding_circle) == 3:
@ -1014,7 +1034,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
raise ValueError(msg) raise ValueError(msg)
# 2. Define Helper Functions # 2. Define Helper Functions
def _apply_rotation(point, degrees, centroid): def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]:
return ( return (
round( round(
point[0] * math.cos(math.radians(360 - degrees)) point[0] * math.cos(math.radians(360 - degrees))
@ -1030,11 +1050,11 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
), ),
) )
def _compute_polygon_vertex(centroid, polygon_radius, angle): def _compute_polygon_vertex(angle: float) -> tuple[int, int]:
start_point = [polygon_radius, 0] start_point = [polygon_radius, 0]
return _apply_rotation(start_point, angle, centroid) return _apply_rotation(start_point, angle)
def _get_angles(n_sides, rotation): def _get_angles(n_sides: int, rotation: float) -> list[float]:
angles = [] angles = []
degrees = 360 / n_sides degrees = 360 / n_sides
# Start with the bottom left polygon vertex # Start with the bottom left polygon vertex
@ -1050,12 +1070,10 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
angles = _get_angles(n_sides, rotation) angles = _get_angles(n_sides, rotation)
# 4. Compute Vertices # 4. Compute Vertices
return [ return [_compute_polygon_vertex(angle) for angle in angles]
_compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles
]
def _color_diff(color1, color2): def _color_diff(color1, color2: float | tuple[int, ...]) -> float:
""" """
Uses 1-norm distance to calculate difference between two values. Uses 1-norm distance to calculate difference between two values.
""" """

View File

@ -91,7 +91,7 @@ def _tilesort(t):
class _Tile(NamedTuple): class _Tile(NamedTuple):
encoder_name: str codec_name: str
extents: tuple[int, int, int, int] extents: tuple[int, int, int, int]
offset: int offset: int
args: tuple[Any, ...] | str | None args: tuple[Any, ...] | str | None

View File

@ -872,7 +872,7 @@ def load_path(filename):
raise OSError(msg) raise OSError(msg)
def load_default(size=None): def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
"""If FreeType support is available, load a version of Aileron Regular, """If FreeType support is available, load a version of Aileron Regular,
https://dotcolon.net/font/aileron, with a more limited character set. https://dotcolon.net/font/aileron, with a more limited character set.

View File

@ -149,18 +149,7 @@ def grabclipboard():
session_type = None session_type = None
if shutil.which("wl-paste") and session_type in ("wayland", None): if shutil.which("wl-paste") and session_type in ("wayland", None):
output = subprocess.check_output(["wl-paste", "-l"]).decode() args = ["wl-paste", "-t", "image"]
mimetypes = output.splitlines()
if "image/png" in mimetypes:
mimetype = "image/png"
elif mimetypes:
mimetype = mimetypes[0]
else:
mimetype = None
args = ["wl-paste"]
if mimetype:
args.extend(["-t", mimetype])
elif shutil.which("xclip") and session_type in ("x11", None): elif shutil.which("xclip") and session_type in ("x11", None):
args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]
else: else:
@ -168,10 +157,29 @@ def grabclipboard():
raise NotImplementedError(msg) raise NotImplementedError(msg)
p = subprocess.run(args, capture_output=True) p = subprocess.run(args, capture_output=True)
if p.returncode != 0:
err = p.stderr err = p.stderr
for silent_error in [
# wl-paste, when the clipboard is empty
b"Nothing is copied",
# Ubuntu/Debian wl-paste, when the clipboard is empty
b"No selection",
# Ubuntu/Debian wl-paste, when an image isn't available
b"No suitable type of content copied",
# wl-paste or Ubuntu/Debian xclip, when an image isn't available
b" not available",
# xclip, when an image isn't available
b"cannot convert ",
# xclip, when the clipboard isn't initialized
b"xclip: Error: There is no owner for the ",
]:
if silent_error in err:
return None
msg = f"{args[0]} error"
if err: if err:
msg = f"{args[0]} error: {err.strip().decode()}" msg += f": {err.strip().decode()}"
raise ChildProcessError(msg) raise ChildProcessError(msg)
data = io.BytesIO(p.stdout) data = io.BytesIO(p.stdout)
im = Image.open(data) im = Image.open(data)
im.load() im.load()

View File

@ -19,19 +19,26 @@ from __future__ import annotations
import sys import sys
from io import BytesIO from io import BytesIO
from typing import Callable
from . import Image from . import Image
from ._util import is_path from ._util import is_path
qt_version: str | None
qt_versions = [ qt_versions = [
["6", "PyQt6"], ["6", "PyQt6"],
["side6", "PySide6"], ["side6", "PySide6"],
] ]
# If a version has already been imported, attempt it first # If a version has already been imported, attempt it first
qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True) qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True)
for qt_version, qt_module in qt_versions: for version, qt_module in qt_versions:
try: try:
QBuffer: type
QIODevice: type
QImage: type
QPixmap: type
qRgba: Callable[[int, int, int, int], int]
if qt_module == "PyQt6": if qt_module == "PyQt6":
from PyQt6.QtCore import QBuffer, QIODevice from PyQt6.QtCore import QBuffer, QIODevice
from PyQt6.QtGui import QImage, QPixmap, qRgba from PyQt6.QtGui import QImage, QPixmap, qRgba
@ -41,6 +48,7 @@ for qt_version, qt_module in qt_versions:
except (ImportError, RuntimeError): except (ImportError, RuntimeError):
continue continue
qt_is_installed = True qt_is_installed = True
qt_version = version
break break
else: else:
qt_is_installed = False qt_is_installed = False

View File

@ -184,7 +184,7 @@ class UnixViewer(Viewer):
@abc.abstractmethod @abc.abstractmethod
def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
pass # pragma: no cover pass
def get_command(self, file: str, **options: Any) -> str: def get_command(self, file: str, **options: Any) -> str:
command = self.get_command_ex(file, **options)[0] command = self.get_command_ex(file, **options)[0]

View File

@ -62,6 +62,7 @@ Libjpeg ref.:
https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html
""" """
from __future__ import annotations from __future__ import annotations
# fmt: off # fmt: off

View File

@ -188,9 +188,9 @@ def _save(im, fp, filename, save_all=False):
x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0)
info = { info = {
"title": None "title": (
if is_appending None if is_appending else os.path.splitext(os.path.basename(filename))[0]
else os.path.splitext(os.path.basename(filename))[0], ),
"author": None, "author": None,
"subject": None, "subject": None,
"keywords": None, "keywords": None,

View File

@ -8,6 +8,7 @@ import os
import re import re
import time import time
import zlib import zlib
from typing import TYPE_CHECKING, Any, List, Union
# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
@ -239,12 +240,18 @@ class PdfName:
return bytes(result) return bytes(result)
class PdfArray(list): class PdfArray(List[Any]):
def __bytes__(self): def __bytes__(self):
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
class PdfDict(collections.UserDict): if TYPE_CHECKING:
_DictBase = collections.UserDict[Union[str, bytes], Any]
else:
_DictBase = collections.UserDict
class PdfDict(_DictBase):
def __setattr__(self, key, value): def __setattr__(self, key, value):
if key == "data": if key == "data":
collections.UserDict.__setattr__(self, key, value) collections.UserDict.__setattr__(self, key, value)

View File

@ -25,6 +25,7 @@ import sys
from ._deprecate import deprecate from ._deprecate import deprecate
FFI: type
try: try:
from cffi import FFI from cffi import FFI

View File

@ -50,6 +50,7 @@ import warnings
from collections.abc import MutableMapping from collections.abc import MutableMapping
from fractions import Fraction from fractions import Fraction
from numbers import Number, Rational from numbers import Number, Rational
from typing import TYPE_CHECKING, Any, Callable
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -306,6 +307,13 @@ _load_dispatch = {}
_write_dispatch = {} _write_dispatch = {}
def _delegate(op):
def delegate(self, *args):
return getattr(self._val, op)(*args)
return delegate
class IFDRational(Rational): class IFDRational(Rational):
"""Implements a rational class where 0/0 is a legal value to match """Implements a rational class where 0/0 is a legal value to match
the in the wild use of exif rationals. the in the wild use of exif rationals.
@ -391,12 +399,6 @@ class IFDRational(Rational):
self._numerator = _numerator self._numerator = _numerator
self._denominator = _denominator self._denominator = _denominator
def _delegate(op):
def delegate(self, *args):
return getattr(self._val, op)(*args)
return delegate
""" a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',
'truediv', 'rtruediv', 'floordiv', 'rfloordiv', 'truediv', 'rtruediv', 'floordiv', 'rfloordiv',
'mod','rmod', 'pow','rpow', 'pos', 'neg', 'mod','rmod', 'pow','rpow', 'pos', 'neg',
@ -436,7 +438,50 @@ class IFDRational(Rational):
__int__ = _delegate("__int__") __int__ = _delegate("__int__")
class ImageFileDirectory_v2(MutableMapping): def _register_loader(idx, size):
def decorator(func):
from .TiffTags import TYPES
if func.__name__.startswith("load_"):
TYPES[idx] = func.__name__[5:].replace("_", " ")
_load_dispatch[idx] = size, func # noqa: F821
return func
return decorator
def _register_writer(idx):
def decorator(func):
_write_dispatch[idx] = func # noqa: F821
return func
return decorator
def _register_basic(idx_fmt_name):
from .TiffTags import TYPES
idx, fmt, name = idx_fmt_name
TYPES[idx] = name
size = struct.calcsize("=" + fmt)
_load_dispatch[idx] = ( # noqa: F821
size,
lambda self, data, legacy_api=True: (
self._unpack(f"{len(data) // size}{fmt}", data)
),
)
_write_dispatch[idx] = lambda self, *values: ( # noqa: F821
b"".join(self._pack(fmt, value) for value in values)
)
if TYPE_CHECKING:
_IFDv2Base = MutableMapping[int, Any]
else:
_IFDv2Base = MutableMapping
class ImageFileDirectory_v2(_IFDv2Base):
"""This class represents a TIFF tag directory. To speed things up, we """This class represents a TIFF tag directory. To speed things up, we
don't decode tags unless they're asked for. don't decode tags unless they're asked for.
@ -497,6 +542,9 @@ class ImageFileDirectory_v2(MutableMapping):
""" """
_load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {}
_write_dispatch: dict[int, Callable[..., Any]] = {}
def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None):
"""Initialize an ImageFileDirectory. """Initialize an ImageFileDirectory.
@ -531,7 +579,10 @@ class ImageFileDirectory_v2(MutableMapping):
prefix = property(lambda self: self._prefix) prefix = property(lambda self: self._prefix)
offset = property(lambda self: self._offset) offset = property(lambda self: self._offset)
legacy_api = property(lambda self: self._legacy_api)
@property
def legacy_api(self):
return self._legacy_api
@legacy_api.setter @legacy_api.setter
def legacy_api(self, value): def legacy_api(self, value):
@ -674,40 +725,6 @@ class ImageFileDirectory_v2(MutableMapping):
def _pack(self, fmt, *values): def _pack(self, fmt, *values):
return struct.pack(self._endian + fmt, *values) return struct.pack(self._endian + fmt, *values)
def _register_loader(idx, size):
def decorator(func):
from .TiffTags import TYPES
if func.__name__.startswith("load_"):
TYPES[idx] = func.__name__[5:].replace("_", " ")
_load_dispatch[idx] = size, func # noqa: F821
return func
return decorator
def _register_writer(idx):
def decorator(func):
_write_dispatch[idx] = func # noqa: F821
return func
return decorator
def _register_basic(idx_fmt_name):
from .TiffTags import TYPES
idx, fmt, name = idx_fmt_name
TYPES[idx] = name
size = struct.calcsize("=" + fmt)
_load_dispatch[idx] = ( # noqa: F821
size,
lambda self, data, legacy_api=True: (
self._unpack(f"{len(data) // size}{fmt}", data)
),
)
_write_dispatch[idx] = lambda self, *values: ( # noqa: F821
b"".join(self._pack(fmt, value) for value in values)
)
list( list(
map( map(
_register_basic, _register_basic,
@ -995,7 +1012,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
tagdata = property(lambda self: self._tagdata) tagdata = property(lambda self: self._tagdata)
# defined in ImageFileDirectory_v2 # defined in ImageFileDirectory_v2
tagtype: dict tagtype: dict[int, int]
"""Dictionary of tag types""" """Dictionary of tag types"""
@classmethod @classmethod
@ -1835,11 +1852,11 @@ def _save(im, fp, filename):
tags = list(atts.items()) tags = list(atts.items())
tags.sort() tags.sort()
a = (rawmode, compression, _fp, filename, tags, types) a = (rawmode, compression, _fp, filename, tags, types)
e = Image._getencoder(im.mode, "libtiff", a, encoderconfig) encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
e.setimage(im.im, (0, 0) + im.size) encoder.setimage(im.im, (0, 0) + im.size)
while True: while True:
# undone, change to self.decodermaxblock: # undone, change to self.decodermaxblock:
errcode, data = e.encode(16 * 1024)[1:] errcode, data = encoder.encode(16 * 1024)[1:]
if not _fp: if not _fp:
fp.write(data) fp.write(data)
if errcode: if errcode:

View File

@ -22,7 +22,7 @@ from collections import namedtuple
class TagInfo(namedtuple("_TagInfo", "value name type length enum")): class TagInfo(namedtuple("_TagInfo", "value name type length enum")):
__slots__ = [] __slots__: list[str] = []
def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None):
return super().__new__(cls, value, name, type, length, enum or {}) return super().__new__(cls, value, name, type, length, enum or {})
@ -437,7 +437,7 @@ _populate()
## ##
# Map type numbers to type names -- defined in ImageFileDirectory. # Map type numbers to type names -- defined in ImageFileDirectory.
TYPES = {} TYPES: dict[int, str] = {}
# #
# These tags are handled by default in libtiff, without # These tags are handled by default in libtiff, without

View File

@ -12,6 +12,7 @@ Use PIL.__version__ for this Pillow version.
;-) ;-)
""" """
from __future__ import annotations from __future__ import annotations
from . import _version from . import _version

5
src/PIL/_imaging.pyi Normal file
View File

@ -0,0 +1,5 @@
from __future__ import annotations
from typing import Any
def __getattr__(name: str) -> Any: ...

View File

@ -1,10 +1,12 @@
""" Find compiled module linking to Tcl / Tk libraries """ Find compiled module linking to Tcl / Tk libraries
""" """
from __future__ import annotations from __future__ import annotations
import sys import sys
import tkinter import tkinter
from tkinter import _tkinter as tk
tk = getattr(tkinter, "_tkinter")
try: try:
if hasattr(sys, "pypy_find_executable"): if hasattr(sys, "pypy_find_executable"):

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from typing import Sequence, Union
if sys.version_info >= (3, 10): if sys.version_info >= (3, 10):
from typing import TypeGuard from typing import TypeGuard
@ -15,4 +16,7 @@ else:
return bool return bool
Coords = Union[Sequence[float], Sequence[Sequence[float]]]
__all__ = ["TypeGuard"] __all__ = ["TypeGuard"]

5
src/PIL/_webp.pyi Normal file
View File

@ -0,0 +1,5 @@
from __future__ import annotations
from typing import Any
def __getattr__(name: str) -> Any: ...

View File

@ -33,9 +33,14 @@ commands =
[testenv:mypy] [testenv:mypy]
skip_install = true skip_install = true
deps = deps =
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython ipython
mypy==1.7.1 mypy==1.7.1
numpy numpy
packaging
types-cffi
types-defusedxml
extras = extras =
typing typing
commands = commands =