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 runs-on: ubuntu-latest
steps: steps:
# Drafts your next release notes as pull requests are merged into "main" # 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: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

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

View File

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

View File

@ -101,7 +101,7 @@ jobs:
MATRIX_DOCKER: ${{ matrix.docker }} MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3.1.5
with: with:
flags: GHA_Docker flags: GHA_Docker
name: ${{ matrix.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 python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3.1.5
with: with:
file: ./coverage.xml file: ./coverage.xml
flags: GHA_Windows flags: GHA_Windows

View File

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

View File

@ -149,7 +149,7 @@ jobs:
.ci/after_success.sh .ci/after_success.sh
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3.1.5
with: with:
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} 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 build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then if [ -n "$IS_MACOS" ]; then
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto 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 libXau 1.0.11 https://www.x.org/pub/individual/lib
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist 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 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 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
fi
else else
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
fi fi
@ -131,13 +129,13 @@ untar pillow-depends-main.zip
if [[ -n "$IS_MACOS" ]]; then if [[ -n "$IS_MACOS" ]]; then
# webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb # 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 # if php is installed, brew tries to reinstall these after installing openblas
# remove cairo to fix building harfbuzz on arm64 # remove cairo to fix building harfbuzz on arm64
# remove lcms2 and libpng to fix building openjpeg on arm64 # remove lcms2 and libpng to fix building openjpeg on arm64
# remove zstd to avoid inclusion on x86_64 # remove zstd to avoid inclusion on x86_64
# curl from brew requires zstd, use system curl # 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 brew install pkg-config
fi fi

View File

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

View File

@ -5,6 +5,12 @@ Changelog (Pillow)
10.3.0 (unreleased) 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 - Do not support using test-image-results to upload images after test failures #7739
[radarhere] [radarhere]

View File

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

@ -119,3 +119,15 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
subprocess.call(["wl-copy"], stdin=fp) subprocess.call(["wl-copy"], stdin=fp)
im = ImageGrab.grabclipboard() im = ImageGrab.grabclipboard()
assert_image_equal_tofile(im, image_path) 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) @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

@ -9,6 +9,7 @@ The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license: Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/ https://creativecommons.org/publicdomain/zero/1.0/
""" """
from __future__ import annotations from __future__ import annotations
import struct 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-command = "cd {project} && .github/workflows/wheels-test.sh"
test-extras = "tests" test-extras = "tests"
[tool.ruff] [tool.ruff.lint]
select = [ select = [
"C4", # flake8-comprehensions "C4", # flake8-comprehensions
"E", # pycodestyle errors "E", # pycodestyle errors
@ -104,25 +104,25 @@ select = [
"F", # pyflakes errors "F", # pyflakes errors
"I", # isort "I", # isort
"ISC", # flake8-implicit-str-concat "ISC", # flake8-implicit-str-concat
"LOG", # flake8-logging
"PGH", # pygrep-hooks "PGH", # pygrep-hooks
"RUF100", # unused noqa (yesqa) "RUF100", # unused noqa (yesqa)
"UP", # pyupgrade "UP", # pyupgrade
"W", # pycodestyle warnings "W", # pycodestyle warnings
"YTT", # flake8-2020 "YTT", # flake8-2020
# "LOG", # TODO: enable flake8-logging when it's not in preview anymore
] ]
extend-ignore = [ ignore = [
"E203", # Whitespace before ':' "E203", # Whitespace before ':'
"E221", # Multiple spaces before operator "E221", # Multiple spaces before operator
"E226", # Missing whitespace around arithmetic operator "E226", # Missing whitespace around arithmetic operator
"E241", # Multiple spaces after ',' "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_font.py" = ["I002"]
"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] "Tests/oss-fuzz/fuzz_pillow.py" = ["I002"]
[tool.ruff.isort] [tool.ruff.lint.isort]
known-first-party = ["PIL"] known-first-party = ["PIL"]
required-imports = ["from __future__ import annotations"] 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. - DXT3 compression is used if alpha_encoding == 1.
- DXT5 compression is used if alpha_encoding == 7. - DXT5 compression is used if alpha_encoding == 7.
""" """
from __future__ import annotations from __future__ import annotations
import os 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: Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/ https://creativecommons.org/publicdomain/zero/1.0/
""" """
from __future__ import annotations from __future__ import annotations
import io import io

View File

@ -50,9 +50,7 @@ class FontFile:
| None | None
] = [None] * 256 ] = [None] * 256
def __getitem__( def __getitem__(self, ix: int) -> (
self, ix: int
) -> (
tuple[ tuple[
tuple[int, int], tuple[int, int],
tuple[int, int, 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. Note: All data is stored in little-Endian (Intel) byte order.
""" """
from __future__ import annotations from __future__ import annotations
import struct 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 encoderinfo.get("optimize") and im_frame.mode != "1":
if "transparency" not in encoderinfo: if "transparency" not in encoderinfo:
try: try:
encoderinfo[ encoderinfo["transparency"] = (
"transparency" im_frame.palette._new_color_index(im_frame)
] = im_frame.palette._new_color_index(im_frame) )
except ValueError: except ValueError:
pass pass
if "transparency" in encoderinfo: if "transparency" in encoderinfo:

View File

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

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

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

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

View File

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

View File

@ -149,18 +149,7 @@ def grabclipboard():
session_type = None session_type = None
if shutil.which("wl-paste") and session_type in ("wayland", None): if shutil.which("wl-paste") and session_type in ("wayland", None):
output = subprocess.check_output(["wl-paste", "-l"]).decode() args = ["wl-paste", "-t", "image"]
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])
elif shutil.which("xclip") and session_type in ("x11", None): elif shutil.which("xclip") and session_type in ("x11", None):
args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]
else: else:
@ -168,10 +157,29 @@ def grabclipboard():
raise NotImplementedError(msg) raise NotImplementedError(msg)
p = subprocess.run(args, capture_output=True) p = subprocess.run(args, capture_output=True)
if p.returncode != 0:
err = p.stderr 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: if err:
msg = f"{args[0]} error: {err.strip().decode()}" msg += f": {err.strip().decode()}"
raise ChildProcessError(msg) raise ChildProcessError(msg)
data = io.BytesIO(p.stdout) data = io.BytesIO(p.stdout)
im = Image.open(data) im = Image.open(data)
im.load() im.load()

View File

@ -62,6 +62,7 @@ Libjpeg ref.:
https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html
""" """
from __future__ import annotations from __future__ import annotations
# fmt: off # 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) x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0)
info = { info = {
"title": None "title": (
if is_appending None if is_appending else os.path.splitext(os.path.basename(filename))[0]
else os.path.splitext(os.path.basename(filename))[0], ),
"author": None, "author": None,
"subject": None, "subject": None,
"keywords": None, "keywords": None,

View File

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

View File

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

View File

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