mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-11-14 05:36:48 +03:00
Merge branch 'main' into convert_mode
This commit is contained in:
commit
e617e604b5
|
@ -10,6 +10,8 @@ exclude_also =
|
|||
if DEBUG:
|
||||
# Don't complain about compatibility code for missing optional dependencies
|
||||
except ImportError
|
||||
if TYPE_CHECKING:
|
||||
@abc.abstractmethod
|
||||
|
||||
[run]
|
||||
omit =
|
||||
|
|
2
.github/workflows/release-drafter.yml
vendored
2
.github/workflows/release-drafter.yml
vendored
|
@ -23,6 +23,6 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# 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:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
1
.github/workflows/system-info.py
vendored
1
.github/workflows/system-info.py
vendored
|
@ -6,6 +6,7 @@ This sort of info is missing from GitHub Actions.
|
|||
Requested here:
|
||||
https://github.com/actions/virtual-environments/issues/79
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
|
5
.github/workflows/test-cygwin.yml
vendored
5
.github/workflows/test-cygwin.yml
vendored
|
@ -49,7 +49,6 @@ jobs:
|
|||
- name: Install Cygwin
|
||||
uses: egor-tensin/setup-cygwin@v4
|
||||
with:
|
||||
platform: x86_64
|
||||
packages: >
|
||||
gcc-g++
|
||||
ghostscript
|
||||
|
@ -81,7 +80,7 @@ jobs:
|
|||
zlib-devel
|
||||
|
||||
- name: Add Lapack to PATH
|
||||
uses: egor-tensin/cleanup-path@v3
|
||||
uses: egor-tensin/cleanup-path@v4
|
||||
with:
|
||||
dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
|
||||
|
||||
|
@ -142,7 +141,7 @@ jobs:
|
|||
bash.exe .ci/after_success.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: GHA_Cygwin
|
||||
|
|
2
.github/workflows/test-docker.yml
vendored
2
.github/workflows/test-docker.yml
vendored
|
@ -101,7 +101,7 @@ jobs:
|
|||
MATRIX_DOCKER: ${{ matrix.docker }}
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
with:
|
||||
flags: GHA_Docker
|
||||
name: ${{ matrix.docker }}
|
||||
|
|
2
.github/workflows/test-mingw.yml
vendored
2
.github/workflows/test-mingw.yml
vendored
|
@ -82,7 +82,7 @@ jobs:
|
|||
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: GHA_Windows
|
||||
|
|
2
.github/workflows/test-windows.yml
vendored
2
.github/workflows/test-windows.yml
vendored
|
@ -202,7 +202,7 @@ jobs:
|
|||
shell: pwsh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: GHA_Windows
|
||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -149,7 +149,7 @@ jobs:
|
|||
.ci/after_success.sh
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
with:
|
||||
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
|
||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||
|
|
16
.github/workflows/wheels-dependencies.sh
vendored
16
.github/workflows/wheels-dependencies.sh
vendored
|
@ -72,13 +72,11 @@ function build {
|
|||
|
||||
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
|
||||
if [ -n "$IS_MACOS" ]; then
|
||||
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
|
||||
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 libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
||||
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
|
||||
fi
|
||||
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 libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
|
||||
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
|
||||
fi
|
||||
else
|
||||
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
|
||||
|
@ -131,13 +129,13 @@ untar pillow-depends-main.zip
|
|||
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
# 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
|
||||
# remove cairo to fix building harfbuzz on arm64
|
||||
# remove lcms2 and libpng to fix building openjpeg on arm64
|
||||
# remove zstd to avoid inclusion on x86_64
|
||||
# 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
|
||||
fi
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.1.9
|
||||
rev: v0.2.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 23.12.1
|
||||
rev: 24.1.1
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.6
|
||||
rev: 1.7.7
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: [--severity-level=high]
|
||||
|
@ -48,12 +48,12 @@ repos:
|
|||
- id: sphinx-lint
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: 1.5.3
|
||||
rev: 1.7.0
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.15
|
||||
rev: v0.16
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
|
|
|
@ -5,6 +5,12 @@ Changelog (Pillow)
|
|||
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
|
||||
[radarhere]
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
Helper functions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
|
|
@ -10,7 +10,7 @@ from .helper import assert_image_similar
|
|||
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 [
|
||||
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
|
||||
|
||||
|
||||
def test_questionable():
|
||||
def test_questionable() -> None:
|
||||
"""These shouldn't crash/dos, but it's not well defined that these
|
||||
are in spec"""
|
||||
supported = [
|
||||
|
@ -80,7 +80,7 @@ def test_good() -> None:
|
|||
"rgb32bf.bmp": "rgb24.png",
|
||||
}
|
||||
|
||||
def get_compare(f):
|
||||
def get_compare(f: str) -> str:
|
||||
name = os.path.split(f)[1]
|
||||
if name in file_map:
|
||||
return os.path.join(base, "html", file_map[name])
|
||||
|
|
|
@ -23,11 +23,11 @@ def test_imageops_box_blur() -> None:
|
|||
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))
|
||||
|
||||
|
||||
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())
|
||||
for data_row in data:
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
assert_image(box_blur(im, radius, passes), data, delta)
|
||||
rgba = Image.merge("RGBA", (im, im, im, im))
|
||||
|
|
|
@ -15,7 +15,9 @@ except ImportError:
|
|||
|
||||
|
||||
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):
|
||||
size_1d, size_2d, size_3d = size
|
||||
else:
|
||||
|
|
|
@ -47,7 +47,7 @@ def test_apng_basic() -> None:
|
|||
"filename",
|
||||
("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:
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
|
@ -338,7 +338,7 @@ def test_apng_syntax_errors() -> None:
|
|||
"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 Image.open(f"Tests/images/apng/{test_file}") as im:
|
||||
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("duplicate", (True, False))
|
||||
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:
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ def test_seek_mode_2() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_read_n0(bytesmode) -> None:
|
||||
def test_read_n0(bytesmode: bool) -> None:
|
||||
# Arrange
|
||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
@ -80,7 +80,7 @@ def test_read_n0(bytesmode) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_read_n(bytesmode) -> None:
|
||||
def test_read_n(bytesmode: bool) -> None:
|
||||
# Arrange
|
||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
@ -96,7 +96,7 @@ def test_read_n(bytesmode) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_read_eof(bytesmode) -> None:
|
||||
def test_read_eof(bytesmode: bool) -> None:
|
||||
# Arrange
|
||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 22, 100)
|
||||
|
@ -112,7 +112,7 @@ def test_read_eof(bytesmode) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_readline(bytesmode) -> None:
|
||||
def test_readline(bytesmode: bool) -> None:
|
||||
# Arrange
|
||||
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 0, 120)
|
||||
|
@ -127,7 +127,7 @@ def test_readline(bytesmode) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bytesmode", (True, False))
|
||||
def test_readlines(bytesmode) -> None:
|
||||
def test_readlines(bytesmode: bool) -> None:
|
||||
# Arrange
|
||||
expected = [
|
||||
"This is line 1\n",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Test DdsImagePlugin"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
|
|
@ -84,7 +84,7 @@ simple_eps_file_with_long_binary_data = (
|
|||
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
|
||||
)
|
||||
@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)
|
||||
with Image.open(filename) as image:
|
||||
image.load(scale=scale)
|
||||
|
@ -129,28 +129,28 @@ def test_binary_header_only() -> None:
|
|||
|
||||
|
||||
@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))
|
||||
with pytest.raises(SyntaxError):
|
||||
EpsImagePlugin.EpsImageFile(data)
|
||||
|
||||
|
||||
@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))
|
||||
with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'):
|
||||
EpsImagePlugin.EpsImageFile(data)
|
||||
|
||||
|
||||
@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))
|
||||
with pytest.raises(OSError, match="cannot determine EPS bounding box"):
|
||||
EpsImagePlugin.EpsImageFile(data)
|
||||
|
||||
|
||||
@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(
|
||||
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))
|
||||
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))
|
||||
with pytest.raises(SyntaxError, match="not an EPS file"):
|
||||
EpsImagePlugin.EpsImageFile(data)
|
||||
|
||||
|
||||
@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))
|
||||
EpsImagePlugin.EpsImageFile(data)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
@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))
|
||||
with Image.open(data) as img:
|
||||
img.load()
|
||||
|
@ -305,7 +305,7 @@ def test_render_scale2() -> None:
|
|||
|
||||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
@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:
|
||||
new_size = (100, 100)
|
||||
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.parametrize("filename", (FILE1, FILE2))
|
||||
def test_thumbnail(filename) -> None:
|
||||
def test_thumbnail(filename: str) -> None:
|
||||
# Issue #619
|
||||
with Image.open(filename) as im:
|
||||
new_size = (100, 100)
|
||||
|
@ -335,7 +335,7 @@ def test_readline_psfile(tmp_path: Path) -> None:
|
|||
line_endings = ["\r\n", "\n", "\n\r", "\r"]
|
||||
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" % (
|
||||
"".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") == "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"))
|
||||
with pytest.warns(DeprecationWarning):
|
||||
t = EpsImagePlugin.PSFile(f)
|
||||
_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")
|
||||
with open(f, "wb") as w:
|
||||
w.write(test_string.encode("latin-1"))
|
||||
|
@ -376,7 +376,7 @@ def test_psfile_deprecation() -> None:
|
|||
"line_ending",
|
||||
(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)
|
||||
data = io.BytesIO(simple_file)
|
||||
test_file = EpsImagePlugin.EpsImageFile(data)
|
||||
|
@ -394,7 +394,7 @@ def test_readline(prefix, line_ending) -> None:
|
|||
"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
|
||||
with Image.open(filename) as img:
|
||||
assert img.mode == "RGB"
|
||||
|
@ -417,7 +417,7 @@ def test_emptyline() -> None:
|
|||
"test_file",
|
||||
["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 pytest.raises(Image.UnidentifiedImageError):
|
||||
with Image.open(f):
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
import warnings
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -144,13 +145,13 @@ def test_strategy() -> None:
|
|||
|
||||
|
||||
def test_optimize() -> None:
|
||||
def test_grayscale(optimize):
|
||||
def test_grayscale(optimize: int) -> int:
|
||||
im = Image.new("L", (1, 1), 0)
|
||||
filename = BytesIO()
|
||||
im.save(filename, "GIF", optimize=optimize)
|
||||
return len(filename.getvalue())
|
||||
|
||||
def test_bilevel(optimize):
|
||||
def test_bilevel(optimize: int) -> int:
|
||||
im = Image.new("1", (1, 1), 0)
|
||||
test_file = BytesIO()
|
||||
im.save(test_file, "GIF", optimize=optimize)
|
||||
|
@ -178,7 +179,9 @@ def test_optimize() -> None:
|
|||
(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.
|
||||
# Size bigger and smaller than 512x512.
|
||||
# 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"),
|
||||
),
|
||||
)
|
||||
def test_loading_multiple_palettes(path, mode) -> None:
|
||||
def test_loading_multiple_palettes(path: str, mode: str) -> None:
|
||||
with Image.open(path) as im:
|
||||
assert im.mode == "P"
|
||||
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:
|
||||
# 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")
|
||||
im.copy().save(out, *args, **kwargs)
|
||||
im.copy().save(out, **kwargs)
|
||||
reloaded = Image.open(out)
|
||||
|
||||
return reloaded
|
||||
|
@ -429,7 +432,7 @@ def test_seek_rewind() -> None:
|
|||
("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
|
||||
with Image.open(path) as im:
|
||||
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
|
||||
try:
|
||||
with Image.open("Tests/images/transparent_dispose.gif") as img:
|
||||
|
@ -889,7 +895,9 @@ def test_identical_frames(tmp_path: Path) -> None:
|
|||
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")
|
||||
im_list = [
|
||||
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:
|
||||
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)
|
||||
with Image.open(out) as reread:
|
||||
assert reread.info["version"] == version
|
||||
|
@ -1088,7 +1096,7 @@ def test_append_images(tmp_path: Path) -> None:
|
|||
assert reread.n_frames == 3
|
||||
|
||||
# Tests appending using a generator
|
||||
def im_generator(ims):
|
||||
def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]:
|
||||
yield from ims
|
||||
|
||||
im.save(out, save_all=True, append_images=im_generator(ims))
|
||||
|
|
|
@ -5,6 +5,7 @@ import re
|
|||
import warnings
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -42,7 +43,7 @@ TEST_FILE = "Tests/images/hopper.jpg"
|
|||
|
||||
@skip_unless_feature("jpg")
|
||||
class TestFileJpeg:
|
||||
def roundtrip(self, im, **options):
|
||||
def roundtrip(self, im: Image.Image, **options: Any) -> Image.Image:
|
||||
out = BytesIO()
|
||||
im.save(out, "JPEG", **options)
|
||||
test_bytes = out.tell()
|
||||
|
@ -51,7 +52,7 @@ class TestFileJpeg:
|
|||
im.bytes = test_bytes # for testing only
|
||||
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
|
||||
:param size: tuple
|
||||
:param mode: optional image mode
|
||||
|
@ -71,7 +72,7 @@ class TestFileJpeg:
|
|||
assert im.get_format_mimetype() == "image/jpeg"
|
||||
|
||||
@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")
|
||||
im = Image.new("RGB", size)
|
||||
with pytest.raises(ValueError):
|
||||
|
@ -108,13 +109,11 @@ class TestFileJpeg:
|
|||
assert "comment" not in reloaded.info
|
||||
|
||||
# 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()
|
||||
im.save(out, format="JPEG", comment=comment)
|
||||
with Image.open(out) as reloaded:
|
||||
if not isinstance(comment, bytes):
|
||||
comment = comment.encode()
|
||||
assert reloaded.info["comment"] == comment
|
||||
assert reloaded.info["comment"] == b"Test comment text"
|
||||
|
||||
def test_cmyk(self) -> None:
|
||||
# Test CMYK handling. Thanks to Tim and Charlie for test data,
|
||||
|
@ -145,7 +144,7 @@ class TestFileJpeg:
|
|||
assert k > 0.9
|
||||
|
||||
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)
|
||||
|
||||
im = hopper()
|
||||
|
@ -161,8 +160,8 @@ class TestFileJpeg:
|
|||
"test_image_path",
|
||||
[TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"],
|
||||
)
|
||||
def test_dpi(self, test_image_path) -> None:
|
||||
def test(xdpi, ydpi=None):
|
||||
def test_dpi(self, test_image_path: str) -> None:
|
||||
def test(xdpi: int, ydpi: int | None = None):
|
||||
with Image.open(test_image_path) as im:
|
||||
im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi))
|
||||
return im.info.get("dpi")
|
||||
|
@ -207,7 +206,7 @@ class TestFileJpeg:
|
|||
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
|
||||
# The ICC APP marker can store 65519 bytes per marker, so
|
||||
# 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)
|
||||
|
||||
def test_subsampling(self) -> None:
|
||||
def getsampling(im):
|
||||
def getsampling(im: Image.Image):
|
||||
layer = im.layer
|
||||
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"
|
||||
)
|
||||
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:
|
||||
f = str(tmp_path / "temp.jpg")
|
||||
im.save(f, qtables=[[n] * 64] * n)
|
||||
|
@ -666,7 +665,7 @@ class TestFileJpeg:
|
|||
"blocks, rows, markers",
|
||||
((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
|
||||
out = BytesIO()
|
||||
im.save(
|
||||
|
@ -724,7 +723,7 @@ class TestFileJpeg:
|
|||
assert im.format == "JPEG"
|
||||
|
||||
@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()
|
||||
img = Image.new(mode, (20, 20))
|
||||
img.save(out, "JPEG")
|
||||
|
@ -993,12 +992,12 @@ class TestFileJpeg:
|
|||
# Even though this decoder never says that it is finished
|
||||
# the image should still end when there is no new data
|
||||
class InfiniteMockPyDecoder(ImageFile.PyDecoder):
|
||||
def decode(self, buffer):
|
||||
def decode(self, buffer: bytes) -> tuple[int, int]:
|
||||
return 0, 0
|
||||
|
||||
decoder = InfiniteMockPyDecoder(None)
|
||||
|
||||
def closure(mode, *args):
|
||||
def closure(mode: str, *args) -> InfiniteMockPyDecoder:
|
||||
decoder.__init__(mode, *args)
|
||||
return decoder
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import os
|
|||
import re
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -36,7 +37,7 @@ test_card.load()
|
|||
# 'Not enough memory to handle tile data'
|
||||
|
||||
|
||||
def roundtrip(im, **options):
|
||||
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
|
||||
out = BytesIO()
|
||||
im.save(out, "JPEG2000", **options)
|
||||
test_bytes = out.tell()
|
||||
|
@ -138,7 +139,7 @@ def test_prog_res_rt() -> None:
|
|||
|
||||
|
||||
@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)
|
||||
im = test_card.resize((d - 1, d - 1))
|
||||
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]:
|
||||
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):
|
||||
test_card.save(outfile, quality_layers=quality_layers)
|
||||
test_card.save(outfile, quality_layers=quality_layers_str)
|
||||
|
||||
|
||||
def test_layers() -> None:
|
||||
|
@ -233,7 +234,7 @@ def test_layers() -> None:
|
|||
("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()
|
||||
if name:
|
||||
out.name = name
|
||||
|
@ -278,7 +279,7 @@ def test_sgnd(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
||||
def test_rgba(ext) -> None:
|
||||
def test_rgba(ext: str) -> None:
|
||||
# Arrange
|
||||
with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im:
|
||||
# Act
|
||||
|
@ -289,7 +290,7 @@ def test_rgba(ext) -> None:
|
|||
|
||||
|
||||
@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:
|
||||
im.load()
|
||||
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"
|
||||
)
|
||||
@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"
|
||||
reference = f"{EXTRA_DIR}/{name}.ppm"
|
||||
|
||||
with Image.open(test) as im:
|
||||
epsilon = 3 # for YCbCr images
|
||||
epsilon = 3.0 # for YCbCr images
|
||||
with Image.open(reference) as im2:
|
||||
width, height = im2.size
|
||||
if name[-1] == "2":
|
||||
|
@ -400,7 +401,7 @@ def test_save_comment() -> None:
|
|||
"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 Image.open(f) as im:
|
||||
# Valgrind should not complain here
|
||||
|
|
|
@ -27,7 +27,7 @@ from .helper import (
|
|||
|
||||
@skip_unless_feature("libtiff")
|
||||
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"""
|
||||
# 1 bit
|
||||
assert im.mode == "1"
|
||||
|
@ -140,7 +140,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
|
||||
|
||||
@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"""
|
||||
f = str(tmp_path / "temp.tiff")
|
||||
with Image.open("Tests/images/hopper_g4.tif") as img:
|
||||
|
@ -243,7 +243,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
|
||||
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 = {
|
||||
37000 + k: v
|
||||
for k, v in enumerate(
|
||||
|
@ -284,7 +284,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
for libtiff in libtiffs:
|
||||
TiffImagePlugin.WRITE_LIBTIFF = libtiff
|
||||
|
||||
def check_tags(tiffinfo) -> None:
|
||||
def check_tags(
|
||||
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
|
||||
) -> None:
|
||||
im = hopper()
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
@ -502,7 +504,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert_image_equal_tofile(im, out)
|
||||
|
||||
@pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
|
||||
def test_palette_save(self, im, tmp_path: Path) -> None:
|
||||
def test_palette_save(self, im: Image.Image, tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
TiffImagePlugin.WRITE_LIBTIFF = True
|
||||
|
@ -514,7 +516,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert len(reloaded.tag_v2[320]) == 768
|
||||
|
||||
@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")
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
|
@ -647,7 +649,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Generate test image
|
||||
pilim = hopper()
|
||||
|
||||
def save_bytesio(compression=None) -> None:
|
||||
def save_bytesio(compression: str | None = None) -> None:
|
||||
buffer_io = io.BytesIO()
|
||||
pilim.save(buffer_io, format="tiff", compression=compression)
|
||||
buffer_io.seek(0)
|
||||
|
@ -731,7 +733,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert icc == icc_libtiff
|
||||
|
||||
def test_write_icc(self, tmp_path: Path) -> None:
|
||||
def check_write(libtiff) -> None:
|
||||
def check_write(libtiff: bool) -> None:
|
||||
TiffImagePlugin.WRITE_LIBTIFF = libtiff
|
||||
|
||||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||
|
@ -837,7 +839,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert reloaded.mode == "F"
|
||||
assert reloaded.getexif()[SAMPLEFORMAT] == 3
|
||||
|
||||
def test_lzma(self, capfd):
|
||||
def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None:
|
||||
try:
|
||||
with Image.open("Tests/images/hopper_lzma.tif") as im:
|
||||
assert im.mode == "RGB"
|
||||
|
@ -853,7 +855,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
sys.stderr.write(captured.err)
|
||||
raise
|
||||
|
||||
def test_webp(self, capfd):
|
||||
def test_webp(self, capfd: pytest.CaptureFixture[str]) -> None:
|
||||
try:
|
||||
with Image.open("Tests/images/hopper_webp.tif") as im:
|
||||
assert im.mode == "RGB"
|
||||
|
@ -971,7 +973,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png")
|
||||
|
||||
@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()
|
||||
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:
|
||||
assert im.mode == mode
|
||||
assert im.size == size
|
||||
|
@ -1086,7 +1090,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
@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))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
im.save(out, compression=compression)
|
||||
|
@ -1096,14 +1100,14 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert len(im.tag_v2[STRIPOFFSETS]) > 1
|
||||
|
||||
@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))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
|
||||
if not argument:
|
||||
TiffImagePlugin.STRIP_SIZE = 2**18
|
||||
try:
|
||||
arguments = {"compression": "tiff_adobe_deflate"}
|
||||
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
|
||||
if argument:
|
||||
arguments["strip_size"] = 2**18
|
||||
im.save(out, **arguments)
|
||||
|
@ -1114,7 +1118,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
TiffImagePlugin.STRIP_SIZE = 65536
|
||||
|
||||
@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))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
with pytest.raises(SystemError):
|
||||
|
@ -1134,7 +1138,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
("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:
|
||||
ims = im.get_child_images()
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ from .test_file_libtiff import LibTiffTestCase
|
|||
|
||||
|
||||
class TestFileLibTiffSmall(LibTiffTestCase):
|
||||
|
||||
"""The small lena image was failing on open in the libtiff
|
||||
decoder because the file pointer was set to the wrong place
|
||||
by a spurious seek. It wasn't failing with the byteio method.
|
||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import warnings
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -19,7 +20,7 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
|
|||
pytestmark = skip_unless_feature("jpg")
|
||||
|
||||
|
||||
def roundtrip(im, **options):
|
||||
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
|
||||
out = BytesIO()
|
||||
im.save(out, "MPO", **options)
|
||||
test_bytes = out.tell()
|
||||
|
@ -30,7 +31,7 @@ def roundtrip(im, **options):
|
|||
|
||||
|
||||
@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:
|
||||
im.load()
|
||||
assert im.mode == "RGB"
|
||||
|
@ -70,7 +71,7 @@ def test_context_manager() -> None:
|
|||
|
||||
|
||||
@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)
|
||||
with Image.open(test_file) as im:
|
||||
assert im.applist[0][0] == "APP1"
|
||||
|
@ -82,7 +83,7 @@ def test_app(test_file) -> None:
|
|||
|
||||
|
||||
@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:
|
||||
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)
|
||||
def test_mp(test_file) -> None:
|
||||
def test_mp(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
mpinfo = im._getmp()
|
||||
assert mpinfo[45056] == b"0100"
|
||||
|
@ -168,7 +169,7 @@ def test_mp_no_data() -> None:
|
|||
|
||||
|
||||
@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:
|
||||
mpinfo = im._getmp()
|
||||
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)
|
||||
def test_seek(test_file) -> None:
|
||||
def test_seek(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
assert im.tell() == 0
|
||||
# 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)
|
||||
def test_image_grab(test_file) -> None:
|
||||
def test_image_grab(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
assert im.tell() == 0
|
||||
im0 = im.tobytes()
|
||||
|
@ -244,7 +245,7 @@ def test_image_grab(test_file) -> None:
|
|||
|
||||
|
||||
@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:
|
||||
assert im.tell() == 0
|
||||
jpg0 = roundtrip(im)
|
||||
|
|
|
@ -6,6 +6,7 @@ import warnings
|
|||
import zlib
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -36,7 +37,7 @@ TEST_PNG_FILE = "Tests/images/hopper.png"
|
|||
MAGIC = PngImagePlugin._MAGIC
|
||||
|
||||
|
||||
def chunk(cid, *data):
|
||||
def chunk(cid: bytes, *data: bytes) -> bytes:
|
||||
test_file = BytesIO()
|
||||
PngImagePlugin.putchunk(*(test_file, cid) + data)
|
||||
return test_file.getvalue()
|
||||
|
@ -52,11 +53,11 @@ HEAD = MAGIC + IHDR
|
|||
TAIL = IDAT + IEND
|
||||
|
||||
|
||||
def load(data):
|
||||
def load(data: bytes) -> Image.Image:
|
||||
return Image.open(BytesIO(data))
|
||||
|
||||
|
||||
def roundtrip(im, **options):
|
||||
def roundtrip(im: Image.Image, **options: Any) -> Image.Image:
|
||||
out = BytesIO()
|
||||
im.save(out, "PNG", **options)
|
||||
out.seek(0)
|
||||
|
@ -65,7 +66,7 @@ def roundtrip(im, **options):
|
|||
|
||||
@skip_unless_feature("zlib")
|
||||
class TestFilePng:
|
||||
def get_chunks(self, filename):
|
||||
def get_chunks(self, filename: str) -> list[bytes]:
|
||||
chunks = []
|
||||
with open(filename, "rb") as fp:
|
||||
fp.read(8)
|
||||
|
@ -444,7 +445,7 @@ class TestFilePng:
|
|||
def test_unicode_text(self) -> None:
|
||||
# Check preservation of non-ASCII characters
|
||||
|
||||
def rt_text(value) -> None:
|
||||
def rt_text(value: str) -> None:
|
||||
im = Image.new("RGB", (32, 32))
|
||||
info = PngImagePlugin.PngInfo()
|
||||
info.add_text("Text", value)
|
||||
|
@ -644,7 +645,7 @@ class TestFilePng:
|
|||
@pytest.mark.parametrize(
|
||||
"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()
|
||||
with PngImagePlugin.PngStream(fp) as png:
|
||||
with pytest.raises(ValueError):
|
||||
|
@ -763,7 +764,7 @@ class TestFilePng:
|
|||
im.seek(1)
|
||||
|
||||
@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
|
||||
|
||||
if buffer:
|
||||
|
|
|
@ -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)
|
||||
with Image.open(fp) as im:
|
||||
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",
|
||||
],
|
||||
)
|
||||
def test_pfm_invalid(data) -> None:
|
||||
def test_pfm_invalid(data: bytes) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
with Image.open(BytesIO(data)):
|
||||
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:
|
||||
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),
|
||||
),
|
||||
)
|
||||
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")
|
||||
path2 = str(tmp_path / "temp2.ppm")
|
||||
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"))
|
||||
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")
|
||||
with open(path, "wb") as f:
|
||||
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"))
|
||||
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")
|
||||
with open(path, "wb") as f:
|
||||
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
|
||||
),
|
||||
)
|
||||
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")
|
||||
with open(path, "wb") as f:
|
||||
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"))
|
||||
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")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P6\n3 1 " + maxval)
|
||||
|
@ -351,7 +355,7 @@ def test_mimetypes(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("buffer", (True, False))
|
||||
def test_save_stdout(buffer) -> None:
|
||||
def test_save_stdout(buffer: bool) -> None:
|
||||
old_stdout = sys.stdout
|
||||
|
||||
if buffer:
|
||||
|
|
|
@ -72,7 +72,7 @@ def test_invalid_file() -> None:
|
|||
|
||||
|
||||
def test_write(tmp_path: Path) -> None:
|
||||
def roundtrip(img) -> None:
|
||||
def roundtrip(img: Image.Image) -> None:
|
||||
out = str(tmp_path / "temp.sgi")
|
||||
img.save(out, format="sgi")
|
||||
assert_image_equal_tofile(img, out)
|
||||
|
|
|
@ -230,9 +230,7 @@ class TestImageGetPixel(AccessTest):
|
|||
assert im.getpixel([0, 0]) == (20, 20, 70)
|
||||
|
||||
@pytest.mark.parametrize("mode", ("I;16", "I;16B"))
|
||||
@pytest.mark.parametrize(
|
||||
"expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)
|
||||
)
|
||||
@pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1))
|
||||
def test_signedness(self, mode, expected_color) -> None:
|
||||
# see https://github.com/python-pillow/Pillow/issues/452
|
||||
# pixelaccess is using signed int* instead of uint*
|
||||
|
|
|
@ -8,7 +8,7 @@ from .helper import assert_image_equal, hopper
|
|||
|
||||
|
||||
@pytest.mark.parametrize("data_type", ("bytes", "memoryview"))
|
||||
def test_sanity(data_type) -> None:
|
||||
def test_sanity(data_type: str) -> None:
|
||||
im1 = hopper()
|
||||
|
||||
data = im1.tobytes()
|
||||
|
|
|
@ -26,7 +26,7 @@ def test_close() -> None:
|
|||
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.load()
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
|
|
|
@ -11,10 +11,9 @@ class TestImagingPaste:
|
|||
masks = {}
|
||||
size = 128
|
||||
|
||||
def assert_9points_image(self, im, expected) -> None:
|
||||
expected = [
|
||||
point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected
|
||||
]
|
||||
def assert_9points_image(
|
||||
self, im: Image.Image, expected: list[tuple[int, int, int, int]]
|
||||
) -> None:
|
||||
px = im.load()
|
||||
actual = [
|
||||
px[0, 0],
|
||||
|
@ -27,9 +26,17 @@ class TestImagingPaste:
|
|||
px[self.size // 2, 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.paste(im2, (0, 0), mask)
|
||||
self.assert_9points_image(im3, expected)
|
||||
|
@ -106,7 +113,7 @@ class TestImagingPaste:
|
|||
)
|
||||
|
||||
@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")
|
||||
im2 = getattr(self, "gradient_" + mode)
|
||||
|
||||
|
@ -116,7 +123,7 @@ class TestImagingPaste:
|
|||
assert_image_equal(im, im2)
|
||||
|
||||
@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")
|
||||
im2 = getattr(self, "gradient_" + mode)
|
||||
|
||||
|
@ -138,7 +145,7 @@ class TestImagingPaste:
|
|||
)
|
||||
|
||||
@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")
|
||||
im2 = getattr(self, "gradient_" + mode)
|
||||
|
||||
|
@ -160,7 +167,7 @@ class TestImagingPaste:
|
|||
)
|
||||
|
||||
@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")
|
||||
im2 = getattr(self, "gradient_" + mode)
|
||||
|
||||
|
@ -182,7 +189,7 @@ class TestImagingPaste:
|
|||
)
|
||||
|
||||
@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")
|
||||
im2 = getattr(self, "gradient_" + mode)
|
||||
|
||||
|
@ -204,7 +211,7 @@ class TestImagingPaste:
|
|||
)
|
||||
|
||||
@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")
|
||||
im2 = getattr(self, "gradient_" + mode)
|
||||
|
||||
|
@ -226,7 +233,7 @@ class TestImagingPaste:
|
|||
)
|
||||
|
||||
@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")
|
||||
|
||||
rect = (12, 23, 128 + 12, 128 + 23)
|
||||
|
@ -239,7 +246,7 @@ class TestImagingPaste:
|
|||
assert sum(head[:255]) == 0
|
||||
|
||||
@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)])
|
||||
color = (10, 20, 30, 40)[: len(mode)]
|
||||
|
||||
|
@ -261,7 +268,7 @@ class TestImagingPaste:
|
|||
)
|
||||
|
||||
@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()
|
||||
color = "white"
|
||||
|
||||
|
@ -283,7 +290,7 @@ class TestImagingPaste:
|
|||
)
|
||||
|
||||
@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()
|
||||
color = "white"
|
||||
|
||||
|
@ -305,7 +312,7 @@ class TestImagingPaste:
|
|||
)
|
||||
|
||||
@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()
|
||||
color = "white"
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ gradients_image.load()
|
|||
((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))
|
||||
assert expected == im.reduce(size).size
|
||||
|
||||
|
@ -56,7 +56,7 @@ def test_args_factor(size, expected) -> None:
|
|||
@pytest.mark.parametrize(
|
||||
"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))
|
||||
with pytest.raises(expected_error):
|
||||
im.reduce(size)
|
||||
|
@ -69,7 +69,7 @@ def test_args_factor_error(size, expected_error) -> None:
|
|||
((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))
|
||||
assert expected == im.reduce(2, size).size
|
||||
|
||||
|
@ -86,20 +86,20 @@ def test_args_box(size, expected) -> None:
|
|||
((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))
|
||||
with pytest.raises(expected_error):
|
||||
im.reduce(2, size).size
|
||||
|
||||
|
||||
@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))
|
||||
with pytest.raises(ValueError):
|
||||
im.reduce(3)
|
||||
|
||||
|
||||
def get_image(mode):
|
||||
def get_image(mode: str) -> Image.Image:
|
||||
mode_info = ImageMode.getmode(mode)
|
||||
if mode_info.basetype == "L":
|
||||
bands = [gradients_image]
|
||||
|
@ -119,7 +119,7 @@ def get_image(mode):
|
|||
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)
|
||||
reduced = im.reduce(factor, box=box)
|
||||
reference = im.crop(box).reduce(factor)
|
||||
|
@ -127,7 +127,10 @@ def compare_reduce_with_box(im, factor) -> None:
|
|||
|
||||
|
||||
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:
|
||||
"""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)
|
||||
|
||||
|
||||
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.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)
|
||||
def test_mode_L(factor) -> None:
|
||||
def test_mode_L(factor: int | tuple[int, int]) -> None:
|
||||
im = get_image("L")
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
|
||||
|
||||
@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")
|
||||
compare_reduce_with_reference(im, factor, 0.8, 5)
|
||||
|
||||
|
||||
@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")
|
||||
# With opaque alpha, an error should be way smaller.
|
||||
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)
|
||||
def test_mode_La(factor) -> None:
|
||||
def test_mode_La(factor: int | tuple[int, int]) -> None:
|
||||
im = get_image("La")
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
|
||||
|
||||
@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")
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
|
||||
|
||||
@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")
|
||||
compare_reduce_with_reference(im, factor, 0.8, 5)
|
||||
|
||||
|
||||
@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")
|
||||
# With opaque alpha, an error should be way smaller.
|
||||
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)
|
||||
def test_mode_RGBa(factor) -> None:
|
||||
def test_mode_RGBa(factor: int | tuple[int, int]) -> None:
|
||||
im = get_image("RGBa")
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
|
||||
|
||||
@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")
|
||||
compare_reduce_with_reference(im, factor)
|
||||
compare_reduce_with_box(im, factor)
|
||||
|
||||
|
||||
@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")
|
||||
compare_reduce_with_reference(im, factor, 0, 0)
|
||||
compare_reduce_with_box(im, factor)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -51,7 +52,7 @@ class TestImagingResampleVulnerability:
|
|||
|
||||
|
||||
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.
|
||||
For example:
|
||||
e0 e0 1f 1f
|
||||
|
@ -66,7 +67,7 @@ class TestImagingCoreResampleAccuracy:
|
|||
|
||||
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
|
||||
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
|
||||
return sample
|
||||
|
||||
def check_case(self, case, sample) -> None:
|
||||
def check_case(self, case: Image.Image, sample: Image.Image) -> None:
|
||||
s_px = sample.load()
|
||||
c_px = case.load()
|
||||
for y in range(case.size[1]):
|
||||
|
@ -95,7 +96,7 @@ class TestImagingCoreResampleAccuracy:
|
|||
)
|
||||
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()
|
||||
return "\n".join(
|
||||
" ".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"))
|
||||
def test_reduce_box(self, mode) -> None:
|
||||
def test_reduce_box(self, mode: str) -> None:
|
||||
case = self.make_case(mode, (8, 8), 0xE1)
|
||||
case = case.resize((4, 4), Image.Resampling.BOX)
|
||||
# fmt: off
|
||||
|
@ -114,7 +115,7 @@ class TestImagingCoreResampleAccuracy:
|
|||
self.check_case(channel, self.make_sample(data, (4, 4)))
|
||||
|
||||
@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 = case.resize((4, 4), Image.Resampling.BILINEAR)
|
||||
# fmt: off
|
||||
|
@ -125,7 +126,7 @@ class TestImagingCoreResampleAccuracy:
|
|||
self.check_case(channel, self.make_sample(data, (4, 4)))
|
||||
|
||||
@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 = case.resize((4, 4), Image.Resampling.HAMMING)
|
||||
# fmt: off
|
||||
|
@ -136,7 +137,7 @@ class TestImagingCoreResampleAccuracy:
|
|||
self.check_case(channel, self.make_sample(data, (4, 4)))
|
||||
|
||||
@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 = case.resize((6, 6), Image.Resampling.BICUBIC)
|
||||
# fmt: off
|
||||
|
@ -148,7 +149,7 @@ class TestImagingCoreResampleAccuracy:
|
|||
self.check_case(channel, self.make_sample(data, (6, 6)))
|
||||
|
||||
@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 = case.resize((8, 8), Image.Resampling.LANCZOS)
|
||||
# fmt: off
|
||||
|
@ -161,7 +162,7 @@ class TestImagingCoreResampleAccuracy:
|
|||
self.check_case(channel, self.make_sample(data, (8, 8)))
|
||||
|
||||
@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 = case.resize((4, 4), Image.Resampling.BOX)
|
||||
# fmt: off
|
||||
|
@ -172,7 +173,7 @@ class TestImagingCoreResampleAccuracy:
|
|||
self.check_case(channel, self.make_sample(data, (4, 4)))
|
||||
|
||||
@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 = case.resize((4, 4), Image.Resampling.BILINEAR)
|
||||
# fmt: off
|
||||
|
@ -183,7 +184,7 @@ class TestImagingCoreResampleAccuracy:
|
|||
self.check_case(channel, self.make_sample(data, (4, 4)))
|
||||
|
||||
@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 = case.resize((4, 4), Image.Resampling.HAMMING)
|
||||
# fmt: off
|
||||
|
@ -194,7 +195,7 @@ class TestImagingCoreResampleAccuracy:
|
|||
self.check_case(channel, self.make_sample(data, (4, 4)))
|
||||
|
||||
@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 = case.resize((8, 8), Image.Resampling.BICUBIC)
|
||||
# fmt: off
|
||||
|
@ -207,7 +208,7 @@ class TestImagingCoreResampleAccuracy:
|
|||
self.check_case(channel, self.make_sample(data, (8, 8)))
|
||||
|
||||
@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 = case.resize((12, 12), Image.Resampling.LANCZOS)
|
||||
data = (
|
||||
|
@ -230,7 +231,7 @@ class TestImagingCoreResampleAccuracy:
|
|||
|
||||
|
||||
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)
|
||||
return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0]
|
||||
|
||||
|
@ -265,7 +266,7 @@ class TestCoreResampleConsistency:
|
|||
|
||||
|
||||
class TestCoreResampleAlphaCorrect:
|
||||
def make_levels_case(self, mode):
|
||||
def make_levels_case(self, mode: str) -> Image.Image:
|
||||
i = Image.new(mode, (256, 16))
|
||||
px = i.load()
|
||||
for y in range(i.size[1]):
|
||||
|
@ -275,7 +276,7 @@ class TestCoreResampleAlphaCorrect:
|
|||
px[x, y] = tuple(pix)
|
||||
return i
|
||||
|
||||
def run_levels_case(self, i) -> None:
|
||||
def run_levels_case(self, i: Image.Image) -> None:
|
||||
px = i.load()
|
||||
for y in range(i.size[1]):
|
||||
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.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)
|
||||
px = i.load()
|
||||
xdiv4 = i.size[0] // 4
|
||||
|
@ -312,7 +315,7 @@ class TestCoreResampleAlphaCorrect:
|
|||
px[x + xdiv4, y + ydiv4] = clean_pixel
|
||||
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()
|
||||
for y in range(i.size[1]):
|
||||
for x in range(i.size[0]):
|
||||
|
@ -432,7 +435,7 @@ class TestCoreResampleBox:
|
|||
Image.Resampling.LANCZOS,
|
||||
),
|
||||
)
|
||||
def test_wrong_arguments(self, resample) -> None:
|
||||
def test_wrong_arguments(self, resample: Image.Resampling) -> None:
|
||||
im = hopper()
|
||||
im.resize((32, 32), resample, (0, 0, 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"):
|
||||
im.resize((32, 32), resample, (0, 0, im.width, im.height + 1))
|
||||
|
||||
def resize_tiled(self, im, dst_size, xtiles, ytiles):
|
||||
def split_range(size, tiles):
|
||||
def resize_tiled(
|
||||
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
|
||||
for i in range(tiles):
|
||||
yield int(round(scale * i)), int(round(scale * (i + 1)))
|
||||
|
@ -518,7 +525,7 @@ class TestCoreResampleBox:
|
|||
@pytest.mark.parametrize(
|
||||
"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)
|
||||
box = (20, 20, im.size[0] - 20, im.size[1] - 20)
|
||||
with_box = im.resize((32, 32), resample, box)
|
||||
|
@ -558,7 +565,7 @@ class TestCoreResampleBox:
|
|||
@pytest.mark.parametrize(
|
||||
"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
|
||||
im = hopper()
|
||||
|
||||
|
@ -581,7 +588,7 @@ class TestCoreResampleBox:
|
|||
@pytest.mark.parametrize(
|
||||
"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
|
||||
im = hopper()
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
Tests for resize functionality.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from itertools import permutations
|
||||
|
|
|
@ -342,9 +342,11 @@ def test_extended_information() -> None:
|
|||
|
||||
def truncate_tuple(tuple_or_float):
|
||||
return tuple(
|
||||
truncate_tuple(val)
|
||||
if isinstance(val, tuple)
|
||||
else int(val * power) / power
|
||||
(
|
||||
truncate_tuple(val)
|
||||
if isinstance(val, tuple)
|
||||
else int(val * power) / power
|
||||
)
|
||||
for val in tuple_or_float
|
||||
)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import os.path
|
|||
import pytest
|
||||
|
||||
from PIL import Image, ImageColor, ImageDraw, ImageFont, features
|
||||
from PIL._typing import Coords
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -74,7 +75,7 @@ def test_mode_mismatch() -> None:
|
|||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
@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
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -87,7 +88,7 @@ def test_arc(bbox, start, end) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_arc_end_le_start(bbox) -> None:
|
||||
def test_arc_end_le_start(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -102,7 +103,7 @@ def test_arc_end_le_start(bbox) -> None:
|
|||
|
||||
|
||||
@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
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
|
@ -118,7 +119,7 @@ def test_arc_no_loops(bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_arc_width(bbox) -> None:
|
||||
def test_arc_width(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -131,7 +132,7 @@ def test_arc_width(bbox) -> None:
|
|||
|
||||
|
||||
@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
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
|
@ -145,7 +146,7 @@ def test_arc_width_pieslice_large(bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_arc_width_fill(bbox) -> None:
|
||||
def test_arc_width_fill(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -158,7 +159,7 @@ def test_arc_width_fill(bbox) -> None:
|
|||
|
||||
|
||||
@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
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -200,7 +201,7 @@ def test_bitmap() -> None:
|
|||
|
||||
@pytest.mark.parametrize("mode", ("RGB", "L"))
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_chord(mode, bbox) -> None:
|
||||
def test_chord(mode: str, bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new(mode, (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -214,7 +215,7 @@ def test_chord(mode, bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_chord_width(bbox) -> None:
|
||||
def test_chord_width(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -227,7 +228,7 @@ def test_chord_width(bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_chord_width_fill(bbox) -> None:
|
||||
def test_chord_width_fill(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -240,7 +241,7 @@ def test_chord_width_fill(bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_chord_zero_width(bbox) -> None:
|
||||
def test_chord_zero_width(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -266,7 +267,7 @@ def test_chord_too_fat() -> None:
|
|||
|
||||
@pytest.mark.parametrize("mode", ("RGB", "L"))
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_ellipse(mode, bbox) -> None:
|
||||
def test_ellipse(mode: str, bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new(mode, (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -280,7 +281,7 @@ def test_ellipse(mode, bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_ellipse_translucent(bbox) -> None:
|
||||
def test_ellipse_translucent(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im, "RGBA")
|
||||
|
@ -317,7 +318,7 @@ def test_ellipse_symmetric() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_ellipse_width(bbox) -> None:
|
||||
def test_ellipse_width(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -342,7 +343,7 @@ def test_ellipse_width_large() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_ellipse_width_fill(bbox) -> None:
|
||||
def test_ellipse_width_fill(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -355,7 +356,7 @@ def test_ellipse_width_fill(bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_ellipse_zero_width(bbox) -> None:
|
||||
def test_ellipse_zero_width(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
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")
|
||||
|
||||
|
||||
def ellipse_various_sizes_helper(filled):
|
||||
def ellipse_various_sizes_helper(filled: bool) -> Image.Image:
|
||||
ellipse_sizes = range(32)
|
||||
image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1
|
||||
im = Image.new("RGB", (image_size, image_size))
|
||||
|
@ -409,7 +410,7 @@ def test_ellipse_various_sizes_filled() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("points", POINTS)
|
||||
def test_line(points) -> None:
|
||||
def test_line(points: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -482,7 +483,7 @@ def test_transform() -> None:
|
|||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
@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
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -495,7 +496,7 @@ def test_pieslice(bbox, start, end) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_pieslice_width(bbox) -> None:
|
||||
def test_pieslice_width(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -508,7 +509,7 @@ def test_pieslice_width(bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_pieslice_width_fill(bbox) -> None:
|
||||
def test_pieslice_width_fill(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -522,7 +523,7 @@ def test_pieslice_width_fill(bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_pieslice_zero_width(bbox) -> None:
|
||||
def test_pieslice_zero_width(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -577,7 +578,7 @@ def test_pieslice_no_spikes() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("points", POINTS)
|
||||
def test_point(points) -> None:
|
||||
def test_point(points: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -602,7 +603,7 @@ def test_point_I16() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("points", POINTS)
|
||||
def test_polygon(points) -> None:
|
||||
def test_polygon(points: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -616,7 +617,9 @@ def test_polygon(points) -> None:
|
|||
|
||||
@pytest.mark.parametrize("mode", ("RGB", "L"))
|
||||
@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
|
||||
# vertical (dx==0) and horizontal (dy==0) lines
|
||||
# Arrange
|
||||
|
@ -673,7 +676,7 @@ def test_polygon_translucent() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_rectangle(bbox) -> None:
|
||||
def test_rectangle(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -700,7 +703,7 @@ def test_big_rectangle() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_rectangle_width(bbox) -> None:
|
||||
def test_rectangle_width(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -714,7 +717,7 @@ def test_rectangle_width(bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_rectangle_width_fill(bbox) -> None:
|
||||
def test_rectangle_width_fill(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -728,7 +731,7 @@ def test_rectangle_width_fill(bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_rectangle_zero_width(bbox) -> None:
|
||||
def test_rectangle_zero_width(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -741,7 +744,7 @@ def test_rectangle_zero_width(bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_rectangle_I16(bbox) -> None:
|
||||
def test_rectangle_I16(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("I;16", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -754,7 +757,7 @@ def test_rectangle_I16(bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_rectangle_translucent_outline(bbox) -> None:
|
||||
def test_rectangle_translucent_outline(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im, "RGBA")
|
||||
|
@ -772,7 +775,13 @@ def test_rectangle_translucent_outline(bbox) -> None:
|
|||
"xy",
|
||||
[(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
|
||||
im = Image.new("RGB", (200, 200))
|
||||
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_left", (True, False))
|
||||
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:
|
||||
corners = (top_left, top_right, bottom_right, bottom_left)
|
||||
|
||||
|
@ -824,7 +833,9 @@ def test_rounded_rectangle_corners(
|
|||
((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
|
||||
im = Image.new("RGB", (200, 200))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -840,7 +851,7 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_rounded_rectangle_zero_radius(bbox) -> None:
|
||||
def test_rounded_rectangle_zero_radius(bbox: Coords) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
@ -862,7 +873,9 @@ def test_rounded_rectangle_zero_radius(bbox) -> None:
|
|||
((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
|
||||
im = Image.new("RGB", (W, H))
|
||||
draw = ImageDraw.Draw(im, "RGBA")
|
||||
|
@ -879,7 +892,7 @@ def test_rounded_rectangle_translucent(xy, suffix) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_floodfill(bbox) -> None:
|
||||
def test_floodfill(bbox: Coords) -> None:
|
||||
red = ImageColor.getrgb("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)
|
||||
def test_floodfill_border(bbox) -> None:
|
||||
def test_floodfill_border(bbox: Coords) -> None:
|
||||
# floodfill() is experimental
|
||||
|
||||
# Arrange
|
||||
|
@ -934,7 +947,7 @@ def test_floodfill_border(bbox) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_floodfill_thresh(bbox) -> None:
|
||||
def test_floodfill_thresh(bbox: Coords) -> None:
|
||||
# floodfill() is experimental
|
||||
|
||||
# Arrange
|
||||
|
@ -968,8 +981,11 @@ def test_floodfill_not_negative() -> None:
|
|||
|
||||
|
||||
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)
|
||||
for x in range(0, size[0]):
|
||||
for y in range(0, size[1]):
|
||||
|
@ -1003,7 +1019,7 @@ def test_triangle_right() -> None:
|
|||
"fill, suffix",
|
||||
((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))
|
||||
draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5)
|
||||
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))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
||||
|
@ -1388,7 +1404,7 @@ def test_default_font_size() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("bbox", BBOX)
|
||||
def test_same_color_outline(bbox) -> None:
|
||||
def test_same_color_outline(bbox: Coords) -> None:
|
||||
# Prepare shape
|
||||
x0, y0 = 5, 5
|
||||
x1, y1 = 5, 50
|
||||
|
@ -1402,7 +1418,8 @@ def test_same_color_outline(bbox) -> None:
|
|||
|
||||
# Begin
|
||||
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 {
|
||||
"chord": [bbox, 0, 180],
|
||||
"ellipse": [bbox],
|
||||
|
@ -1417,6 +1434,7 @@ def test_same_color_outline(bbox) -> None:
|
|||
|
||||
# Act
|
||||
draw_method = getattr(draw, operation)
|
||||
assert isinstance(args, list)
|
||||
args += [fill, outline]
|
||||
draw_method(*args)
|
||||
|
||||
|
@ -1434,7 +1452,9 @@ def test_same_color_outline(bbox) -> None:
|
|||
(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))
|
||||
filename = f"Tests/images/imagedraw_{polygon_name}.png"
|
||||
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)
|
||||
vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0)
|
||||
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"),
|
||||
(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,
|
||||
(50, 50, 100, 100),
|
||||
|
@ -1569,7 +1591,7 @@ def test_polygon2() -> None:
|
|||
|
||||
|
||||
@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))
|
||||
draw = ImageDraw.Draw(im)
|
||||
with pytest.raises(ValueError):
|
||||
|
|
|
@ -119,3 +119,15 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
|
|||
subprocess.call(["wl-copy"], stdin=fp)
|
||||
im = ImageGrab.grabclipboard()
|
||||
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
|
||||
|
|
|
@ -5,12 +5,12 @@ import pytest
|
|||
from PIL import Image, ImageMath
|
||||
|
||||
|
||||
def pixel(im):
|
||||
if hasattr(im, "im"):
|
||||
return f"{im.mode} {repr(im.getpixel((0, 0)))}"
|
||||
def pixel(im: Image.Image | int) -> str | int:
|
||||
if isinstance(im, int):
|
||||
return int(im) # hack to deal with booleans
|
||||
|
||||
return f"{im.mode} {repr(im.getpixel((0, 0)))}"
|
||||
|
||||
|
||||
A = Image.new("L", (1, 1), 1)
|
||||
B = Image.new("L", (1, 1), 2)
|
||||
|
@ -60,7 +60,7 @@ def test_ops() -> None:
|
|||
"(lambda: (lambda: exec('pass'))())()",
|
||||
),
|
||||
)
|
||||
def test_prevent_exec(expression) -> None:
|
||||
def test_prevent_exec(expression: str) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
ImageMath.eval(expression)
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ def test_kw() -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("mode", TK_MODES)
|
||||
def test_photoimage(mode) -> None:
|
||||
def test_photoimage(mode: str) -> None:
|
||||
# test as image:
|
||||
im = hopper(mode)
|
||||
|
||||
|
@ -79,7 +79,7 @@ def test_photoimage_apply_transparency() -> None:
|
|||
|
||||
|
||||
@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:
|
||||
im_tk = ImageTk.PhotoImage(mode, (100, 100))
|
||||
|
||||
|
|
|
@ -10,7 +10,13 @@ X = 255
|
|||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
@ -228,7 +234,13 @@ class TestLibPack:
|
|||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
|
|
@ -11,7 +11,7 @@ from .helper import hopper
|
|||
original = hopper().resize((32, 32)).convert("I")
|
||||
|
||||
|
||||
def verify(im1) -> None:
|
||||
def verify(im1: Image.Image) -> None:
|
||||
im2 = original.copy()
|
||||
assert im1.size == im2.size
|
||||
pix1 = im1.load()
|
||||
|
@ -27,7 +27,7 @@ def verify(im1) -> None:
|
|||
|
||||
|
||||
@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
|
||||
# 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 tobytes(mode):
|
||||
def tobytes(mode: str) -> Image.Image:
|
||||
return Image.new(mode, (1, 1), 1).tobytes()
|
||||
|
||||
order = 1 if Image._ENDIAN == "<" else -1
|
||||
|
|
|
@ -9,6 +9,7 @@ The contents of this file are hereby released in the public domain (CC0)
|
|||
Full text of the CC0 license:
|
||||
https://creativecommons.org/publicdomain/zero/1.0/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
|
|
@ -96,7 +96,7 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable"
|
|||
test-command = "cd {project} && .github/workflows/wheels-test.sh"
|
||||
test-extras = "tests"
|
||||
|
||||
[tool.ruff]
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"C4", # flake8-comprehensions
|
||||
"E", # pycodestyle errors
|
||||
|
@ -104,25 +104,25 @@ select = [
|
|||
"F", # pyflakes errors
|
||||
"I", # isort
|
||||
"ISC", # flake8-implicit-str-concat
|
||||
"LOG", # flake8-logging
|
||||
"PGH", # pygrep-hooks
|
||||
"RUF100", # unused noqa (yesqa)
|
||||
"UP", # pyupgrade
|
||||
"W", # pycodestyle warnings
|
||||
"YTT", # flake8-2020
|
||||
# "LOG", # TODO: enable flake8-logging when it's not in preview anymore
|
||||
]
|
||||
extend-ignore = [
|
||||
ignore = [
|
||||
"E203", # Whitespace before ':'
|
||||
"E221", # Multiple spaces before operator
|
||||
"E226", # Missing whitespace around arithmetic operator
|
||||
"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_pillow.py" = ["I002"]
|
||||
|
||||
[tool.ruff.isort]
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["PIL"]
|
||||
required-imports = ["from __future__ import annotations"]
|
||||
|
||||
|
@ -141,16 +141,6 @@ warn_redundant_casts = true
|
|||
warn_unreachable = true
|
||||
warn_unused_ignores = true
|
||||
exclude = [
|
||||
'^src/PIL/_tkinter_finder.py$',
|
||||
'^src/PIL/DdsImagePlugin.py$',
|
||||
'^src/PIL/FpxImagePlugin.py$',
|
||||
'^src/PIL/Image.py$',
|
||||
'^src/PIL/ImageQt.py$',
|
||||
'^src/PIL/ImImagePlugin.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$',
|
||||
]
|
||||
|
|
|
@ -28,6 +28,7 @@ BLP files come in many different flavours:
|
|||
- DXT3 compression is used if alpha_encoding == 1.
|
||||
- DXT5 compression is used if alpha_encoding == 7.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
|
|
@ -9,6 +9,7 @@ The contents of this file are hereby released in the public domain (CC0)
|
|||
Full text of the CC0 license:
|
||||
https://creativecommons.org/publicdomain/zero/1.0/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
|
@ -269,13 +270,17 @@ class D3DFMT(IntEnum):
|
|||
# Backward compatibility layer
|
||||
module = sys.modules[__name__]
|
||||
for item in DDSD:
|
||||
assert item.name is not None
|
||||
setattr(module, "DDSD_" + item.name, item.value)
|
||||
for item in DDSCAPS:
|
||||
setattr(module, "DDSCAPS_" + item.name, item.value)
|
||||
for item in DDSCAPS2:
|
||||
setattr(module, "DDSCAPS2_" + item.name, item.value)
|
||||
for item in DDPF:
|
||||
setattr(module, "DDPF_" + item.name, item.value)
|
||||
for item1 in DDSCAPS:
|
||||
assert item1.name is not None
|
||||
setattr(module, "DDSCAPS_" + item1.name, item1.value)
|
||||
for item2 in DDSCAPS2:
|
||||
assert item2.name is not None
|
||||
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_RGB = DDPF.RGB
|
||||
|
|
|
@ -50,9 +50,7 @@ class FontFile:
|
|||
| None
|
||||
] = [None] * 256
|
||||
|
||||
def __getitem__(
|
||||
self, ix: int
|
||||
) -> (
|
||||
def __getitem__(self, ix: int) -> (
|
||||
tuple[
|
||||
tuple[int, int],
|
||||
tuple[int, int, int, int],
|
||||
|
|
|
@ -50,6 +50,7 @@ bytes for that mipmap level.
|
|||
|
||||
Note: All data is stored in little-Endian (Intel) byte order.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
|
|
|
@ -641,9 +641,9 @@ def _write_multiple_frames(im, fp, palette):
|
|||
if encoderinfo.get("optimize") and im_frame.mode != "1":
|
||||
if "transparency" not in encoderinfo:
|
||||
try:
|
||||
encoderinfo[
|
||||
"transparency"
|
||||
] = im_frame.palette._new_color_index(im_frame)
|
||||
encoderinfo["transparency"] = (
|
||||
im_frame.palette._new_color_index(im_frame)
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
if "transparency" in encoderinfo:
|
||||
|
|
|
@ -93,8 +93,8 @@ for i in ["16", "16L", "16B"]:
|
|||
for i in ["32S"]:
|
||||
OPEN[f"L {i} image"] = ("I", f"I;{i}")
|
||||
OPEN[f"L*{i} image"] = ("I", f"I;{i}")
|
||||
for i in range(2, 33):
|
||||
OPEN[f"L*{i} image"] = ("F", f"F;{i}")
|
||||
for j in range(2, 33):
|
||||
OPEN[f"L*{j} image"] = ("F", f"F;{j}")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import atexit
|
||||
import builtins
|
||||
import io
|
||||
|
@ -40,11 +41,8 @@ import warnings
|
|||
from collections.abc import Callable, MutableMapping
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from defusedxml import ElementTree
|
||||
except ImportError:
|
||||
ElementTree = None
|
||||
from types import ModuleType
|
||||
from typing import IO, TYPE_CHECKING, Any
|
||||
|
||||
# VERSION was removed in Pillow 6.0.0.
|
||||
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
||||
|
@ -60,6 +58,12 @@ from . import (
|
|||
from ._binary import i32le, o32be, o32le
|
||||
from ._util import DeferredError, is_path
|
||||
|
||||
ElementTree: ModuleType | None
|
||||
try:
|
||||
from defusedxml import ElementTree
|
||||
except ImportError:
|
||||
ElementTree = None
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -110,6 +114,7 @@ except ImportError as v:
|
|||
|
||||
|
||||
USE_CFFI_ACCESS = False
|
||||
cffi: ModuleType | None
|
||||
try:
|
||||
import cffi
|
||||
except ImportError:
|
||||
|
@ -211,14 +216,22 @@ if hasattr(core, "DEFAULT_STRATEGY"):
|
|||
# --------------------------------------------------------------------
|
||||
# Registries
|
||||
|
||||
ID = []
|
||||
OPEN = {}
|
||||
MIME = {}
|
||||
SAVE = {}
|
||||
SAVE_ALL = {}
|
||||
EXTENSION = {}
|
||||
DECODERS = {}
|
||||
ENCODERS = {}
|
||||
if TYPE_CHECKING:
|
||||
from . import ImageFile
|
||||
ID: list[str] = []
|
||||
OPEN: dict[
|
||||
str,
|
||||
tuple[
|
||||
Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
|
||||
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
|
||||
|
@ -571,7 +584,7 @@ class Image:
|
|||
# object is gone.
|
||||
self.im = DeferredError(ValueError("Operation on closed image"))
|
||||
|
||||
def _copy(self):
|
||||
def _copy(self) -> None:
|
||||
self.load()
|
||||
self.im = self.im.copy()
|
||||
self.pyaccess = None
|
||||
|
@ -2383,12 +2396,12 @@ class Image:
|
|||
may have been created, and may contain partial data.
|
||||
"""
|
||||
|
||||
filename = ""
|
||||
filename: str | bytes = ""
|
||||
open_fp = False
|
||||
if isinstance(fp, Path):
|
||||
filename = str(fp)
|
||||
open_fp = True
|
||||
elif is_path(fp):
|
||||
elif isinstance(fp, (str, bytes)):
|
||||
filename = fp
|
||||
open_fp = True
|
||||
elif fp == sys.stdout:
|
||||
|
@ -2398,14 +2411,15 @@ class Image:
|
|||
pass
|
||||
if not filename and hasattr(fp, "name") and is_path(fp.name):
|
||||
# only set the name for metadata purposes
|
||||
filename = fp.name
|
||||
filename = os.path.realpath(os.fspath(fp.name))
|
||||
|
||||
# may mutate self!
|
||||
self._ensure_mutable()
|
||||
|
||||
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 ext not in EXTENSION:
|
||||
|
@ -2514,7 +2528,7 @@ class Image:
|
|||
elif 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
|
||||
beyond the end of the sequence, the method raises an
|
||||
|
@ -2574,10 +2588,8 @@ class Image:
|
|||
|
||||
self.load()
|
||||
if self.im.bands == 1:
|
||||
ims = [self.copy()]
|
||||
else:
|
||||
ims = map(self._new, self.im.split())
|
||||
return tuple(ims)
|
||||
return (self.copy(),)
|
||||
return tuple(map(self._new, self.im.split()))
|
||||
|
||||
def getchannel(self, channel):
|
||||
"""
|
||||
|
@ -2934,7 +2946,14 @@ class ImageTransformHandler:
|
|||
(for use with :py:meth:`~PIL.Image.Image.transform`)
|
||||
"""
|
||||
|
||||
pass
|
||||
@abc.abstractmethod
|
||||
def transform(
|
||||
self,
|
||||
size: tuple[int, int],
|
||||
image: Image,
|
||||
**options: dict[str, str | int | tuple[int, ...] | list[int]],
|
||||
) -> Image:
|
||||
pass
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
@ -3306,11 +3325,9 @@ def open(fp, mode="r", formats=None) -> Image:
|
|||
raise TypeError(msg)
|
||||
|
||||
exclusive_fp = False
|
||||
filename = ""
|
||||
if isinstance(fp, Path):
|
||||
filename = str(fp.resolve())
|
||||
elif is_path(fp):
|
||||
filename = fp
|
||||
filename: str | bytes = ""
|
||||
if is_path(fp):
|
||||
filename = os.path.realpath(os.fspath(fp))
|
||||
|
||||
if filename:
|
||||
fp = builtins.open(filename, "rb")
|
||||
|
@ -3484,7 +3501,11 @@ def merge(mode, bands):
|
|||
# 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
|
||||
in application code.
|
||||
|
@ -3694,7 +3715,13 @@ _apply_env_variables()
|
|||
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::
|
||||
|
||||
|
|
|
@ -281,7 +281,6 @@ class ImageCmsProfile:
|
|||
|
||||
|
||||
class ImageCmsTransform(Image.ImagePointHandler):
|
||||
|
||||
"""
|
||||
Transform. This can be used with the procedural API, or with the standard
|
||||
:py:func:`~PIL.Image.Image.point` method.
|
||||
|
@ -369,7 +368,6 @@ def get_display_profile(handle=None):
|
|||
|
||||
|
||||
class PyCMSError(Exception):
|
||||
|
||||
"""(pyCMS) Exception class.
|
||||
This is used for all errors in the pyCMS API."""
|
||||
|
||||
|
|
|
@ -34,8 +34,10 @@ from __future__ import annotations
|
|||
import math
|
||||
import numbers
|
||||
import struct
|
||||
from typing import Sequence, cast
|
||||
|
||||
from . import Image, ImageColor
|
||||
from ._typing import Coords
|
||||
|
||||
"""
|
||||
A simple 2D drawing interface for PIL images.
|
||||
|
@ -48,7 +50,7 @@ directly.
|
|||
class ImageDraw:
|
||||
font = None
|
||||
|
||||
def __init__(self, im, mode=None):
|
||||
def __init__(self, im: Image.Image, mode: str | None = None) -> None:
|
||||
"""
|
||||
Create a drawing instance.
|
||||
|
||||
|
@ -115,7 +117,7 @@ class ImageDraw:
|
|||
self.font = ImageFont.load_default()
|
||||
return self.font
|
||||
|
||||
def _getfont(self, font_size):
|
||||
def _getfont(self, font_size: float | None):
|
||||
if font_size is not None:
|
||||
from . import ImageFont
|
||||
|
||||
|
@ -124,7 +126,7 @@ class ImageDraw:
|
|||
font = self.getfont()
|
||||
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 self.fill:
|
||||
fill = self.ink
|
||||
|
@ -145,13 +147,13 @@ class ImageDraw:
|
|||
fill = self.draw.draw_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."""
|
||||
ink, fill = self._getink(fill)
|
||||
if ink is not None:
|
||||
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."""
|
||||
bitmap.load()
|
||||
ink, fill = self._getink(fill)
|
||||
|
@ -160,7 +162,7 @@ class ImageDraw:
|
|||
if ink is not None:
|
||||
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."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
|
@ -168,7 +170,7 @@ class ImageDraw:
|
|||
if ink is not None and ink != fill and width != 0:
|
||||
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."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
|
@ -176,20 +178,29 @@ class ImageDraw:
|
|||
if ink is not None and ink != fill and width != 0:
|
||||
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."""
|
||||
ink = self._getink(fill)[0]
|
||||
if ink is not None:
|
||||
self.draw.draw_lines(xy, ink, width)
|
||||
if joint == "curve" and width > 4:
|
||||
if not isinstance(xy[0], (list, tuple)):
|
||||
xy = [tuple(xy[i : i + 2]) for i in range(0, len(xy), 2)]
|
||||
for i in range(1, len(xy) - 1):
|
||||
point = xy[i]
|
||||
points: Sequence[Sequence[float]]
|
||||
if isinstance(xy[0], (list, tuple)):
|
||||
points = cast(Sequence[Sequence[float]], xy)
|
||||
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 = [
|
||||
math.degrees(math.atan2(end[0] - start[0], start[1] - end[1]))
|
||||
% 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]:
|
||||
# This is a straight line, so no joint is required
|
||||
|
@ -236,7 +247,7 @@ class ImageDraw:
|
|||
]
|
||||
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."""
|
||||
shape.close()
|
||||
ink, fill = self._getink(outline, fill)
|
||||
|
@ -245,7 +256,9 @@ class ImageDraw:
|
|||
if ink is not None and ink != fill:
|
||||
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."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
|
@ -253,13 +266,13 @@ class ImageDraw:
|
|||
if ink is not None and ink != fill and width != 0:
|
||||
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."""
|
||||
ink, fill = self._getink(fill)
|
||||
if ink is not None:
|
||||
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."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
|
@ -267,7 +280,7 @@ class ImageDraw:
|
|||
if ink is not None and ink != fill and width != 0:
|
||||
if width == 1:
|
||||
self.draw.draw_polygon(xy, ink, 0, width)
|
||||
else:
|
||||
elif self.im is not None:
|
||||
# To avoid expanding the polygon outwards,
|
||||
# use the fill as a mask
|
||||
mask = Image.new("1", self.im.size)
|
||||
|
@ -291,12 +304,12 @@ class ImageDraw:
|
|||
|
||||
def regular_polygon(
|
||||
self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1
|
||||
):
|
||||
) -> None:
|
||||
"""Draw a regular polygon."""
|
||||
xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
|
||||
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."""
|
||||
ink, fill = self._getink(outline, fill)
|
||||
if fill is not None:
|
||||
|
@ -305,13 +318,13 @@ class ImageDraw:
|
|||
self.draw.draw_rectangle(xy, ink, 0, width)
|
||||
|
||||
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."""
|
||||
if isinstance(xy[0], (list, tuple)):
|
||||
(x0, y0), (x1, y1) = xy
|
||||
(x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy)
|
||||
else:
|
||||
x0, y0, x1, y1 = xy
|
||||
x0, y0, x1, y1 = cast(Sequence[float], xy)
|
||||
if x1 < x0:
|
||||
msg = "x1 must be greater than or equal to x0"
|
||||
raise ValueError(msg)
|
||||
|
@ -346,7 +359,8 @@ class ImageDraw:
|
|||
r = d // 2
|
||||
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:
|
||||
# Draw top and bottom halves
|
||||
parts = (
|
||||
|
@ -361,17 +375,18 @@ class ImageDraw:
|
|||
)
|
||||
else:
|
||||
# Draw four separate corners
|
||||
parts = []
|
||||
for i, part in enumerate(
|
||||
(
|
||||
((x0, y0, x0 + d, y0 + d), 180, 270),
|
||||
((x1 - d, y0, x1, y0 + d), 270, 360),
|
||||
((x1 - d, y1 - d, x1, y1), 0, 90),
|
||||
((x0, y1 - d, x0 + d, y1), 90, 180),
|
||||
parts = tuple(
|
||||
part
|
||||
for i, part in enumerate(
|
||||
(
|
||||
((x0, y0, x0 + d, y0 + d), 180, 270),
|
||||
((x1 - d, y0, x1, y0 + d), 270, 360),
|
||||
((x1 - d, y1 - d, x1, y1), 0, 90),
|
||||
((x0, y1 - d, x0 + d, y1), 90, 180),
|
||||
)
|
||||
)
|
||||
):
|
||||
if corners[i]:
|
||||
parts.append(part)
|
||||
if corners[i]
|
||||
)
|
||||
for part in parts:
|
||||
if pieslice:
|
||||
self.draw.draw_pieslice(*(part + (fill, 1)))
|
||||
|
@ -431,12 +446,12 @@ class ImageDraw:
|
|||
right[3] -= r + 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"
|
||||
|
||||
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"
|
||||
|
||||
return text.split(split_character)
|
||||
|
@ -465,7 +480,7 @@ class ImageDraw:
|
|||
embedded_color=False,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
) -> None:
|
||||
"""Draw text."""
|
||||
if embedded_color and self.mode not in ("RGB", "RGBA"):
|
||||
msg = "Embedded color supported only in RGB and RGBA modes"
|
||||
|
@ -497,7 +512,7 @@ class ImageDraw:
|
|||
return fill
|
||||
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
|
||||
if stroke_width == 0 and embedded_color:
|
||||
mode = "RGBA"
|
||||
|
@ -520,7 +535,7 @@ class ImageDraw:
|
|||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
coord = coord[0] + offset[0], coord[1] + offset[1]
|
||||
coord = [coord[0] + offset[0], coord[1] + offset[1]]
|
||||
except AttributeError:
|
||||
try:
|
||||
mask = font.getmask(
|
||||
|
@ -539,7 +554,7 @@ class ImageDraw:
|
|||
except TypeError:
|
||||
mask = font.getmask(text)
|
||||
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":
|
||||
# font.getmask2(mode="RGBA") returns color in RGB bands and mask in A
|
||||
# extract mask and set text alpha
|
||||
|
@ -547,7 +562,10 @@ class ImageDraw:
|
|||
ink_alpha = struct.pack("i", ink)[3]
|
||||
color.fillband(3, ink_alpha)
|
||||
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:
|
||||
self.draw.draw_bitmap(coord, mask, ink)
|
||||
|
||||
|
@ -584,7 +602,7 @@ class ImageDraw:
|
|||
embedded_color=False,
|
||||
*,
|
||||
font_size=None,
|
||||
):
|
||||
) -> None:
|
||||
if direction == "ttb":
|
||||
msg = "ttb direction is unsupported for multiline text"
|
||||
raise ValueError(msg)
|
||||
|
@ -693,7 +711,7 @@ class ImageDraw:
|
|||
embedded_color=False,
|
||||
*,
|
||||
font_size=None,
|
||||
):
|
||||
) -> tuple[int, int, int, int]:
|
||||
"""Get the bounding box of a given string, in pixels."""
|
||||
if embedded_color and self.mode not in ("RGB", "RGBA"):
|
||||
msg = "Embedded color supported only in RGB and RGBA modes"
|
||||
|
@ -738,7 +756,7 @@ class ImageDraw:
|
|||
embedded_color=False,
|
||||
*,
|
||||
font_size=None,
|
||||
):
|
||||
) -> tuple[int, int, int, int]:
|
||||
if direction == "ttb":
|
||||
msg = "ttb direction is unsupported for multiline text"
|
||||
raise ValueError(msg)
|
||||
|
@ -777,7 +795,7 @@ class ImageDraw:
|
|||
elif anchor[1] == "d":
|
||||
top -= (len(lines) - 1) * line_spacing
|
||||
|
||||
bbox = None
|
||||
bbox: tuple[int, int, int, int] | None = None
|
||||
|
||||
for idx, line in enumerate(lines):
|
||||
left = xy[0]
|
||||
|
@ -828,7 +846,7 @@ class ImageDraw:
|
|||
return bbox
|
||||
|
||||
|
||||
def Draw(im, mode=None):
|
||||
def Draw(im, mode: str | None = None) -> ImageDraw:
|
||||
"""
|
||||
A simple 2D drawing interface for PIL images.
|
||||
|
||||
|
@ -876,7 +894,7 @@ def getdraw(im=None, hints=None):
|
|||
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.
|
||||
|
||||
|
@ -932,7 +950,9 @@ def floodfill(image, xy, value, border=None, thresh=0):
|
|||
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.
|
||||
|
||||
|
@ -982,7 +1002,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
|
|||
|
||||
# 1.2 Check `bounding_circle` has an appropriate value
|
||||
if not isinstance(bounding_circle, (list, tuple)):
|
||||
msg = "bounding_circle should be a tuple"
|
||||
msg = "bounding_circle should be a sequence"
|
||||
raise TypeError(msg)
|
||||
|
||||
if len(bounding_circle) == 3:
|
||||
|
@ -1014,7 +1034,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation):
|
|||
raise ValueError(msg)
|
||||
|
||||
# 2. Define Helper Functions
|
||||
def _apply_rotation(point, degrees, centroid):
|
||||
def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]:
|
||||
return (
|
||||
round(
|
||||
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]
|
||||
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 = []
|
||||
degrees = 360 / n_sides
|
||||
# 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)
|
||||
|
||||
# 4. Compute Vertices
|
||||
return [
|
||||
_compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles
|
||||
]
|
||||
return [_compute_polygon_vertex(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.
|
||||
"""
|
||||
|
|
|
@ -91,7 +91,7 @@ def _tilesort(t):
|
|||
|
||||
|
||||
class _Tile(NamedTuple):
|
||||
encoder_name: str
|
||||
codec_name: str
|
||||
extents: tuple[int, int, int, int]
|
||||
offset: int
|
||||
args: tuple[Any, ...] | str | None
|
||||
|
|
|
@ -872,7 +872,7 @@ def load_path(filename):
|
|||
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,
|
||||
https://dotcolon.net/font/aileron, with a more limited character set.
|
||||
|
||||
|
|
|
@ -149,18 +149,7 @@ def grabclipboard():
|
|||
session_type = None
|
||||
|
||||
if shutil.which("wl-paste") and session_type in ("wayland", None):
|
||||
output = subprocess.check_output(["wl-paste", "-l"]).decode()
|
||||
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])
|
||||
args = ["wl-paste", "-t", "image"]
|
||||
elif shutil.which("xclip") and session_type in ("x11", None):
|
||||
args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]
|
||||
else:
|
||||
|
@ -168,10 +157,29 @@ def grabclipboard():
|
|||
raise NotImplementedError(msg)
|
||||
|
||||
p = subprocess.run(args, capture_output=True)
|
||||
err = p.stderr
|
||||
if err:
|
||||
msg = f"{args[0]} error: {err.strip().decode()}"
|
||||
if p.returncode != 0:
|
||||
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:
|
||||
msg += f": {err.strip().decode()}"
|
||||
raise ChildProcessError(msg)
|
||||
|
||||
data = io.BytesIO(p.stdout)
|
||||
im = Image.open(data)
|
||||
im.load()
|
||||
|
|
|
@ -19,19 +19,26 @@ from __future__ import annotations
|
|||
|
||||
import sys
|
||||
from io import BytesIO
|
||||
from typing import Callable
|
||||
|
||||
from . import Image
|
||||
from ._util import is_path
|
||||
|
||||
qt_version: str | None
|
||||
qt_versions = [
|
||||
["6", "PyQt6"],
|
||||
["side6", "PySide6"],
|
||||
]
|
||||
|
||||
# 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)
|
||||
for qt_version, qt_module in qt_versions:
|
||||
qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True)
|
||||
for version, qt_module in qt_versions:
|
||||
try:
|
||||
QBuffer: type
|
||||
QIODevice: type
|
||||
QImage: type
|
||||
QPixmap: type
|
||||
qRgba: Callable[[int, int, int, int], int]
|
||||
if qt_module == "PyQt6":
|
||||
from PyQt6.QtCore import QBuffer, QIODevice
|
||||
from PyQt6.QtGui import QImage, QPixmap, qRgba
|
||||
|
@ -41,6 +48,7 @@ for qt_version, qt_module in qt_versions:
|
|||
except (ImportError, RuntimeError):
|
||||
continue
|
||||
qt_is_installed = True
|
||||
qt_version = version
|
||||
break
|
||||
else:
|
||||
qt_is_installed = False
|
||||
|
|
|
@ -184,7 +184,7 @@ class UnixViewer(Viewer):
|
|||
|
||||
@abc.abstractmethod
|
||||
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:
|
||||
command = self.get_command_ex(file, **options)[0]
|
||||
|
|
|
@ -62,6 +62,7 @@ Libjpeg ref.:
|
|||
https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# fmt: off
|
||||
|
|
|
@ -188,9 +188,9 @@ def _save(im, fp, filename, save_all=False):
|
|||
x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0)
|
||||
|
||||
info = {
|
||||
"title": None
|
||||
if is_appending
|
||||
else os.path.splitext(os.path.basename(filename))[0],
|
||||
"title": (
|
||||
None if is_appending else os.path.splitext(os.path.basename(filename))[0]
|
||||
),
|
||||
"author": None,
|
||||
"subject": None,
|
||||
"keywords": None,
|
||||
|
|
|
@ -8,6 +8,7 @@ import os
|
|||
import re
|
||||
import time
|
||||
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
|
||||
|
@ -239,12 +240,18 @@ class PdfName:
|
|||
return bytes(result)
|
||||
|
||||
|
||||
class PdfArray(list):
|
||||
class PdfArray(List[Any]):
|
||||
def __bytes__(self):
|
||||
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):
|
||||
if key == "data":
|
||||
collections.UserDict.__setattr__(self, key, value)
|
||||
|
|
|
@ -25,6 +25,7 @@ import sys
|
|||
|
||||
from ._deprecate import deprecate
|
||||
|
||||
FFI: type
|
||||
try:
|
||||
from cffi import FFI
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ import warnings
|
|||
from collections.abc import MutableMapping
|
||||
from fractions import Fraction
|
||||
from numbers import Number, Rational
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
|
||||
from ._binary import i16be as i16
|
||||
|
@ -306,6 +307,13 @@ _load_dispatch = {}
|
|||
_write_dispatch = {}
|
||||
|
||||
|
||||
def _delegate(op):
|
||||
def delegate(self, *args):
|
||||
return getattr(self._val, op)(*args)
|
||||
|
||||
return delegate
|
||||
|
||||
|
||||
class IFDRational(Rational):
|
||||
"""Implements a rational class where 0/0 is a legal value to match
|
||||
the in the wild use of exif rationals.
|
||||
|
@ -391,12 +399,6 @@ class IFDRational(Rational):
|
|||
self._numerator = _numerator
|
||||
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',
|
||||
'truediv', 'rtruediv', 'floordiv', 'rfloordiv',
|
||||
'mod','rmod', 'pow','rpow', 'pos', 'neg',
|
||||
|
@ -436,7 +438,50 @@ class IFDRational(Rational):
|
|||
__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
|
||||
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):
|
||||
"""Initialize an ImageFileDirectory.
|
||||
|
||||
|
@ -531,7 +579,10 @@ class ImageFileDirectory_v2(MutableMapping):
|
|||
|
||||
prefix = property(lambda self: self._prefix)
|
||||
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
|
||||
def legacy_api(self, value):
|
||||
|
@ -674,40 +725,6 @@ class ImageFileDirectory_v2(MutableMapping):
|
|||
def _pack(self, 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(
|
||||
map(
|
||||
_register_basic,
|
||||
|
@ -995,7 +1012,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
|
|||
tagdata = property(lambda self: self._tagdata)
|
||||
|
||||
# defined in ImageFileDirectory_v2
|
||||
tagtype: dict
|
||||
tagtype: dict[int, int]
|
||||
"""Dictionary of tag types"""
|
||||
|
||||
@classmethod
|
||||
|
@ -1835,11 +1852,11 @@ def _save(im, fp, filename):
|
|||
tags = list(atts.items())
|
||||
tags.sort()
|
||||
a = (rawmode, compression, _fp, filename, tags, types)
|
||||
e = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
|
||||
e.setimage(im.im, (0, 0) + im.size)
|
||||
encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
|
||||
encoder.setimage(im.im, (0, 0) + im.size)
|
||||
while True:
|
||||
# undone, change to self.decodermaxblock:
|
||||
errcode, data = e.encode(16 * 1024)[1:]
|
||||
errcode, data = encoder.encode(16 * 1024)[1:]
|
||||
if not _fp:
|
||||
fp.write(data)
|
||||
if errcode:
|
||||
|
|
|
@ -22,7 +22,7 @@ from collections import namedtuple
|
|||
|
||||
|
||||
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):
|
||||
return super().__new__(cls, value, name, type, length, enum or {})
|
||||
|
@ -437,7 +437,7 @@ _populate()
|
|||
##
|
||||
# Map type numbers to type names -- defined in ImageFileDirectory.
|
||||
|
||||
TYPES = {}
|
||||
TYPES: dict[int, str] = {}
|
||||
|
||||
#
|
||||
# These tags are handled by default in libtiff, without
|
||||
|
|
|
@ -12,6 +12,7 @@ Use PIL.__version__ for this Pillow version.
|
|||
|
||||
;-)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import _version
|
||||
|
|
5
src/PIL/_imaging.pyi
Normal file
5
src/PIL/_imaging.pyi
Normal file
|
@ -0,0 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
def __getattr__(name: str) -> Any: ...
|
|
@ -1,10 +1,12 @@
|
|||
""" Find compiled module linking to Tcl / Tk libraries
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tkinter
|
||||
from tkinter import _tkinter as tk
|
||||
|
||||
tk = getattr(tkinter, "_tkinter")
|
||||
|
||||
try:
|
||||
if hasattr(sys, "pypy_find_executable"):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Sequence, Union
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeGuard
|
||||
|
@ -15,4 +16,7 @@ else:
|
|||
return bool
|
||||
|
||||
|
||||
Coords = Union[Sequence[float], Sequence[Sequence[float]]]
|
||||
|
||||
|
||||
__all__ = ["TypeGuard"]
|
||||
|
|
5
src/PIL/_webp.pyi
Normal file
5
src/PIL/_webp.pyi
Normal file
|
@ -0,0 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
def __getattr__(name: str) -> Any: ...
|
Loading…
Reference in New Issue
Block a user