Merge branch 'main' into webp-get-next-without-gil

This commit is contained in:
Andrew Murray 2024-02-13 21:33:15 +11:00 committed by GitHub
commit 8acacffb70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 740 additions and 515 deletions

View File

@ -10,6 +10,11 @@ exclude_also =
if DEBUG: if DEBUG:
# Don't complain about compatibility code for missing optional dependencies # Don't complain about compatibility code for missing optional dependencies
except ImportError except ImportError
if TYPE_CHECKING:
@abc.abstractmethod
# Empty bodies in protocols or abstract methods
^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$
^\s*\.\.\.(\s*#.*)?$
[run] [run]
omit = omit =

View File

@ -244,7 +244,7 @@ def fromstring(data: bytes) -> Image.Image:
return Image.open(BytesIO(data)) return Image.open(BytesIO(data))
def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes: def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes:
out = BytesIO() out = BytesIO()
im.save(out, string_format, **options) im.save(out, string_format, **options)
return out.getvalue() return out.getvalue()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -162,8 +162,6 @@ class TestImage:
pass pass
def test_pathlib(self, tmp_path: Path) -> None: def test_pathlib(self, tmp_path: Path) -> None:
from PIL.Image import Path
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im:
assert im.mode == "P" assert im.mode == "P"
assert im.size == (10, 10) assert im.size == (10, 10)

View File

@ -4,6 +4,7 @@ import os
import subprocess import subprocess
import sys import sys
import sysconfig import sysconfig
from types import ModuleType
import pytest import pytest
@ -23,6 +24,7 @@ else:
except ImportError: except ImportError:
cffi = None cffi = None
numpy: ModuleType | None
try: try:
import numpy import numpy
except ImportError: except ImportError:
@ -71,9 +73,10 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load() pix1 = im1.load()
pix2 = im2.load() pix2 = im2.load()
for x, y in ((0, "0"), ("0", 0)): with pytest.raises(TypeError):
with pytest.raises(TypeError): pix1[0, "0"]
pix1[x, y] with pytest.raises(TypeError):
pix1["0", 0]
for y in range(im1.size[1]): for y in range(im1.size[1]):
for x in range(im1.size[0]): for x in range(im1.size[0]):
@ -123,12 +126,13 @@ class TestImagePutPixel(AccessTest):
im = hopper() im = hopper()
pix = im.load() pix = im.load()
assert numpy is not None
assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
class TestImageGetPixel(AccessTest): class TestImageGetPixel(AccessTest):
@staticmethod @staticmethod
def color(mode): def color(mode: str) -> int | tuple[int, ...]:
bands = Image.getmodebands(mode) bands = Image.getmodebands(mode)
if bands == 1: if bands == 1:
return 1 return 1
@ -138,12 +142,13 @@ class TestImageGetPixel(AccessTest):
return (16, 32, 49) return (16, 32, 49)
return tuple(range(1, bands + 1)) return tuple(range(1, bands + 1))
def check(self, mode, expected_color=None) -> None: def check(self, mode: str, expected_color_int: int | None = None) -> None:
if self._need_cffi_access and mode.startswith("BGR;"): if self._need_cffi_access and mode.startswith("BGR;"):
pytest.skip("Support not added to deprecated module for BGR;* modes") pytest.skip("Support not added to deprecated module for BGR;* modes")
if not expected_color: expected_color = (
expected_color = self.color(mode) self.color(mode) if expected_color_int is None else expected_color_int
)
# check putpixel # check putpixel
im = Image.new(mode, (1, 1), None) im = Image.new(mode, (1, 1), None)
@ -222,7 +227,7 @@ class TestImageGetPixel(AccessTest):
"YCbCr", "YCbCr",
), ),
) )
def test_basic(self, mode) -> None: def test_basic(self, mode: str) -> None:
self.check(mode) self.check(mode)
def test_list(self) -> None: def test_list(self) -> None:
@ -231,14 +236,14 @@ class TestImageGetPixel(AccessTest):
@pytest.mark.parametrize("mode", ("I;16", "I;16B")) @pytest.mark.parametrize("mode", ("I;16", "I;16B"))
@pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1))
def test_signedness(self, mode, expected_color) -> None: def test_signedness(self, mode: str, expected_color: int) -> None:
# see https://github.com/python-pillow/Pillow/issues/452 # see https://github.com/python-pillow/Pillow/issues/452
# pixelaccess is using signed int* instead of uint* # pixelaccess is using signed int* instead of uint*
self.check(mode, expected_color) self.check(mode, expected_color)
@pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("mode", ("P", "PA"))
@pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
def test_p_putpixel_rgb_rgba(self, mode, color) -> None: def test_p_putpixel_rgb_rgba(self, mode: str, color: tuple[int, ...]) -> None:
im = Image.new(mode, (1, 1)) im = Image.new(mode, (1, 1))
im.putpixel((0, 0), color) im.putpixel((0, 0), color)
@ -262,7 +267,7 @@ class TestCffiGetPixel(TestImageGetPixel):
class TestCffi(AccessTest): class TestCffi(AccessTest):
_need_cffi_access = True _need_cffi_access = True
def _test_get_access(self, im) -> None: def _test_get_access(self, im: Image.Image) -> None:
"""Do we get the same thing as the old pixel access """Do we get the same thing as the old pixel access
Using private interfaces, forcing a capi access and Using private interfaces, forcing a capi access and
@ -299,7 +304,7 @@ class TestCffi(AccessTest):
# im = Image.new('I;32B', (10, 10), 2**10) # im = Image.new('I;32B', (10, 10), 2**10)
# self._test_get_access(im) # self._test_get_access(im)
def _test_set_access(self, im, color) -> None: def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None:
"""Are we writing the correct bits into the image? """Are we writing the correct bits into the image?
Using private interfaces, forcing a capi access and Using private interfaces, forcing a capi access and
@ -359,7 +364,7 @@ class TestCffi(AccessTest):
assert px[i, 0] == 0 assert px[i, 0] == 0
@pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("mode", ("P", "PA"))
def test_p_putpixel_rgb_rgba(self, mode) -> None: def test_p_putpixel_rgb_rgba(self, mode: str) -> None:
for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
im = Image.new(mode, (1, 1)) im = Image.new(mode, (1, 1))
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
@ -377,7 +382,7 @@ class TestImagePutPixelError(AccessTest):
INVALID_TYPES = ["foo", 1.0, None] INVALID_TYPES = ["foo", 1.0, None]
@pytest.mark.parametrize("mode", IMAGE_MODES1) @pytest.mark.parametrize("mode", IMAGE_MODES1)
def test_putpixel_type_error1(self, mode) -> None: def test_putpixel_type_error1(self, mode: str) -> None:
im = hopper(mode) im = hopper(mode)
for v in self.INVALID_TYPES: for v in self.INVALID_TYPES:
with pytest.raises(TypeError, match="color must be int or tuple"): with pytest.raises(TypeError, match="color must be int or tuple"):
@ -400,14 +405,16 @@ class TestImagePutPixelError(AccessTest):
), ),
), ),
) )
def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match) -> None: def test_putpixel_invalid_number_of_bands(
self, mode: str, band_numbers: tuple[int, ...], match: str
) -> None:
im = hopper(mode) im = hopper(mode)
for band_number in band_numbers: for band_number in band_numbers:
with pytest.raises(TypeError, match=match): with pytest.raises(TypeError, match=match):
im.putpixel((0, 0), (0,) * band_number) im.putpixel((0, 0), (0,) * band_number)
@pytest.mark.parametrize("mode", IMAGE_MODES2) @pytest.mark.parametrize("mode", IMAGE_MODES2)
def test_putpixel_type_error2(self, mode) -> None: def test_putpixel_type_error2(self, mode: str) -> None:
im = hopper(mode) im = hopper(mode)
for v in self.INVALID_TYPES: for v in self.INVALID_TYPES:
with pytest.raises( with pytest.raises(
@ -416,7 +423,7 @@ class TestImagePutPixelError(AccessTest):
im.putpixel((0, 0), v) im.putpixel((0, 0), v)
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
def test_putpixel_overflow_error(self, mode) -> None: def test_putpixel_overflow_error(self, mode: str) -> None:
im = hopper(mode) im = hopper(mode)
with pytest.raises(OverflowError): with pytest.raises(OverflowError):
im.putpixel((0, 0), 2**80) im.putpixel((0, 0), 2**80)
@ -428,7 +435,7 @@ class TestEmbeddable:
def test_embeddable(self) -> None: def test_embeddable(self) -> None:
import ctypes import ctypes
from setuptools.command.build_ext import new_compiler from setuptools.command import build_ext
with open("embed_pil.c", "w", encoding="utf-8") as fh: with open("embed_pil.c", "w", encoding="utf-8") as fh:
fh.write( fh.write(
@ -457,7 +464,7 @@ int main(int argc, char* argv[])
% sys.prefix.replace("\\", "\\\\") % sys.prefix.replace("\\", "\\\\")
) )
compiler = new_compiler() compiler = getattr(build_ext, "new_compiler")()
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
@ -471,7 +478,7 @@ int main(int argc, char* argv[])
env["PATH"] = sys.prefix + ";" + env["PATH"] env["PATH"] = sys.prefix + ";" + env["PATH"]
# do not display the Windows Error Reporting dialog # do not display the Windows Error Reporting dialog
ctypes.windll.kernel32.SetErrorMode(0x0002) getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002)
process = subprocess.Popen(["embed_pil.exe"], env=env) process = subprocess.Popen(["embed_pil.exe"], env=env)
process.communicate() process.communicate()

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
@ -13,7 +15,7 @@ im = hopper().resize((128, 100))
def test_toarray() -> None: def test_toarray() -> None:
def test(mode): def test(mode: str) -> tuple[tuple[int, ...], str, int]:
ai = numpy.array(im.convert(mode)) ai = numpy.array(im.convert(mode))
return ai.shape, ai.dtype.str, ai.nbytes return ai.shape, ai.dtype.str, ai.nbytes
@ -50,14 +52,14 @@ def test_fromarray() -> None:
class Wrapper: class Wrapper:
"""Class with API matching Image.fromarray""" """Class with API matching Image.fromarray"""
def __init__(self, img, arr_params) -> None: def __init__(self, img: Image.Image, arr_params: dict[str, Any]) -> None:
self.img = img self.img = img
self.__array_interface__ = arr_params self.__array_interface__ = arr_params
def tobytes(self): def tobytes(self) -> bytes:
return self.img.tobytes() return self.img.tobytes()
def test(mode): def test(mode: str) -> tuple[str, tuple[int, int], bool]:
i = im.convert(mode) i = im.convert(mode)
a = numpy.array(i) a = numpy.array(i)
# Make wrapper instance for image, new array interface # Make wrapper instance for image, new array interface

View File

@ -7,7 +7,12 @@ from .helper import fromstring, skip_unless_feature, tostring
pytestmark = skip_unless_feature("jpg") pytestmark = skip_unless_feature("jpg")
def draft_roundtrip(in_mode, in_size, req_mode, req_size): def draft_roundtrip(
in_mode: str,
in_size: tuple[int, int],
req_mode: str | None,
req_size: tuple[int, int] | None,
) -> Image.Image:
im = Image.new(in_mode, in_size) im = Image.new(in_mode, in_size)
data = tostring(im, "JPEG") data = tostring(im, "JPEG")
im = fromstring(data) im = fromstring(data)

View File

@ -4,7 +4,7 @@ from .helper import hopper
def test_entropy() -> None: def test_entropy() -> None:
def entropy(mode): def entropy(mode: str) -> float:
return hopper(mode).entropy() return hopper(mode).entropy()
assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0 assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0

View File

@ -36,7 +36,7 @@ from .helper import assert_image_equal, hopper
), ),
) )
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
def test_sanity(filter_to_apply, mode) -> None: def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None:
im = hopper(mode) im = hopper(mode)
if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter):
out = im.filter(filter_to_apply) out = im.filter(filter_to_apply)
@ -45,7 +45,7 @@ def test_sanity(filter_to_apply, mode) -> None:
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
def test_sanity_error(mode) -> None: def test_sanity_error(mode: str) -> None:
with pytest.raises(TypeError): with pytest.raises(TypeError):
im = hopper(mode) im = hopper(mode)
im.filter("hello") im.filter("hello")
@ -53,7 +53,7 @@ def test_sanity_error(mode) -> None:
# crashes on small images # crashes on small images
@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) @pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3)))
def test_crash(size) -> None: def test_crash(size: tuple[int, int]) -> None:
im = Image.new("RGB", size) im = Image.new("RGB", size)
im.filter(ImageFilter.SMOOTH) im.filter(ImageFilter.SMOOTH)
@ -67,7 +67,10 @@ def test_crash(size) -> None:
("RGB", ((4, 0, 0), (0, 0, 0))), ("RGB", ((4, 0, 0), (0, 0, 0))),
), ),
) )
def test_modefilter(mode, expected) -> None: def test_modefilter(
mode: str,
expected: tuple[int, int] | tuple[tuple[int, int, int], tuple[int, int, int]],
) -> None:
im = Image.new(mode, (3, 3), None) im = Image.new(mode, (3, 3), None)
im.putdata(list(range(9))) im.putdata(list(range(9)))
# image is: # image is:
@ -90,7 +93,13 @@ def test_modefilter(mode, expected) -> None:
("F", (0.0, 4.0, 8.0)), ("F", (0.0, 4.0, 8.0)),
), ),
) )
def test_rankfilter(mode, expected) -> None: def test_rankfilter(
mode: str,
expected: (
tuple[float, float, float]
| tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]]
),
) -> None:
im = Image.new(mode, (3, 3), None) im = Image.new(mode, (3, 3), None)
im.putdata(list(range(9))) im.putdata(list(range(9)))
# image is: # image is:
@ -106,7 +115,7 @@ def test_rankfilter(mode, expected) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter)
) )
def test_rankfilter_error(filter) -> None: def test_rankfilter_error(filter: ImageFilter.RankFilter) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im = Image.new("P", (3, 3), None) im = Image.new("P", (3, 3), None)
im.putdata(list(range(9))) im.putdata(list(range(9)))
@ -137,7 +146,7 @@ def test_kernel_not_enough_coefficients() -> None:
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
def test_consistency_3x3(mode) -> None: def test_consistency_3x3(mode: str) -> None:
with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper.bmp") as source:
reference_name = "hopper_emboss" reference_name = "hopper_emboss"
reference_name += "_I.png" if mode == "I" else ".bmp" reference_name += "_I.png" if mode == "I" else ".bmp"
@ -163,7 +172,7 @@ def test_consistency_3x3(mode) -> None:
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
def test_consistency_5x5(mode) -> None: def test_consistency_5x5(mode: str) -> None:
with Image.open("Tests/images/hopper.bmp") as source: with Image.open("Tests/images/hopper.bmp") as source:
reference_name = "hopper_emboss_more" reference_name = "hopper_emboss_more"
reference_name += "_I.png" if mode == "I" else ".bmp" reference_name += "_I.png" if mode == "I" else ".bmp"
@ -199,7 +208,7 @@ def test_consistency_5x5(mode) -> None:
(2, -2), (2, -2),
), ),
) )
def test_invalid_box_blur_filter(radius) -> None: def test_invalid_box_blur_filter(radius: int | tuple[int, int]) -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFilter.BoxBlur(radius) ImageFilter.BoxBlur(radius)

