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

This commit is contained in:
Andrew Murray 2024-02-07 20:50:36 +11:00
commit 159fc068ca
49 changed files with 328 additions and 226 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -570,7 +570,7 @@ class Image:
# object is gone.
self.im = DeferredError(ValueError("Operation on closed image"))
def _copy(self):
def _copy(self) -> None:
self.load()
self.im = self.im.copy()
self.pyaccess = None

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
""" Find compiled module linking to Tcl / Tk libraries
"""
from __future__ import annotations
import sys

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import os
import sys
from typing import Protocol, TypeVar, Union
from typing import Protocol, Sequence, TypeVar, Union
if sys.version_info >= (3, 10):
from typing import TypeGuard
@ -17,12 +17,14 @@ else:
return bool
Coords = Union[Sequence[float], Sequence[Sequence[float]]]
_T_co = TypeVar("_T_co", covariant=True)
class SupportsRead(Protocol[_T_co]):
def read(self, __length: int = ...) -> _T_co:
...
def read(self, __length: int = ...) -> _T_co: ...
FileDescriptor = int