Merge branch 'main' into type-hints-replace-io.BytesIO

This commit is contained in:
Andrew Murray 2024-02-11 22:02:55 +11:00
commit 29dd02509d
25 changed files with 331 additions and 231 deletions

View File

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

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

@ -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

@ -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

@ -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

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

View File

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

View File

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

View File

@ -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

@ -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

@ -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,11 +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 types import ModuleType
try: from typing import IO, TYPE_CHECKING, Any
from defusedxml import ElementTree
except ImportError:
ElementTree = None
# VERSION was removed in Pillow 6.0.0. # VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0.
@ -59,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__)
@ -109,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:
@ -210,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
@ -2382,7 +2395,7 @@ 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 is_path(fp): if is_path(fp):
filename = os.path.realpath(os.fspath(fp)) filename = os.path.realpath(os.fspath(fp))
@ -2394,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()
@ -2405,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:
@ -2447,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
@ -2507,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):
""" """
@ -2867,6 +2879,13 @@ class ImageTransformHandler:
(for use with :py:meth:`~PIL.Image.Image.transform`) (for use with :py:meth:`~PIL.Image.Image.transform`)
""" """
@abc.abstractmethod
def transform(
self,
size: tuple[int, int],
image: Image,
**options: dict[str, str | int | tuple[int, ...] | list[int]],
) -> Image:
pass pass
@ -3239,7 +3258,7 @@ 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 is_path(fp): if is_path(fp):
filename = os.path.realpath(os.fspath(fp)) filename = os.path.realpath(os.fspath(fp))
@ -3415,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.
@ -3625,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

@ -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

@ -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"):

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 =