View File

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

View File

@ -6,7 +6,7 @@ from .helper import hopper
def test_extrema() -> None: def test_extrema() -> None:
def extrema(mode): def extrema(mode: str) -> tuple[int, int] | tuple[tuple[int, int], ...]:
return hopper(mode).getextrema() return hopper(mode).getextrema()
assert extrema("1") == (0, 255) assert extrema("1") == (0, 255)

View File

@ -6,7 +6,7 @@ from .helper import hopper
def test_palette() -> None: def test_palette() -> None:
def palette(mode): def palette(mode: str) -> list[int] | None:
p = hopper(mode).getpalette() p = hopper(mode).getpalette()
if p: if p:
return p[:10] return p[:10]

View File

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

View File

@ -11,10 +11,9 @@ class TestImagingPaste:
masks = {} masks = {}
size = 128 size = 128
def assert_9points_image(self, im, expected) -> None: def assert_9points_image(
expected = [ self, im: Image.Image, expected: list[tuple[int, int, int, int]]
point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected ) -> None:
]
px = im.load() px = im.load()
actual = [ actual = [
px[0, 0], px[0, 0],
@ -27,9 +26,17 @@ class TestImagingPaste:
px[self.size // 2, self.size - 1], px[self.size // 2, self.size - 1],
px[self.size - 1, self.size - 1], px[self.size - 1, self.size - 1],
] ]
assert actual == expected assert actual == [
point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected
]
def assert_9points_paste(self, im, im2, mask, expected) -> None: def assert_9points_paste(
self,
im: Image.Image,
im2: Image.Image,
mask: Image.Image,
expected: list[tuple[int, int, int, int]],
) -> None:
im3 = im.copy() im3 = im.copy()
im3.paste(im2, (0, 0), mask) im3.paste(im2, (0, 0), mask)
self.assert_9points_image(im3, expected) self.assert_9points_image(im3, expected)
@ -39,7 +46,7 @@ class TestImagingPaste:
self.assert_9points_image(im, expected) self.assert_9points_image(im, expected)
@CachedProperty @CachedProperty
def mask_1(self): def mask_1(self) -> Image.Image:
mask = Image.new("1", (self.size, self.size)) mask = Image.new("1", (self.size, self.size))
px = mask.load() px = mask.load()
for y in range(mask.height): for y in range(mask.height):
@ -48,11 +55,11 @@ class TestImagingPaste:
return mask return mask
@CachedProperty @CachedProperty
def mask_L(self): def mask_L(self) -> Image.Image:
return self.gradient_L.transpose(Image.Transpose.ROTATE_270) return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
@CachedProperty @CachedProperty
def gradient_L(self): def gradient_L(self) -> Image.Image:
gradient = Image.new("L", (self.size, self.size)) gradient = Image.new("L", (self.size, self.size))
px = gradient.load() px = gradient.load()
for y in range(gradient.height): for y in range(gradient.height):
@ -61,7 +68,7 @@ class TestImagingPaste:
return gradient return gradient
@CachedProperty @CachedProperty
def gradient_RGB(self): def gradient_RGB(self) -> Image.Image:
return Image.merge( return Image.merge(
"RGB", "RGB",
[ [
@ -72,7 +79,7 @@ class TestImagingPaste:
) )
@CachedProperty @CachedProperty
def gradient_LA(self): def gradient_LA(self) -> Image.Image:
return Image.merge( return Image.merge(
"LA", "LA",
[ [
@ -82,7 +89,7 @@ class TestImagingPaste:
) )
@CachedProperty @CachedProperty
def gradient_RGBA(self): def gradient_RGBA(self) -> Image.Image:
return Image.merge( return Image.merge(
"RGBA", "RGBA",
[ [
@ -94,7 +101,7 @@ class TestImagingPaste:
) )
@CachedProperty @CachedProperty
def gradient_RGBa(self): def gradient_RGBa(self) -> Image.Image:
return Image.merge( return Image.merge(
"RGBa", "RGBa",
[ [
@ -106,7 +113,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_solid(self, mode) -> None: def test_image_solid(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "red") im = Image.new(mode, (200, 200), "red")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -116,7 +123,7 @@ class TestImagingPaste:
assert_image_equal(im, im2) assert_image_equal(im, im2)
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_mask_1(self, mode) -> None: def test_image_mask_1(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -138,7 +145,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_mask_L(self, mode) -> None: def test_image_mask_L(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -160,7 +167,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_mask_LA(self, mode) -> None: def test_image_mask_LA(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -182,7 +189,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_mask_RGBA(self, mode) -> None: def test_image_mask_RGBA(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -204,7 +211,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_image_mask_RGBa(self, mode) -> None: def test_image_mask_RGBa(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "white") im = Image.new(mode, (200, 200), "white")
im2 = getattr(self, "gradient_" + mode) im2 = getattr(self, "gradient_" + mode)
@ -226,7 +233,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_color_solid(self, mode) -> None: def test_color_solid(self, mode: str) -> None:
im = Image.new(mode, (200, 200), "black") im = Image.new(mode, (200, 200), "black")
rect = (12, 23, 128 + 12, 128 + 23) rect = (12, 23, 128 + 12, 128 + 23)
@ -239,7 +246,7 @@ class TestImagingPaste:
assert sum(head[:255]) == 0 assert sum(head[:255]) == 0
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_color_mask_1(self, mode) -> None: def test_color_mask_1(self, mode: str) -> None:
im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)])
color = (10, 20, 30, 40)[: len(mode)] color = (10, 20, 30, 40)[: len(mode)]
@ -261,7 +268,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_color_mask_L(self, mode) -> None: def test_color_mask_L(self, mode: str) -> None:
im = getattr(self, "gradient_" + mode).copy() im = getattr(self, "gradient_" + mode).copy()
color = "white" color = "white"
@ -283,7 +290,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_color_mask_RGBA(self, mode) -> None: def test_color_mask_RGBA(self, mode: str) -> None:
im = getattr(self, "gradient_" + mode).copy() im = getattr(self, "gradient_" + mode).copy()
color = "white" color = "white"
@ -305,7 +312,7 @@ class TestImagingPaste:
) )
@pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"])
def test_color_mask_RGBa(self, mode) -> None: def test_color_mask_RGBa(self, mode: str) -> None:
im = getattr(self, "gradient_" + mode).copy() im = getattr(self, "gradient_" + mode).copy()
color = "white" color = "white"

View File

@ -31,7 +31,7 @@ def test_sanity() -> None:
def test_long_integers() -> None: def test_long_integers() -> None:
# see bug-200802-systemerror # see bug-200802-systemerror
def put(value): def put(value: int) -> tuple[int, int, int, int]:
im = Image.new("RGBA", (1, 1)) im = Image.new("RGBA", (1, 1))
im.putdata([value]) im.putdata([value])
return im.getpixel((0, 0)) return im.getpixel((0, 0))
@ -58,7 +58,7 @@ def test_mode_with_L_with_float() -> None:
@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) @pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B"))
def test_mode_i(mode) -> None: def test_mode_i(mode: str) -> None:
src = hopper("L") src = hopper("L")
data = list(src.getdata()) data = list(src.getdata())
im = Image.new(mode, src.size, 0) im = Image.new(mode, src.size, 0)
@ -79,7 +79,7 @@ def test_mode_F() -> None:
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
def test_mode_BGR(mode) -> None: def test_mode_BGR(mode: str) -> None:
data = [(16, 32, 49), (32, 32, 98)] data = [(16, 32, 49), (32, 32, 98)]
im = Image.new(mode, (1, 2)) im = Image.new(mode, (1, 2))
im.putdata(data) im.putdata(data)

View File

@ -8,7 +8,7 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper
def test_putpalette() -> None: def test_putpalette() -> None:
def palette(mode): def palette(mode: str) -> str | tuple[str, list[int]]:
im = hopper(mode).copy() im = hopper(mode).copy()
im.putpalette(list(range(256)) * 3) im.putpalette(list(range(256)) * 3)
p = im.getpalette() p = im.getpalette()
@ -81,7 +81,7 @@ def test_putpalette_with_alpha_values() -> None:
("RGBAX", (1, 2, 3, 4, 0)), ("RGBAX", (1, 2, 3, 4, 0)),
), ),
) )
def test_rgba_palette(mode, palette) -> None: def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
im = Image.new("P", (1, 1)) im = Image.new("P", (1, 1))
im.putpalette(palette, mode) im.putpalette(palette, mode)
assert im.getpalette() == [1, 2, 3] assert im.getpalette() == [1, 2, 3]

View File

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

View File

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

View File

@ -12,7 +12,13 @@ from .helper import (
) )
def rotate(im, mode, angle, center=None, translate=None) -> None: def rotate(
im: Image.Image,
mode: str,
angle: int,
center: tuple[int, int] | None = None,
translate: tuple[int, int] | None = None,
) -> None:
out = im.rotate(angle, center=center, translate=translate) out = im.rotate(angle, center=center, translate=translate)
assert out.mode == mode assert out.mode == mode
assert out.size == im.size # default rotate clips output assert out.size == im.size # default rotate clips output
@ -27,13 +33,13 @@ def rotate(im, mode, angle, center=None, translate=None) -> None:
@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
def test_mode(mode) -> None: def test_mode(mode: str) -> None:
im = hopper(mode) im = hopper(mode)
rotate(im, mode, 45) rotate(im, mode, 45)
@pytest.mark.parametrize("angle", (0, 90, 180, 270)) @pytest.mark.parametrize("angle", (0, 90, 180, 270))
def test_angle(angle) -> None: def test_angle(angle: int) -> None:
with Image.open("Tests/images/test-card.png") as im: with Image.open("Tests/images/test-card.png") as im:
rotate(im, im.mode, angle) rotate(im, im.mode, angle)
@ -42,7 +48,7 @@ def test_angle(angle) -> None:
@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270))
def test_zero(angle) -> None: def test_zero(angle: int) -> None:
im = Image.new("RGB", (0, 0)) im = Image.new("RGB", (0, 0))
rotate(im, im.mode, angle) rotate(im, im.mode, angle)

View File

@ -111,7 +111,7 @@ def test_load_first_unless_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
draft = im.draft draft = im.draft
def im_draft(mode, size): def im_draft(mode: str, size: tuple[int, int]):
result = draft(mode, size) result = draft(mode, size)
assert result is not None assert result is not None

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import math import math
from typing import Callable
import pytest import pytest
@ -91,7 +92,7 @@ class TestImageTransform:
("LA", (76, 0)), ("LA", (76, 0)),
), ),
) )
def test_fill(self, mode, expected_pixel) -> None: def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None:
im = hopper(mode) im = hopper(mode)
(w, h) = im.size (w, h) = im.size
transformed = im.transform( transformed = im.transform(
@ -142,7 +143,9 @@ class TestImageTransform:
assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2))) assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2)))
assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h))) assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h)))
def _test_alpha_premult(self, op) -> None: def _test_alpha_premult(
self, op: Callable[[Image.Image, tuple[int, int]], Image.Image]
) -> None:
# create image with half white, half black, # create image with half white, half black,
# with the black half transparent. # with the black half transparent.
# do op, # do op,
@ -159,13 +162,13 @@ class TestImageTransform:
assert 40 * 10 == hist[-1] assert 40 * 10 == hist[-1]
def test_alpha_premult_resize(self) -> None: def test_alpha_premult_resize(self) -> None:
def op(im, sz): def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
return im.resize(sz, Image.Resampling.BILINEAR) return im.resize(sz, Image.Resampling.BILINEAR)
self._test_alpha_premult(op) self._test_alpha_premult(op)
def test_alpha_premult_transform(self) -> None: def test_alpha_premult_transform(self) -> None:
def op(im, sz): def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
(w, h) = im.size (w, h) = im.size
return im.transform( return im.transform(
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR
@ -173,7 +176,9 @@ class TestImageTransform:
self._test_alpha_premult(op) self._test_alpha_premult(op)
def _test_nearest(self, op, mode) -> None: def _test_nearest(
self, op: Callable[[Image.Image, tuple[int, int]], Image.Image], mode: str
) -> None:
# create white image with half transparent, # create white image with half transparent,
# do op, # do op,
# the image should remain white with half transparent # the image should remain white with half transparent
@ -196,15 +201,15 @@ class TestImageTransform:
) )
@pytest.mark.parametrize("mode", ("RGBA", "LA")) @pytest.mark.parametrize("mode", ("RGBA", "LA"))
def test_nearest_resize(self, mode) -> None: def test_nearest_resize(self, mode: str) -> None:
def op(im, sz): def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
return im.resize(sz, Image.Resampling.NEAREST) return im.resize(sz, Image.Resampling.NEAREST)
self._test_nearest(op, mode) self._test_nearest(op, mode)
@pytest.mark.parametrize("mode", ("RGBA", "LA")) @pytest.mark.parametrize("mode", ("RGBA", "LA"))
def test_nearest_transform(self, mode) -> None: def test_nearest_transform(self, mode: str) -> None:
def op(im, sz): def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
(w, h) = im.size (w, h) = im.size
return im.transform( return im.transform(
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST
@ -227,7 +232,9 @@ class TestImageTransform:
# Running by default, but I'd totally understand not doing it in # Running by default, but I'd totally understand not doing it in
# the future # the future
pattern = [Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)] pattern: list[Image.Image] | None = [
Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)
]
# Yeah. Watch some JIT optimize this out. # Yeah. Watch some JIT optimize this out.
pattern = None # noqa: F841 pattern = None # noqa: F841
@ -240,7 +247,7 @@ class TestImageTransform:
im.transform((100, 100), None) im.transform((100, 100), None)
@pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown"))
def test_unknown_resampling_filter(self, resample) -> None: def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None:
with hopper() as im: with hopper() as im:
(w, h) = im.size (w, h) = im.size
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -250,7 +257,7 @@ class TestImageTransform:
class TestImageTransformAffine: class TestImageTransformAffine:
transform = Image.Transform.AFFINE transform = Image.Transform.AFFINE
def _test_image(self): def _test_image(self) -> Image.Image:
im = hopper("RGB") im = hopper("RGB")
return im.crop((10, 20, im.width - 10, im.height - 20)) return im.crop((10, 20, im.width - 10, im.height - 20))
@ -263,7 +270,7 @@ class TestImageTransformAffine:
(270, Image.Transpose.ROTATE_270), (270, Image.Transpose.ROTATE_270),
), ),
) )
def test_rotate(self, deg, transpose) -> None: def test_rotate(self, deg: int, transpose: Image.Transpose | None) -> None:
im = self._test_image() im = self._test_image()
angle = -math.radians(deg) angle = -math.radians(deg)
@ -313,7 +320,13 @@ class TestImageTransformAffine:
(Image.Resampling.BICUBIC, 1), (Image.Resampling.BICUBIC, 1),
), ),
) )
def test_resize(self, scale, epsilon_scale, resample, epsilon) -> None: def test_resize(
self,
scale: float,
epsilon_scale: float,
resample: Image.Resampling,
epsilon: int,
) -> None:
im = self._test_image() im = self._test_image()
size_up = int(round(im.width * scale)), int(round(im.height * scale)) size_up = int(round(im.width * scale)), int(round(im.height * scale))
@ -342,7 +355,14 @@ class TestImageTransformAffine:
(Image.Resampling.BICUBIC, 1), (Image.Resampling.BICUBIC, 1),
), ),
) )
def test_translate(self, x, y, epsilon_scale, resample, epsilon) -> None: def test_translate(
self,
x: float,
y: float,
epsilon_scale: float,
resample: Image.Resampling,
epsilon: float,
) -> None:
im = self._test_image() im = self._test_image()
size_up = int(round(im.width + x)), int(round(im.height + y)) size_up = int(round(im.width + x)), int(round(im.height + y))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,29 +1,16 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path, PurePath
import pytest import pytest
from PIL import _util from PIL import _util
def test_is_path() -> None: @pytest.mark.parametrize(
# Arrange "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")]
fp = "filename.ext" )
def test_is_path(test_path) -> None:
# Act
it_is = _util.is_path(fp)
# Assert
assert it_is
def test_path_obj_is_path() -> None:
# Arrange
from pathlib import Path
test_path = Path("filename.ext")
# Act # Act
it_is = _util.is_path(test_path) it_is = _util.is_path(test_path)

