Merge pull request #7769 from radarhere/type_hints

This commit is contained in:
Hugo van Kemenade 2024-02-06 23:12:26 +02:00 committed by GitHub
commit 811dd15ca5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 235 additions and 162 deletions

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

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

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

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

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

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

View File

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

View File

@ -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,11 @@ 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 +796,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 +831,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 +849,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 +871,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 +890,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 +923,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 +945,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 +979,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 +1017,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 +1249,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 +1402,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 +1416,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 +1432,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 +1450,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 +1489,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 +1502,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 +1589,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

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

@ -571,7 +571,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

View File

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

View File

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

View File

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