View File

@ -1,5 +1,5 @@
Internal Reference Docs Internal Reference
======================= ==================
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2

View File

@ -33,6 +33,14 @@ Internal Modules
Provides a convenient way to import type hints that are not available Provides a convenient way to import type hints that are not available
on some Python versions. on some Python versions.
.. py:class:: StrOrBytesPath
Typing alias.
.. py:class:: SupportsRead
An object that supports the read method.
.. py:data:: TypeGuard .. py:data:: TypeGuard
:value: typing.TypeGuard :value: typing.TypeGuard

View File

@ -3,7 +3,7 @@
File Handling in Pillow File Handling in Pillow
======================= =======================
When opening a file as an image, Pillow requires a filename, ``pathlib.Path`` When opening a file as an image, Pillow requires a filename, ``os.PathLike``
object, or a file-like object. Pillow uses the filename or ``Path`` to open a object, or a file-like object. Pillow uses the filename or ``Path`` to open a
file, so for the rest of this article, they will all be treated as a file-like file, so for the rest of this article, they will all be treated as a file-like
object. object.

View File

@ -141,16 +141,6 @@ warn_redundant_casts = true
warn_unreachable = true warn_unreachable = true
warn_unused_ignores = true warn_unused_ignores = true
exclude = [ exclude = [
'^src/PIL/_tkinter_finder.py$',
'^src/PIL/DdsImagePlugin.py$',
'^src/PIL/FpxImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$',
'^src/PIL/Image.py$',
'^src/PIL/ImageQt.py$',
'^src/PIL/ImImagePlugin.py$',
'^src/PIL/MicImagePlugin.py$', '^src/PIL/MicImagePlugin.py$',
'^src/PIL/PdfParser.py$',
'^src/PIL/PyAccess.py$',
'^src/PIL/TiffImagePlugin.py$',
'^src/PIL/TiffTags.py$',
'^src/PIL/WebPImagePlugin.py$',
] ]

View File

@ -270,13 +270,17 @@ class D3DFMT(IntEnum):
# Backward compatibility layer # Backward compatibility layer
module = sys.modules[__name__] module = sys.modules[__name__]
for item in DDSD: for item in DDSD:
assert item.name is not None
setattr(module, "DDSD_" + item.name, item.value) setattr(module, "DDSD_" + item.name, item.value)
for item in DDSCAPS: for item1 in DDSCAPS:
setattr(module, "DDSCAPS_" + item.name, item.value) assert item1.name is not None
for item in DDSCAPS2: setattr(module, "DDSCAPS_" + item1.name, item1.value)
setattr(module, "DDSCAPS2_" + item.name, item.value) for item2 in DDSCAPS2:
for item in DDPF: assert item2.name is not None
setattr(module, "DDPF_" + item.name, item.value) setattr(module, "DDSCAPS2_" + item2.name, item2.value)
for item3 in DDPF:
assert item3.name is not None
setattr(module, "DDPF_" + item3.name, item3.value)
DDS_FOURCC = DDPF.FOURCC DDS_FOURCC = DDPF.FOURCC
DDS_RGB = DDPF.RGB DDS_RGB = DDPF.RGB

View File

@ -27,11 +27,12 @@
""" """
from __future__ import annotations from __future__ import annotations
from io import BytesIO from typing import IO
from . import ImageFile, ImagePalette, UnidentifiedImageError from . import ImageFile, ImagePalette, UnidentifiedImageError
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
from ._typing import StrOrBytesPath
class GdImageFile(ImageFile.ImageFile): class GdImageFile(ImageFile.ImageFile):
@ -80,7 +81,7 @@ class GdImageFile(ImageFile.ImageFile):
] ]
def open(fp: BytesIO, mode: str = "r") -> GdImageFile: def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile:
""" """
Load texture from a GD image file. Load texture from a GD image file.

View File

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

View File

@ -26,6 +26,7 @@
from __future__ import annotations from __future__ import annotations
import abc
import atexit import atexit
import builtins import builtins
import io import io
@ -39,12 +40,8 @@ import tempfile
import warnings import warnings
from collections.abc import Callable, MutableMapping from collections.abc import Callable, MutableMapping
from enum import IntEnum from enum import IntEnum
from pathlib import Path from types import ModuleType
from typing import IO, TYPE_CHECKING, Any
try:
from defusedxml import ElementTree
except ImportError:
ElementTree = None
# VERSION was removed in Pillow 6.0.0. # VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0.
@ -60,6 +57,12 @@ from . import (
from ._binary import i32le, o32be, o32le from ._binary import i32le, o32be, o32le
from ._util import DeferredError, is_path from ._util import DeferredError, is_path
ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
ElementTree = None
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -110,6 +113,7 @@ except ImportError as v:
USE_CFFI_ACCESS = False USE_CFFI_ACCESS = False
cffi: ModuleType | None
try: try:
import cffi import cffi
except ImportError: except ImportError:
@ -211,14 +215,22 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Registries # Registries
ID = [] if TYPE_CHECKING:
OPEN = {} from . import ImageFile
MIME = {} ID: list[str] = []
SAVE = {} OPEN: dict[
SAVE_ALL = {} str,
EXTENSION = {} tuple[
DECODERS = {} Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
ENCODERS = {} Callable[[bytes], bool] | None,
],
] = {}
MIME: dict[str, str] = {}
SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {}
SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {}
EXTENSION: dict[str, str] = {}
DECODERS: dict[str, object] = {}
ENCODERS: dict[str, object] = {}
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Modes # Modes
@ -571,7 +583,7 @@ class Image:
# object is gone. # object is gone.
self.im = DeferredError(ValueError("Operation on closed image")) self.im = DeferredError(ValueError("Operation on closed image"))
def _copy(self): def _copy(self) -> None:
self.load() self.load()
self.im = self.im.copy() self.im = self.im.copy()
self.pyaccess = None self.pyaccess = None
@ -2370,7 +2382,7 @@ class Image:
implement the ``seek``, ``tell``, and ``write`` implement the ``seek``, ``tell``, and ``write``
methods, and be opened in binary mode. methods, and be opened in binary mode.
:param fp: A filename (string), pathlib.Path object or file object. :param fp: A filename (string), os.PathLike object or file object.
:param format: Optional format override. If omitted, the :param format: Optional format override. If omitted, the
format to use is determined from the filename extension. format to use is determined from the filename extension.
If a file object was used instead of a filename, this If a file object was used instead of a filename, this
@ -2383,13 +2395,10 @@ class Image:
may have been created, and may contain partial data. may have been created, and may contain partial data.
""" """
filename = "" filename: str | bytes = ""
open_fp = False open_fp = False
if isinstance(fp, Path): if is_path(fp):
filename = str(fp) filename = os.path.realpath(os.fspath(fp))
open_fp = True
elif is_path(fp):
filename = fp
open_fp = True open_fp = True
elif fp == sys.stdout: elif fp == sys.stdout:
try: try:
@ -2398,7 +2407,7 @@ class Image:
pass pass
if not filename and hasattr(fp, "name") and is_path(fp.name): if not filename and hasattr(fp, "name") and is_path(fp.name):
# only set the name for metadata purposes # only set the name for metadata purposes
filename = fp.name filename = os.path.realpath(os.fspath(fp.name))
# may mutate self! # may mutate self!
self._ensure_mutable() self._ensure_mutable()
@ -2409,7 +2418,8 @@ class Image:
preinit() preinit()
ext = os.path.splitext(filename)[1].lower() filename_ext = os.path.splitext(filename)[1].lower()
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
if not format: if not format:
if ext not in EXTENSION: if ext not in EXTENSION:
@ -2451,7 +2461,7 @@ class Image:
if open_fp: if open_fp:
fp.close() fp.close()
def seek(self, frame) -> Image: def seek(self, frame) -> None:
""" """
Seeks to the given frame in this sequence file. If you seek Seeks to the given frame in this sequence file. If you seek
beyond the end of the sequence, the method raises an beyond the end of the sequence, the method raises an
@ -2511,10 +2521,8 @@ class Image:
self.load() self.load()
if self.im.bands == 1: if self.im.bands == 1:
ims = [self.copy()] return (self.copy(),)
else: return tuple(map(self._new, self.im.split()))
ims = map(self._new, self.im.split())
return tuple(ims)
def getchannel(self, channel): def getchannel(self, channel):
""" """
@ -2871,7 +2879,14 @@ class ImageTransformHandler:
(for use with :py:meth:`~PIL.Image.Image.transform`) (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
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -3206,7 +3221,7 @@ def open(fp, mode="r", formats=None) -> Image:
:py:meth:`~PIL.Image.Image.load` method). See :py:meth:`~PIL.Image.Image.load` method). See
:py:func:`~PIL.Image.new`. See :ref:`file-handling`. :py:func:`~PIL.Image.new`. See :ref:`file-handling`.
:param fp: A filename (string), pathlib.Path object or a file object. :param fp: A filename (string), os.PathLike object or a file object.
The file object must implement ``file.read``, The file object must implement ``file.read``,
``file.seek``, and ``file.tell`` methods, ``file.seek``, and ``file.tell`` methods,
and be opened in binary mode. The file object will also seek to zero and be opened in binary mode. The file object will also seek to zero
@ -3243,11 +3258,9 @@ def open(fp, mode="r", formats=None) -> Image:
raise TypeError(msg) raise TypeError(msg)
exclusive_fp = False exclusive_fp = False
filename = "" filename: str | bytes = ""
if isinstance(fp, Path): if is_path(fp):
filename = str(fp.resolve()) filename = os.path.realpath(os.fspath(fp))
elif is_path(fp):
filename = fp
if filename: if filename:
fp = builtins.open(filename, "rb") fp = builtins.open(filename, "rb")
@ -3421,7 +3434,11 @@ def merge(mode, bands):
# Plugin registry # Plugin registry
def register_open(id, factory, accept=None) -> None: def register_open(
id,
factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
accept: Callable[[bytes], bool] | None = None,
) -> None:
""" """
Register an image file plugin. This function should not be used Register an image file plugin. This function should not be used
in application code. in application code.
@ -3631,7 +3648,13 @@ _apply_env_variables()
atexit.register(core.clear_cache) atexit.register(core.clear_cache)
class Exif(MutableMapping): if TYPE_CHECKING:
_ExifBase = MutableMapping[int, Any]
else:
_ExifBase = MutableMapping
class Exif(_ExifBase):
""" """
This class provides read and write access to EXIF image data:: This class provides read and write access to EXIF image data::

View File

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

View File

@ -33,10 +33,10 @@ import sys
import warnings import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from pathlib import Path
from typing import BinaryIO from typing import BinaryIO
from . import Image from . import Image
from ._typing import StrOrBytesPath
from ._util import is_directory, is_path from ._util import is_directory, is_path
@ -193,7 +193,7 @@ class FreeTypeFont:
def __init__( def __init__(
self, self,
font: bytes | str | Path | BinaryIO | None = None, font: StrOrBytesPath | BinaryIO | None = None,
size: float = 10, size: float = 10,
index: int = 0, index: int = 0,
encoding: str = "", encoding: str = "",
@ -230,8 +230,7 @@ class FreeTypeFont:
) )
if is_path(font): if is_path(font):
if isinstance(font, Path): font = os.path.realpath(os.fspath(font))
font = str(font)
if sys.platform == "win32": if sys.platform == "win32":
font_bytes_path = font if isinstance(font, bytes) else font.encode() font_bytes_path = font if isinstance(font, bytes) else font.encode()
try: try:
@ -872,7 +871,7 @@ def load_path(filename):
raise OSError(msg) raise OSError(msg)
def load_default(size=None): def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
"""If FreeType support is available, load a version of Aileron Regular, """If FreeType support is available, load a version of Aileron Regular,
https://dotcolon.net/font/aileron, with a more limited character set. https://dotcolon.net/font/aileron, with a more limited character set.

View File

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

View File

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

View File

@ -14,17 +14,16 @@
# #
from __future__ import annotations from __future__ import annotations
from io import BytesIO
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i8 from ._binary import i8
from ._typing import SupportsRead
# #
# Bitstream parser # Bitstream parser
class BitStream: class BitStream:
def __init__(self, fp: BytesIO) -> None: def __init__(self, fp: SupportsRead[bytes]) -> None:
self.fp = fp self.fp = fp
self.bits = 0 self.bits = 0
self.bitbuffer = 0 self.bitbuffer = 0

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -5,7 +5,8 @@ from __future__ import annotations
import sys import sys
import tkinter import tkinter
from tkinter import _tkinter as tk
tk = getattr(tkinter, "_tkinter")
try: try:
if hasattr(sys, "pypy_find_executable"): if hasattr(sys, "pypy_find_executable"):

View File

@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import os
import sys import sys
from typing import Protocol, Sequence, TypeVar, Union
if sys.version_info >= (3, 10): if sys.version_info >= (3, 10):
from typing import TypeGuard from typing import TypeGuard
@ -15,4 +17,17 @@ else:
return bool return bool
__all__ = ["TypeGuard"] Coords = Union[Sequence[float], Sequence[Sequence[float]]]
_T_co = TypeVar("_T_co", covariant=True)
class SupportsRead(Protocol[_T_co]):
def read(self, __length: int = ...) -> _T_co: ...
StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"]

View File

@ -1,17 +1,16 @@
from __future__ import annotations from __future__ import annotations
import os import os
from pathlib import Path
from typing import Any, NoReturn from typing import Any, NoReturn
from ._typing import TypeGuard from ._typing import StrOrBytesPath, TypeGuard
def is_path(f: Any) -> TypeGuard[bytes | str | Path]: def is_path(f: Any) -> TypeGuard[StrOrBytesPath]:
return isinstance(f, (bytes, str, Path)) return isinstance(f, (bytes, str, os.PathLike))
def is_directory(f: Any) -> TypeGuard[bytes | str | Path]: def is_directory(f: Any) -> TypeGuard[StrOrBytesPath]:
"""Checks if an object is a string, and that it points to a directory.""" """Checks if an object is a string, and that it points to a directory."""
return is_path(f) and os.path.isdir(f) return is_path(f) and os.path.isdir(f)

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

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

View File

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