diff --git a/.appveyor.yml b/.appveyor.yml index 4c5a7f9ee..b0740b1ac 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -6,6 +6,7 @@ init: # Uncomment previous line to get RDP access during the build. environment: + COVERAGE_CORE: sysmon EXECUTABLE: python.exe TEST_OPTIONS: DEPLOY: YES diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt new file mode 100644 index 000000000..ed3269460 --- /dev/null +++ b/.ci/requirements-mypy.txt @@ -0,0 +1 @@ +mypy==1.7.1 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e0e6804bf..8fc6bd0ad 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -tidelift: "pypi/Pillow" +tidelift: "pypi/pillow" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4319cc8ff..92e860cb5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,10 +7,12 @@ on: paths: - ".github/workflows/docs.yml" - "docs/**" + - "src/PIL/**" pull_request: paths: - ".github/workflows/docs.yml" - "docs/**" + - "src/PIL/**" workflow_dispatch: permissions: diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 2615fb427..4526b9454 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -26,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + COVERAGE_CORE: sysmon + jobs: build: runs-on: windows-latest diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 1c6d15b77..b4e479f12 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -26,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + COVERAGE_CORE: sysmon + jobs: build: runs-on: windows-latest diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 75fccf795..c936be559 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -26,13 +26,16 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + COVERAGE_CORE: sysmon + jobs: build: runs-on: windows-latest strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"] timeout-minutes: 30 @@ -66,8 +69,16 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py - - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma - run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma + - name: Install Python dependencies + run: > + python3 -m pip install + coverage>=7.4.2 + defusedxml + olefile + pyroma + pytest + pytest-cov + pytest-timeout - name: Install dependencies id: install diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 19f4a6dae..643273e58 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,7 @@ concurrency: cancel-in-progress: true env: + COVERAGE_CORE: sysmon FORCE_COLOR: 1 jobs: diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 26bf2f6d6..cc8d7e085 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -19,7 +19,7 @@ FREETYPE_VERSION=2.13.2 HARFBUZZ_VERSION=8.3.0 LIBPNG_VERSION=1.6.40 JPEGTURBO_VERSION=3.0.1 -OPENJPEG_VERSION=2.5.0 +OPENJPEG_VERSION=2.5.2 XZ_VERSION=5.4.5 TIFF_VERSION=4.6.0 LCMS2_VERSION=2.16 @@ -40,7 +40,7 @@ BROTLI_VERSION=1.1.0 if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then function build_openjpeg { - local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz) + local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz) (cd $out_dir \ && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ && make install) @@ -93,6 +93,9 @@ function build { done fi build_openjpeg + if [ -f /usr/local/lib64/libopenjp2.so ]; then + cp /usr/local/lib64/libopenjp2.so /usr/local/lib + fi ORIGINAL_CFLAGS=$CFLAGS CFLAGS="$CFLAGS -O3 -DNDEBUG" diff --git a/CHANGES.rst b/CHANGES.rst index a8404260f..205ffa294 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 10.3.0 (unreleased) ------------------- +- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782 + [evanmiller, radarhere] + +- Fixed reading FLI/FLC images with a prefix chunk #7804 + [twolife] + - Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745 [nik012003, radarhere] diff --git a/README.md b/README.md index 6ca870166..f142ef563 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ As of 2019, Pillow development is src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"> Tidelift + src="https://tidelift.com/badges/package/pypi/pillow?style=flat"> Newest PyPI version @@ -82,9 +82,6 @@ As of 2019, Pillow development is Join the chat at https://gitter.im/python-pillow/Pillow - Follow on https://twitter.com/PythonPillow Follow on https://fosstodon.org/@pillow None: +def test_version(version: int | None, expected: str) -> None: with pytest.warns(DeprecationWarning, match=expected): _deprecate.deprecate("Old thing", version, "new thing") @@ -46,7 +46,7 @@ def test_unknown_version() -> None: ), ], ) -def test_old_version(deprecated, plural, expected) -> None: +def test_old_version(deprecated: str, plural: bool, expected: str) -> None: expected = r"" with pytest.raises(RuntimeError, match=expected): _deprecate.deprecate(deprecated, 1, plural=plural) @@ -76,7 +76,7 @@ def test_replacement_and_action() -> None: "Upgrade to new thing.", ], ) -def test_action(action) -> None: +def test_action(action: str) -> None: expected = ( r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Upgrade to new thing\." diff --git a/Tests/test_features.py b/Tests/test_features.py index de74e9c18..8d2d198ff 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -2,6 +2,7 @@ from __future__ import annotations import io import re +from typing import Callable import pytest @@ -29,7 +30,7 @@ def test_version() -> None: # Check the correctness of the convenience function # and the format of version numbers - def test(name, function) -> None: + def test(name: str, function: Callable[[str], bool]) -> None: version = features.version(name) if not features.check(name): assert version is None @@ -73,12 +74,12 @@ def test_libimagequant_version() -> None: @pytest.mark.parametrize("feature", features.modules) -def test_check_modules(feature) -> None: +def test_check_modules(feature: str) -> None: assert features.check_module(feature) in [True, False] @pytest.mark.parametrize("feature", features.codecs) -def test_check_codecs(feature) -> None: +def test_check_codecs(feature: str) -> None: assert features.check_codec(feature) in [True, False] diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 3904d3bc5..1e2f20c40 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -71,7 +71,7 @@ def test_save(tmp_path: Path) -> None: "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", ], ) -def test_crashes(test_file) -> None: +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index c36466e02..1eaff0c7d 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -16,7 +16,7 @@ from .helper import ( def test_sanity(tmp_path: Path) -> None: - def roundtrip(im) -> None: + def roundtrip(im: Image.Image) -> None: outfile = str(tmp_path / "temp.bmp") im.save(outfile, "BMP") @@ -194,7 +194,7 @@ def test_rle4() -> None: ("Tests/images/bmp/g/pal8rle.bmp", 1064), ), ) -def test_rle8_eof(file_name, length) -> None: +def test_rle8_eof(file_name: str, length: int) -> None: with open(file_name, "rb") as fp: data = fp.read(length) with Image.open(io.BytesIO(data)) as im: diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index fc524721c..f86fb8d09 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -4,7 +4,7 @@ import warnings import pytest -from PIL import FliImagePlugin, Image +from PIL import FliImagePlugin, Image, ImageFile from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy @@ -12,9 +12,12 @@ from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy # save as...-> hopper.fli, default options. static_test_file = "Tests/images/hopper.fli" -# From https://samples.libav.org/fli-flc/ +# From https://samples.ffmpeg.org/fli-flc/ animated_test_file = "Tests/images/a.fli" +# From https://samples.ffmpeg.org/fli-flc/ +animated_test_file_with_prefix_chunk = "Tests/images/2422.flc" + def test_sanity() -> None: with Image.open(static_test_file) as im: @@ -32,6 +35,24 @@ def test_sanity() -> None: assert im.is_animated +def test_prefix_chunk() -> None: + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + with Image.open(animated_test_file_with_prefix_chunk) as im: + assert im.mode == "P" + assert im.size == (320, 200) + assert im.format == "FLI" + assert im.info["duration"] == 171 + assert im.is_animated + + palette = im.getpalette() + assert palette[3:6] == [255, 255, 255] + assert palette[381:384] == [204, 204, 12] + assert palette[765:] == [252, 0, 0] + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file() -> None: def open() -> None: diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 65f090931..f69a290fa 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -135,7 +135,7 @@ def test_different_bit_depths(tmp_path: Path) -> None: @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) -def test_save_to_bytes_bmp(mode) -> None: +def test_save_to_bytes_bmp(mode: str) -> None: output = io.BytesIO() im = hopper(mode) im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index f932069b9..036965bf5 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -82,7 +82,7 @@ def test_eoferror() -> None: @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) -def test_roundtrip(mode, tmp_path: Path) -> None: +def test_roundtrip(mode: str, tmp_path: Path) -> None: out = str(tmp_path / "temp.im") im = hopper(mode) im.save(out) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 9c0969437..88c30d468 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -98,7 +98,7 @@ def test_i() -> None: assert ret == 97 -def test_dump(monkeypatch) -> None: +def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: # Arrange c = b"abc" # Temporarily redirect stdout diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index f9f81d114..b0964aabe 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -52,7 +52,7 @@ def test_open_windows_v1() -> None: assert isinstance(im, MspImagePlugin.MspImageFile) -def _assert_file_image_equal(source_path, target_path) -> None: +def _assert_file_image_equal(source_path: str, target_path: str) -> None: with Image.open(source_path) as im: assert_image_equal_tofile(im, target_path) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index a2486be40..ab9f9663e 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -9,7 +9,7 @@ from PIL import Image, ImageFile, PcxImagePlugin from .helper import assert_image_equal, hopper -def _roundtrip(tmp_path: Path, im) -> None: +def _roundtrip(tmp_path: Path, im: Image.Image) -> None: f = str(tmp_path / "temp.pcx") im.save(f) with Image.open(f) as im2: @@ -44,7 +44,7 @@ def test_invalid_file() -> None: @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) -def test_odd(tmp_path: Path, mode) -> None: +def test_odd(tmp_path: Path, mode: str) -> None: # See issue #523, odd sized images should have a stride that's even. # Not that ImageMagick or GIMP write PCX that way. # We were not handling properly. @@ -89,7 +89,7 @@ def test_large_count(tmp_path: Path) -> None: _roundtrip(tmp_path, im) -def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None: +def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None: _last = ImageFile.MAXBLOCK ImageFile.MAXBLOCK = size try: diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 65a93c138..d39a86565 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -6,6 +6,7 @@ import os.path import tempfile import time from pathlib import Path +from typing import Any, Generator import pytest @@ -14,7 +15,7 @@ from PIL import Image, PdfParser, features from .helper import hopper, mark_if_feature_version, skip_unless_feature -def helper_save_as_pdf(tmp_path: Path, mode, **kwargs): +def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".pdf")) @@ -41,13 +42,13 @@ def helper_save_as_pdf(tmp_path: Path, mode, **kwargs): @pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) -def test_save(tmp_path: Path, mode) -> None: +def test_save(tmp_path: Path, mode: str) -> None: helper_save_as_pdf(tmp_path, mode) @skip_unless_feature("jpg_2000") @pytest.mark.parametrize("mode", ("LA", "RGBA")) -def test_save_alpha(tmp_path: Path, mode) -> None: +def test_save_alpha(tmp_path: Path, mode: str) -> None: helper_save_as_pdf(tmp_path, mode) @@ -112,7 +113,7 @@ def test_resolution(tmp_path: Path) -> None: {"dpi": (75, 150), "resolution": 200}, ), ) -def test_dpi(params, tmp_path: Path) -> None: +def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: im = hopper() outfile = str(tmp_path / "temp.pdf") @@ -156,7 +157,7 @@ def test_save_all(tmp_path: Path) -> None: assert os.path.getsize(outfile) > 0 # Test 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(outfile, save_all=True, append_images=im_generator(ims)) @@ -226,7 +227,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None: im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) -def check_pdf_pages_consistency(pdf) -> None: +def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None: pages_info = pdf.read_indirect(pdf.pages_ref) assert b"Parent" not in pages_info assert b"Kids" in pages_info @@ -339,7 +340,7 @@ def test_pdf_append_to_bytesio() -> None: @pytest.mark.timeout(1) @pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") @pytest.mark.parametrize("newline", (b"\r", b"\n")) -def test_redos(newline) -> None: +def test_redos(newline: bytes) -> None: malicious = b" trailer<<>>" + newline * 3456 # This particular exception isn't relevant here. diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index d4a634316..c51f56ce7 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -6,6 +6,7 @@ import warnings import zlib from io import BytesIO from pathlib import Path +from types import ModuleType from typing import Any import pytest @@ -23,6 +24,7 @@ from .helper import ( skip_unless_feature, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 7eca8d9b1..e60638b22 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -157,7 +157,7 @@ def test_combined_larger_than_size() -> None: ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), ], ) -def test_crashes(test_file, raises) -> None: +def test_crashes(test_file: str, raises) -> None: with open(test_file, "rb") as f: with pytest.raises(raises): with Image.open(f): diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bd8e522c7..3c6da50c5 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -22,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} @pytest.mark.parametrize("mode", _MODES) -def test_sanity(mode, tmp_path: Path) -> None: - def roundtrip(original_im) -> None: +def test_sanity(mode: str, tmp_path: Path) -> None: + def roundtrip(original_im: Image.Image) -> None: out = str(tmp_path / "temp.tga") original_im.save(out, rle=rle) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a16b76e19..0110948ae 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -4,6 +4,8 @@ import os import warnings from io import BytesIO from pathlib import Path +from types import ModuleType +from typing import Generator import pytest @@ -20,6 +22,7 @@ from .helper import ( is_win32, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: @@ -156,7 +159,7 @@ class TestFileTiff: "resolution_unit, dpi", [(None, 72.8), (2, 72.8), (3, 184.912)], ) - def test_load_float_dpi(self, resolution_unit, dpi) -> None: + def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None: with Image.open( "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: @@ -284,7 +287,7 @@ class TestFileTiff: ("Tests/images/multipage.tiff", 3), ), ) - def test_n_frames(self, path, n_frames) -> None: + def test_n_frames(self, path: str, n_frames: int) -> None: with Image.open(path) as im: assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) @@ -402,7 +405,7 @@ class TestFileTiff: assert len_before == len_after + 1 @pytest.mark.parametrize("legacy_api", (False, True)) - def test_load_byte(self, legacy_api) -> None: + def test_load_byte(self, legacy_api: bool) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc" ret = ifd.load_byte(data, legacy_api) @@ -431,7 +434,7 @@ class TestFileTiff: assert 0x8825 in im.tag_v2 def test_exif(self, tmp_path: Path) -> None: - def check_exif(exif) -> None: + def check_exif(exif: Image.Exif) -> None: assert sorted(exif.keys()) == [ 256, 257, @@ -511,7 +514,7 @@ class TestFileTiff: assert im.getexif()[273] == (1408, 1907) @pytest.mark.parametrize("mode", ("1", "L")) - def test_photometric(self, mode, tmp_path: Path) -> None: + def test_photometric(self, mode: str, tmp_path: Path) -> None: filename = str(tmp_path / "temp.tif") im = hopper(mode) im.save(filename, tiffinfo={262: 0}) @@ -660,7 +663,7 @@ class TestFileTiff: assert_image_equal_tofile(reloaded, infile) @pytest.mark.parametrize("mode", ("P", "PA")) - def test_palette(self, mode, tmp_path: Path) -> None: + def test_palette(self, mode: str, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") im = hopper(mode) @@ -689,7 +692,7 @@ class TestFileTiff: assert reread.n_frames == 3 # Test appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims mp = BytesIO() @@ -860,7 +863,7 @@ class TestFileTiff: ], ) @pytest.mark.timeout(2) - def test_oom(self, test_file) -> None: + def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): with Image.open(test_file): diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index bb6225d07..d7a18c725 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -189,7 +189,9 @@ def test_iptc(tmp_path: Path) -> None: @pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) -def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: +def test_writing_other_types_to_ascii( + value: bytes | int, expected: str, tmp_path: Path +) -> None: info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[271] @@ -206,7 +208,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: @pytest.mark.parametrize("value", (1, IFDRational(1))) -def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None: +def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 73aaae6e7..c07024a2c 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -2,21 +2,18 @@ from __future__ import annotations import colorsys import itertools +from typing import Callable from PIL import Image from .helper import assert_image_similar, hopper -def int_to_float(i): +def int_to_float(i: int) -> float: return i / 255 -def str_to_float(i): - return ord(i) / 255 - - -def tuple_to_ints(tp): +def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]: x, y, z = tp return int(x * 255.0), int(y * 255.0), int(z * 255.0) @@ -25,7 +22,7 @@ def test_sanity() -> None: Image.new("HSV", (100, 100)) -def wedge(): +def wedge() -> Image.Image: w = Image._wedge() w90 = w.rotate(90) @@ -49,7 +46,11 @@ def wedge(): return img -def to_xxx_colorsys(im, func, mode): +def to_xxx_colorsys( + im: Image.Image, + func: Callable[[float, float, float], tuple[float, float, float]], + mode: str, +) -> Image.Image: # convert the hard way using the library colorsys routines. (r, g, b) = im.split() @@ -70,11 +71,11 @@ def to_xxx_colorsys(im, func, mode): return hsv -def to_hsv_colorsys(im): +def to_hsv_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") -def to_rgb_colorsys(im): +def to_rgb_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") diff --git a/Tests/test_image.py b/Tests/test_image.py index 4c04e0da4..2a4d453e2 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -685,15 +685,18 @@ class TestImage: _make_new(im, blank_p, ImagePalette.ImagePalette()) _make_new(im, blank_pa, ImagePalette.ImagePalette()) - def test_p_from_rgb_rgba(self) -> None: - for mode, color in [ + @pytest.mark.parametrize( + "mode, color", + ( ("RGB", "#DDEEFF"), ("RGB", (221, 238, 255)), ("RGBA", (221, 238, 255, 255)), - ]: - im = Image.new("P", (100, 100), color) - expected = Image.new(mode, (100, 100), color) - assert_image_equal(im.convert(mode), expected) + ), + ) + def test_p_from_rgb_rgba(self, mode: str, color: str | tuple[int, ...]) -> None: + im = Image.new("P", (100, 100), color) + expected = Image.new(mode, (100, 100), color) + assert_image_equal(im.convert(mode), expected) def test_no_resource_warning_on_save(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/835 diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index ce7345572..2966f724f 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -8,7 +8,6 @@ from .helper import CachedProperty, assert_image_equal class TestImagingPaste: - masks = {} size = 128 def assert_9points_image( diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 94f57e066..7e2290c15 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Callable + from PIL import Image, ImageChops from .helper import assert_image_equal, hopper @@ -387,7 +389,9 @@ def test_overlay() -> None: def test_logical() -> None: - def table(op, a, b): + def table( + op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int + ) -> tuple[int, int, int, int]: out = [] for x in (a, b): imx = Image.new("1", (1, 1), x) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 83fc38ed3..6be29a70f 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -6,6 +6,7 @@ import re import shutil from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -237,7 +238,7 @@ def test_invalid_color_temperature() -> None: @pytest.mark.parametrize("flag", ("my string", -1)) -def test_invalid_flag(flag) -> None: +def test_invalid_flag(flag: str | int) -> None: with hopper() as im: with pytest.raises( ImageCms.PyCMSError, match="flags must be an integer between 0 and " @@ -335,19 +336,21 @@ def test_extended_information() -> None: o = ImageCms.getOpenProfile(SRGB) p = o.profile - def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None: + def assert_truncated_tuple_equal( + tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10 + ) -> None: # Helper function to reduce precision of tuples of floats # recursively and then check equality. power = 10**digits - def truncate_tuple(tuple_or_float): + def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]: return tuple( ( truncate_tuple(val) if isinstance(val, tuple) else int(val * power) / power ) - for val in tuple_or_float + for val in tuple_value ) assert truncate_tuple(tup1) == truncate_tuple(tup2) @@ -504,8 +507,10 @@ def test_profile_typesafety() -> None: ImageCms.ImageCmsProfile(1).tobytes() -def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None: - def create_test_image(): +def assert_aux_channel_preserved( + mode: str, transform_in_place: bool, preserved_channel: str +) -> None: + def create_test_image() -> Image.Image: # set up test image with something interesting in the tested aux channel. # fmt: off nine_grid_deltas = [ @@ -633,7 +638,7 @@ def test_auxiliary_channels_isolated() -> None: @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) -def test_rgb_lab(mode) -> None: +def test_rgb_lab(mode: str) -> None: im = Image.new(mode, (1, 1)) converted_im = im.convert("LAB") assert converted_im.getpixel((0, 0)) == (0, 128, 128) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 07a25b84b..3171eb9ae 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -5,6 +5,7 @@ import os.path import pytest from PIL import Image, ImageDraw, ImageDraw2, features +from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -56,7 +57,7 @@ def test_sanity() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(bbox) -> None: +def test_ellipse(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -84,7 +85,7 @@ def test_ellipse_edge() -> 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 = ImageDraw2.Draw(im) @@ -98,7 +99,7 @@ def test_line(points) -> None: @pytest.mark.parametrize("points", POINTS) -def test_line_pen_as_brush(points) -> None: +def test_line_pen_as_brush(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -114,7 +115,7 @@ def test_line_pen_as_brush(points) -> 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 = ImageDraw2.Draw(im) @@ -129,7 +130,7 @@ def test_polygon(points) -> 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 = ImageDraw2.Draw(im) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index 9ce9cda82..6ebc61e1b 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -22,7 +22,7 @@ def test_crash() -> None: ImageEnhance.Sharpness(im).enhance(0.5) -def _half_transparent_image(): +def _half_transparent_image() -> Image.Image: # returns an image, half transparent, half solid im = hopper("RGB") @@ -34,7 +34,9 @@ def _half_transparent_image(): return im -def _check_alpha(im, original, op, amount) -> None: +def _check_alpha( + im: Image.Image, original: Image.Image, op: str, amount: float +) -> None: assert im.getbands() == original.getbands() assert_image_equal( im.getchannel("A"), @@ -44,7 +46,7 @@ def _check_alpha(im, original, op, amount) -> None: @pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) -def test_alpha(op) -> None: +def test_alpha(op: str) -> None: # Issue https://github.com/python-pillow/Pillow/issues/899 # Is alpha preserved through image enhancement? diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 491409781..44521a8b3 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -31,7 +31,7 @@ SAFEBLOCK = ImageFile.SAFEBLOCK class TestImageFile: def test_parser(self) -> None: - def roundtrip(format): + def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 909026dc8..05b5d4716 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -7,11 +7,13 @@ import shutil import sys from io import BytesIO from pathlib import Path +from typing import Any, BinaryIO import pytest from packaging.version import parse as parse_version from PIL import Image, ImageDraw, ImageFont, features +from PIL._typing import StrOrBytesPath from .helper import ( assert_image_equal, @@ -42,16 +44,16 @@ def test_sanity() -> None: pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), ], ) -def layout_engine(request): +def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout: return request.param @pytest.fixture(scope="module") -def font(layout_engine): +def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont: return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) -def test_font_properties(font) -> None: +def test_font_properties(font: ImageFont.FreeTypeFont) -> None: assert font.path == FONT_PATH assert font.size == FONT_SIZE @@ -67,7 +69,9 @@ def test_font_properties(font) -> None: assert font_copy.path == second_font_path -def _render(font, layout_engine): +def _render( + font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout +) -> Image.Image: txt = "Hello World!" ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) ttf.getbbox(txt) @@ -80,12 +84,12 @@ def _render(font, layout_engine): @pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH))) -def test_font_with_name(layout_engine, font) -> None: +def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None: _render(font, layout_engine) -def test_font_with_filelike(layout_engine) -> None: - def _font_as_bytes(): +def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None: + def _font_as_bytes() -> BytesIO: with open(FONT_PATH, "rb") as f: font_bytes = BytesIO(f.read()) return font_bytes @@ -102,12 +106,12 @@ def test_font_with_filelike(layout_engine) -> None: # _render(shared_bytes) -def test_font_with_open_file(layout_engine) -> None: +def test_font_with_open_file(layout_engine: ImageFont.Layout) -> None: with open(FONT_PATH, "rb") as f: _render(f, layout_engine) -def test_render_equal(layout_engine) -> None: +def test_render_equal(layout_engine: ImageFont.Layout) -> None: img_path = _render(FONT_PATH, layout_engine) with open(FONT_PATH, "rb") as f: font_filelike = BytesIO(f.read()) @@ -116,7 +120,7 @@ def test_render_equal(layout_engine) -> None: assert_image_equal(img_path, img_filelike) -def test_non_ascii_path(tmp_path: Path, layout_engine) -> None: +def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) try: shutil.copy(FONT_PATH, tempfile) @@ -126,7 +130,7 @@ def test_non_ascii_path(tmp_path: Path, layout_engine) -> None: ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) -def test_transparent_background(font) -> None: +def test_transparent_background(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGBA", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -140,7 +144,7 @@ def test_transparent_background(font) -> None: assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_I16(font) -> None: +def test_I16(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="I;16", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -153,7 +157,7 @@ def test_I16(font) -> None: assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_textbbox_equal(font) -> None: +def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -181,7 +185,13 @@ def test_textbbox_equal(font) -> None: ), ) def test_getlength( - text, mode, fontname, size, layout_engine, length_basic, length_raqm + text: str, + mode: str, + fontname: str, + size: int, + layout_engine: ImageFont.Layout, + length_basic: int, + length_raqm: float, ) -> None: f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) @@ -207,7 +217,7 @@ def test_float_size() -> None: assert lengths[0] != lengths[1] != lengths[2] -def test_render_multiline(font) -> None: +def test_render_multiline(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) line_spacing = font.getbbox("A")[3] + 4 @@ -223,7 +233,7 @@ def test_render_multiline(font) -> None: assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) -def test_render_multiline_text(font) -> None: +def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: # Test that text() correctly connects to multiline_text() # and that align defaults to left im = Image.new(mode="RGB", size=(300, 100)) @@ -243,7 +253,9 @@ def test_render_multiline_text(font) -> None: @pytest.mark.parametrize( "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) ) -def test_render_multiline_text_align(font, align, ext) -> None: +def test_render_multiline_text_align( + font: ImageFont.FreeTypeFont, align: str, ext: str +) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) @@ -251,7 +263,7 @@ def test_render_multiline_text_align(font, align, ext) -> None: assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) -def test_unknown_align(font) -> None: +def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -260,14 +272,14 @@ def test_unknown_align(font) -> None: draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") -def test_draw_align(font) -> None: +def test_draw_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (300, 100), "white") draw = ImageDraw.Draw(im) line = "some text" draw.text((100, 40), line, (0, 0, 0), font=font, align="left") -def test_multiline_bbox(font) -> None: +def test_multiline_bbox(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -285,7 +297,7 @@ def test_multiline_bbox(font) -> None: draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) -def test_multiline_width(font) -> None: +def test_multiline_width(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -295,7 +307,7 @@ def test_multiline_width(font) -> None: ) -def test_multiline_spacing(font) -> None: +def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) @@ -306,7 +318,9 @@ def test_multiline_spacing(font) -> None: @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font(font, orientation) -> None: +def test_rotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -347,7 +361,9 @@ def test_rotated_transposed_font(font, orientation) -> None: Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font(font, orientation) -> None: +def test_unrotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -382,7 +398,9 @@ def test_unrotated_transposed_font(font, orientation) -> None: @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font_get_mask(font, orientation) -> None: +def test_rotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -403,7 +421,9 @@ def test_rotated_transposed_font_get_mask(font, orientation) -> None: Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font_get_mask(font, orientation) -> None: +def test_unrotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -415,11 +435,11 @@ def test_unrotated_transposed_font_get_mask(font, orientation) -> None: assert mask.size == (108, 13) -def test_free_type_font_get_name(font) -> None: +def test_free_type_font_get_name(font: ImageFont.FreeTypeFont) -> None: assert ("FreeMono", "Regular") == font.getname() -def test_free_type_font_get_metrics(font) -> None: +def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None: ascent, descent = font.getmetrics() assert isinstance(ascent, int) @@ -427,7 +447,7 @@ def test_free_type_font_get_metrics(font) -> None: assert (ascent, descent) == (16, 4) -def test_free_type_font_get_mask(font) -> None: +def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: # Arrange text = "mask this" @@ -473,16 +493,16 @@ def test_default_font() -> None: @pytest.mark.parametrize("mode", (None, "1", "RGBA")) -def test_getbbox(font, mode) -> None: +def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None: assert (0, 4, 12, 16) == font.getbbox("A", mode) -def test_getbbox_empty(font) -> None: +def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None: # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") -def test_render_empty(font) -> None: +def test_render_empty(font: ImageFont.FreeTypeFont) -> None: # issue 2666 im = Image.new(mode="RGB", size=(300, 100)) target = im.copy() @@ -492,7 +512,7 @@ def test_render_empty(font) -> None: assert_image_equal(im, target) -def test_unicode_extended(layout_engine) -> None: +def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: # issue #3777 text = "A\u278A\U0001F12B" target = "Tests/images/unicode_extended.png" @@ -515,21 +535,23 @@ def test_unicode_extended(layout_engine) -> None: (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), ) @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") -def test_find_font(monkeypatch, platform, font_directory) -> None: - def _test_fake_loading_font(path_to_fake, fontname) -> None: +def test_find_font( + monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str +) -> None: + def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with monkeypatch.context() as m: m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) - def loadable_font(filepath, size, index, encoding, *args, **kwargs): + def loadable_font( + filepath: str, size: int, index: int, encoding: str, *args: Any + ): if filepath == path_to_fake: return ImageFont._FreeTypeFont( - FONT_PATH, size, index, encoding, *args, **kwargs + FONT_PATH, size, index, encoding, *args ) - return ImageFont._FreeTypeFont( - filepath, size, index, encoding, *args, **kwargs - ) + return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args) m.setattr(ImageFont, "FreeTypeFont", loadable_font) font = ImageFont.truetype(fontname) @@ -543,7 +565,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None: if platform == "linux": monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") - def fake_walker(path): + def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]: if path == font_directory: return [ ( @@ -567,7 +589,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None: _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") -def test_imagefont_getters(font) -> None: +def test_imagefont_getters(font: ImageFont.FreeTypeFont) -> None: assert font.getmetrics() == (16, 4) assert font.font.ascent == 16 assert font.font.descent == 4 @@ -588,7 +610,7 @@ def test_imagefont_getters(font) -> None: @pytest.mark.parametrize("stroke_width", (0, 2)) -def test_getsize_stroke(font, stroke_width) -> None: +def test_getsize_stroke(font: ImageFont.FreeTypeFont, stroke_width: int) -> None: assert font.getbbox("A", stroke_width=stroke_width) == ( 0 - stroke_width, 4 - stroke_width, @@ -607,7 +629,7 @@ def test_complex_font_settings() -> None: t.getmask("абвг", language="sr") -def test_variation_get(font) -> None: +def test_variation_get(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -662,7 +684,7 @@ def test_variation_get(font) -> None: ] -def _check_text(font, path, epsilon): +def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None: im = Image.new("RGB", (100, 75), "white") d = ImageDraw.Draw(im) d.text((10, 10), "Text", font=font, fill="black") @@ -677,7 +699,7 @@ def _check_text(font, path, epsilon): raise -def test_variation_set_by_name(font) -> None: +def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -702,7 +724,7 @@ def test_variation_set_by_name(font) -> None: _check_text(font, "Tests/images/variation_tiny_name.png", 40) -def test_variation_set_by_axes(font) -> None: +def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -737,7 +759,9 @@ def test_variation_set_by_axes(font) -> None: ), ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), ) -def test_anchor(layout_engine, anchor, left, top) -> None: +def test_anchor( + layout_engine: ImageFont.Layout, anchor: str, left: int, top: int +) -> None: name, text = "quick", "Quick" path = f"Tests/images/test_anchor_{name}_{anchor}.png" @@ -782,7 +806,9 @@ def test_anchor(layout_engine, anchor, left, top) -> None: ("md", "center"), ), ) -def test_anchor_multiline(layout_engine, anchor, align) -> None: +def test_anchor_multiline( + layout_engine: ImageFont.Layout, anchor: str, align: str +) -> None: target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" text = "a\nlong\ntext sample" @@ -800,7 +826,7 @@ def test_anchor_multiline(layout_engine, anchor, align) -> None: assert_image_similar_tofile(im, target, 4) -def test_anchor_invalid(font) -> None: +def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (100, 100), "white") d = ImageDraw.Draw(im) d.font = font @@ -826,7 +852,7 @@ def test_anchor_invalid(font) -> None: @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) -def test_bitmap_font(layout_engine, bpp) -> None: +def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" @@ -843,7 +869,7 @@ def test_bitmap_font(layout_engine, bpp) -> None: assert_image_equal_tofile(im, target) -def test_bitmap_font_stroke(layout_engine) -> None: +def test_bitmap_font_stroke(layout_engine: ImageFont.Layout) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" @@ -861,7 +887,7 @@ def test_bitmap_font_stroke(layout_engine) -> None: @pytest.mark.parametrize("embedded_color", (False, True)) -def test_bitmap_blend(layout_engine, embedded_color) -> None: +def test_bitmap_blend(layout_engine: ImageFont.Layout, embedded_color: bool) -> None: font = ImageFont.truetype( "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine ) @@ -873,7 +899,7 @@ def test_bitmap_blend(layout_engine, embedded_color) -> None: assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png") -def test_standard_embedded_color(layout_engine) -> None: +def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) ttf.getbbox(txt) @@ -886,7 +912,7 @@ def test_standard_embedded_color(layout_engine) -> None: @pytest.mark.parametrize("fontmode", ("1", "L", "RGBA")) -def test_float_coord(layout_engine, fontmode): +def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) @@ -908,7 +934,7 @@ def test_float_coord(layout_engine, fontmode): raise -def test_cbdt(layout_engine) -> None: +def test_cbdt(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -925,7 +951,7 @@ def test_cbdt(layout_engine) -> None: pytest.skip("freetype compiled without libpng or CBDT support") -def test_cbdt_mask(layout_engine) -> None: +def test_cbdt_mask(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine @@ -942,7 +968,7 @@ def test_cbdt_mask(layout_engine) -> None: pytest.skip("freetype compiled without libpng or CBDT support") -def test_sbix(layout_engine) -> None: +def test_sbix(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -959,7 +985,7 @@ def test_sbix(layout_engine) -> None: pytest.skip("freetype compiled without libpng or SBIX support") -def test_sbix_mask(layout_engine) -> None: +def test_sbix_mask(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -977,7 +1003,7 @@ def test_sbix_mask(layout_engine) -> None: @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr(layout_engine) -> None: +def test_colr(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -993,7 +1019,7 @@ def test_colr(layout_engine) -> None: @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr_mask(layout_engine) -> None: +def test_colr_mask(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -1008,7 +1034,7 @@ def test_colr_mask(layout_engine) -> None: assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) -def test_woff2(layout_engine) -> None: +def test_woff2(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/OpenSans.woff2", @@ -1042,7 +1068,7 @@ def test_render_mono_size() -> None: assert_image_equal_tofile(im, "Tests/images/text_mono.gif") -def test_too_many_characters(font) -> None: +def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None: with pytest.raises(ValueError): font.getlength("A" * 1_000_001) with pytest.raises(ValueError): @@ -1070,14 +1096,14 @@ def test_too_many_characters(font) -> None: "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", ], ) -def test_oom(test_file) -> None: +def test_oom(test_file: str) -> None: with open(test_file, "rb") as f: font = ImageFont.truetype(BytesIO(f.read())) with pytest.raises(Image.DecompressionBombError): font.getmask("Test Text") -def test_raqm_missing_warning(monkeypatch) -> None: +def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) with pytest.warns(UserWarning) as record: font = ImageFont.truetype( @@ -1091,6 +1117,8 @@ def test_raqm_missing_warning(monkeypatch) -> None: @pytest.mark.parametrize("size", [-1, 0]) -def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size) -> None: +def test_invalid_truetype_sizes_raise_valueerror( + layout_engine: ImageFont.Layout, size: int +) -> None: with pytest.raises(ValueError): ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 235a2f993..e23adeb70 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -84,6 +84,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200 @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_file(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') p.communicate() @@ -94,6 +95,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200 @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_png(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None p.stdin.write( rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") $ms = new-object System.IO.MemoryStream(, $bytes) @@ -113,7 +115,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes) reason="Linux with wl-clipboard only", ) @pytest.mark.parametrize("ext", ("gif", "png", "ico")) - def test_grabclipboard_wl_clipboard(self, ext) -> None: + def test_grabclipboard_wl_clipboard(self, ext: str) -> None: image_path = "Tests/images/hopper." + ext with open(image_path, "rb") as fp: subprocess.call(["wl-copy"], stdin=fp) @@ -128,6 +130,6 @@ $ms = new-object System.IO.MemoryStream(, $bytes) reason="Linux with wl-clipboard only", ) @pytest.mark.parametrize("arg", ("text", "--clear")) - def test_grabclipboard_wl_clipboard_errors(self, arg): + def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None: subprocess.call(["wl-copy", arg]) assert ImageGrab.grabclipboard() is None diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 50bf404ae..b320e79c1 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -14,7 +14,7 @@ from .helper import ( class Deformer: - def getmesh(self, im): + def getmesh(self, im: Image.Image) -> list[tuple[tuple[int, ...], tuple[int, ...]]]: x, y = im.size return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] @@ -108,7 +108,7 @@ def test_fit_same_ratio() -> None: @pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) -def test_contain(new_size) -> None: +def test_contain(new_size: tuple[int, int]) -> None: im = hopper() new_im = ImageOps.contain(im, new_size) assert new_im.size == (256, 256) @@ -132,7 +132,7 @@ def test_contain_round() -> None: ("hopper.png", (256, 256)), # square ), ) -def test_cover(image_name, expected_size) -> None: +def test_cover(image_name: str, expected_size: tuple[int, int]) -> None: with Image.open("Tests/images/" + image_name) as im: new_im = ImageOps.cover(im, (256, 256)) assert new_im.size == expected_size @@ -168,7 +168,7 @@ def test_pad_round() -> None: @pytest.mark.parametrize("mode", ("P", "PA")) -def test_palette(mode) -> None: +def test_palette(mode: str) -> None: im = hopper(mode) # Expand @@ -210,7 +210,7 @@ def test_scale() -> None: @pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) -def test_expand_palette(border) -> None: +def test_expand_palette(border: int | tuple[int, int, int, int]) -> None: with Image.open("Tests/images/p_16.tga") as im: im_expanded = ImageOps.expand(im, border, (255, 0, 0)) @@ -366,7 +366,7 @@ def test_exif_transpose() -> None: for ext in exts: with Image.open("Tests/images/hopper" + ext) as base_im: - def check(orientation_im) -> None: + def check(orientation_im: Image.Image) -> None: for im in [ orientation_im, orientation_im.copy(), @@ -445,7 +445,7 @@ def test_autocontrast_cutoff() -> None: # Test the cutoff argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: - def autocontrast(cutoff): + def autocontrast(cutoff: int | tuple[int, int]): return ImageOps.autocontrast(img, cutoff).histogram() assert autocontrast(10) == autocontrast((10, 10)) @@ -486,20 +486,20 @@ def test_autocontrast_mask_real_input() -> None: assert result_nomask != result assert_tuple_approx_equal( ImageStat.Stat(result, mask=rect_mask).median, - [195, 202, 184], + (195, 202, 184), threshold=2, msg="autocontrast with mask pixel incorrect", ) assert_tuple_approx_equal( ImageStat.Stat(result_nomask).median, - [119, 106, 79], + (119, 106, 79), threshold=2, msg="autocontrast without mask pixel incorrect", ) def test_autocontrast_preserve_tone() -> None: - def autocontrast(mode, preserve_tone): + def autocontrast(mode: str, preserve_tone: bool) -> Image.Image: im = hopper(mode) return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() @@ -533,7 +533,7 @@ def test_autocontrast_preserve_gradient() -> None: @pytest.mark.parametrize( "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) ) -def test_autocontrast_preserve_one_color(color) -> None: +def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None: img = Image.new("RGB", (10, 10), color) # single color images shouldn't change diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 03302e20f..519d79105 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,12 +1,14 @@ from __future__ import annotations +from typing import Generator + import pytest from PIL import Image, ImageFilter @pytest.fixture -def test_images(): +def test_images() -> Generator[dict[str, Image.Image], None, None]: ims = { "im": Image.open("Tests/images/hopper.ppm"), "snakes": Image.open("Tests/images/color_snakes.png"), @@ -18,7 +20,7 @@ def test_images(): im.close() -def test_filter_api(test_images) -> None: +def test_filter_api(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] test_filter = ImageFilter.GaussianBlur(2.0) @@ -32,7 +34,7 @@ def test_filter_api(test_images) -> None: assert i.size == (128, 128) -def test_usm_formats(test_images) -> None: +def test_usm_formats(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] usm = ImageFilter.UnsharpMask @@ -50,7 +52,7 @@ def test_usm_formats(test_images) -> None: im.convert("YCbCr").filter(usm) -def test_blur_formats(test_images) -> None: +def test_blur_formats(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] blur = ImageFilter.GaussianBlur @@ -68,7 +70,7 @@ def test_blur_formats(test_images) -> None: im.convert("YCbCr").filter(blur) -def test_usm_accuracy(test_images) -> None: +def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None: snakes = test_images["snakes"] src = snakes.convert("RGB") @@ -77,7 +79,7 @@ def test_usm_accuracy(test_images) -> None: assert i.tobytes() == src.tobytes() -def test_blur_accuracy(test_images) -> None: +def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None: snakes = test_images["snakes"] i = snakes.filter(ImageFilter.GaussianBlur(0.4)) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 8ba745f21..9487560af 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -3,6 +3,7 @@ from __future__ import annotations import array import math import struct +from typing import Sequence import pytest @@ -57,7 +58,9 @@ def test_path() -> None: ImagePath.Path((0, 1)), ), ) -def test_path_constructors(coords) -> None: +def test_path_constructors( + coords: Sequence[float] | array.array[float] | ImagePath.Path, +) -> None: # Arrange / Act p = ImagePath.Path(coords) @@ -75,7 +78,9 @@ def test_path_constructors(coords) -> None: [[0.0, 1.0]], ), ) -def test_invalid_path_constructors(coords) -> None: +def test_invalid_path_constructors( + coords: tuple[str, str] | Sequence[Sequence[int]] +) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -93,7 +98,7 @@ def test_invalid_path_constructors(coords) -> None: [0, 1, 2], ), ) -def test_path_odd_number_of_coordinates(coords) -> None: +def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -111,7 +116,9 @@ def test_path_odd_number_of_coordinates(coords) -> None: (1, (0.0, 0.0, 0.0, 0.0)), ], ) -def test_getbbox(coords, expected) -> None: +def test_getbbox( + coords: int | list[int], expected: tuple[float, float, float, float] +) -> None: # Arrange p = ImagePath.Path(coords) @@ -135,7 +142,7 @@ def test_getbbox_no_args() -> None: (list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]), ], ) -def test_map(coords, expected) -> None: +def test_map(coords: int | list[int], expected: list[tuple[float, float]]) -> None: # Arrange p = ImagePath.Path(coords) @@ -201,9 +208,9 @@ class Evil: def __init__(self) -> None: self.corrupt = Image.core.path(0x4000000000000000) - def __getitem__(self, i): + def __getitem__(self, i: int) -> bytes: x = self.corrupt[i] return struct.pack("dd", x[0], x[1]) - def __setitem__(self, i, x) -> None: + def __setitem__(self, i: int, x: bytes) -> None: self.corrupt[i] = struct.unpack("dd", x) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 909f97167..88ad1f9ee 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -28,7 +28,7 @@ def test_rgb() -> None: assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) - def checkrgb(r, g, b) -> None: + def checkrgb(r: int, g: int, b: int) -> None: val = ImageQt.rgb(r, g, b) val = val % 2**24 # drop the alpha assert val >> 16 == r diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index f7269d45b..8d741d94a 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + import pytest from PIL import Image, ImageShow @@ -24,9 +26,9 @@ def test_register() -> None: "order", [-1, 0], ) -def test_viewer_show(order) -> None: +def test_viewer_show(order: int) -> None: class TestViewer(ImageShow.Viewer): - def show_image(self, image, **options) -> bool: + def show_image(self, image: Image.Image, **options: Any) -> bool: self.methodCalled = True return True @@ -48,7 +50,7 @@ def test_viewer_show(order) -> None: reason="Only run on CIs; hangs on Windows CIs", ) @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) -def test_show(mode) -> None: +def test_show(mode: str) -> None: im = hopper(mode) assert ImageShow.show(im) @@ -73,7 +75,7 @@ def test_viewer() -> None: @pytest.mark.parametrize("viewer", ImageShow._viewers) -def test_viewers(viewer) -> None: +def test_viewers(viewer: ImageShow.Viewer) -> None: try: viewer.get_command("test.jpg") except NotImplementedError: diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index c7f633e62..f59ee7284 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -70,7 +70,7 @@ if is_win32(): ] CreateDIBSection.restype = ctypes.wintypes.HBITMAP - def serialize_dib(bi, pixels): + def serialize_dib(bi, pixels) -> bytearray: bf = BITMAPFILEHEADER() bf.bfType = 0x4D42 bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 6ba95c2d7..9f4e6534e 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -14,7 +14,7 @@ TEST_IMAGE_SIZE = (10, 10) def test_numpy_to_image() -> None: - def to_image(dtype, bands: int = 1, boolean: int = 0): + def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image: if bands == 1: if boolean: data = [0, 255] * 50 @@ -99,7 +99,7 @@ def test_1d_array() -> None: assert_image(Image.fromarray(a), "L", (1, 5)) -def _test_img_equals_nparray(img, np) -> None: +def _test_img_equals_nparray(img: Image.Image, np) -> None: assert len(np.shape) >= 2 np_size = np.shape[1], np.shape[0] assert img.size == np_size @@ -157,7 +157,7 @@ def test_save_tiff_uint16() -> None: ("HSV", numpy.uint8), ), ) -def test_to_array(mode, dtype) -> None: +def test_to_array(mode: str, dtype) -> None: img = hopper(mode) # Resize to non-square diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 7d6c0a8cb..3cd323553 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from PIL import ImageQt +from PIL import Image, ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -37,7 +37,7 @@ if ImageQt.qt_is_installed: lbl.setPixmap(pixmap1.copy()) -def roundtrip(expected) -> None: +def roundtrip(expected: Image.Image) -> None: result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb assert_image_similar(result, expected.convert("RGB"), 1) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index a222a7d71..6110be707 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -17,7 +17,7 @@ if ImageQt.qt_is_installed: @pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) -def test_sanity(mode, tmp_path: Path) -> None: +def test_sanity(mode: str, tmp_path: Path) -> None: src = hopper(mode) data = ImageQt.toqimage(src) diff --git a/Tests/test_util.py b/Tests/test_util.py index 73e4acd55..197ef79ee 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -10,7 +10,7 @@ from PIL import _util @pytest.mark.parametrize( "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] ) -def test_is_path(test_path) -> None: +def test_is_path(test_path: str | Path | PurePath) -> None: # Act it_is = _util.is_path(test_path) diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index 4f4b81a62..8c2967bc2 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -1,7 +1,7 @@ #!/bin/bash # install openjpeg -archive=openjpeg-2.5.0 +archive=openjpeg-2.5.2 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/conf.py b/docs/conf.py index 9ae7ae605..97289c91d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -326,7 +326,7 @@ linkcheck_allowed_redirects = { r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", - r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", + r"https://tidelift.com/badges/package/pypi/pillow?.*": r"https://img.shields.io/badge/.*", r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", } diff --git a/docs/deprecations.rst b/docs/deprecations.rst index fed0cb308..33bc14187 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -536,3 +536,27 @@ PIL.OleFileIO the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0 (2018-01). The deprecated file has now been removed from Pillow. If needed, install from PyPI (eg. ``python3 -m pip install olefile``). + +import _imaging +~~~~~~~~~~~~~~~ + +.. versionremoved:: 2.1.0 + +Pillow >= 2.1.0 no longer supports ``import _imaging``. +Please use ``from PIL.Image import core as _imaging`` instead. + +Pillow and PIL +~~~~~~~~~~~~~~ + +.. versionremoved:: 1.0.0 + +Pillow and PIL cannot co-exist in the same environment. +Before installing Pillow, please uninstall PIL. + +import Image +~~~~~~~~~~~~ + +.. versionremoved:: 1.0.0 + +Pillow >= 1.0 no longer supports ``import Image``. +Please use ``from PIL import Image`` instead. diff --git a/docs/index.rst b/docs/index.rst index 558369919..f4e81e45d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -49,7 +49,7 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more -Warnings --------- - -.. warning:: Pillow and PIL cannot co-exist in the same environment. Before installing Pillow, please uninstall PIL. - -.. warning:: Pillow >= 1.0 no longer supports ``import Image``. Please use ``from PIL import Image`` instead. - -.. warning:: Pillow >= 2.1.0 no longer supports ``import _imaging``. Please use ``from PIL.Image import core as _imaging`` instead. - Python Support -------------- @@ -186,7 +177,7 @@ Many of Pillow's features require external libraries: * **openjpeg** provides JPEG 2000 functionality. * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, - **2.4.0** and **2.5.0**. + **2.4.0**, **2.5.0** and **2.5.2**. * Pillow does **not** support the earlier **1.5** series which ships with Debian Jessie. diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index 475253078..051fdcfc9 100644 --- a/docs/reference/ImageOps.rst +++ b/docs/reference/ImageOps.rst @@ -14,6 +14,8 @@ only work on L and RGB images. .. autofunction:: colorize .. autofunction:: crop .. autofunction:: scale +.. autoclass:: SupportsGetMesh + :show-inheritance: .. autofunction:: deform .. autofunction:: equalize .. autofunction:: expand diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst index 8772a382d..af31cdb74 100644 --- a/docs/releasenotes/10.3.0.rst +++ b/docs/releasenotes/10.3.0.rst @@ -79,3 +79,9 @@ Portable FloatMap (PFM) images Support has been added for reading and writing grayscale (Pf format) Portable FloatMap (PFM) files containing ``F`` data. + +Release GIL when fetching WebP frames +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Python's Global Interpreter Lock is now released when fetching WebP frames from +the libwebp decoder. diff --git a/pyproject.toml b/pyproject.toml index 48c59f2a1..518facc34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ classifiers = [ "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", "Topic :: Multimedia :: Graphics :: Graphics Conversion", "Topic :: Multimedia :: Graphics :: Viewers", + "Typing :: Typed", ] dynamic = [ "version", @@ -79,7 +80,6 @@ Homepage = "https://python-pillow.org" Mastodon = "https://fosstodon.org/@pillow" "Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" Source = "https://github.com/python-pillow/Pillow" -Twitter = "https://twitter.com/PythonPillow" [tool.setuptools] packages = ["PIL"] @@ -140,7 +140,3 @@ follow_imports = "silent" warn_redundant_casts = true warn_unreachable = true warn_unused_ignores = true -exclude = [ - '^src/PIL/FpxImagePlugin.py$', - '^src/PIL/MicImagePlugin.py$', -] diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 9769761fc..f9e4c731c 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -77,6 +77,7 @@ class FliImageFile(ImageFile.ImageFile): if i16(s, 4) == 0xF100: # prefix chunk; ignore it self.__offset = self.__offset + i32(s) + self.fp.seek(self.__offset) s = self.fp.read(16) if i16(s, 4) == 0xF1FA: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a770488b7..ba81a22c7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1430,7 +1430,7 @@ class Image: root = ElementTree.fromstring(xmp_tags) return {get_name(root.tag): get_value(root)} - def getexif(self): + def getexif(self) -> Exif: """ Gets EXIF data from the image. @@ -1438,7 +1438,6 @@ class Image: """ if self._exif is None: self._exif = Exif() - self._exif._loaded = False elif self._exif._loaded: return self._exif self._exif._loaded = True @@ -1525,7 +1524,7 @@ class Image: self.load() return self.im.ptr - def getpalette(self, rawmode="RGB"): + def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: """ Returns the image palette as a list. @@ -1615,7 +1614,7 @@ class Image: x, y = self.im.getprojection() return list(x), list(y) - def histogram(self, mask=None, extrema=None): + def histogram(self, mask=None, extrema=None) -> list[int]: """ Returns a histogram for the image. The histogram is returned as a list of pixel counts, one for each pixel value in the source @@ -1804,7 +1803,7 @@ class Image: result = alpha_composite(background, overlay) self.paste(result, box) - def point(self, lut, mode=None): + def point(self, lut, mode: str | None = None) -> Image: """ Maps this image through a lookup table or function. @@ -1928,7 +1927,7 @@ class Image: self.im.putdata(data, scale, offset) - def putpalette(self, data, rawmode="RGB"): + def putpalette(self, data, rawmode="RGB") -> None: """ Attaches a palette to this image. The image must be a "P", "PA", "L" or "LA" image. @@ -2108,7 +2107,7 @@ class Image: min(self.size[1], math.ceil(box[3] + support_y)), ) - def resize(self, size, resample=None, box=None, reducing_gap=None): + def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image: """ Returns a resized copy of this image. @@ -2200,10 +2199,11 @@ class Image: if factor_x > 1 or factor_y > 1: reduce_box = self._get_safe_box(size, resample, box) factor = (factor_x, factor_y) - if callable(self.reduce): - self = self.reduce(factor, box=reduce_box) - else: - self = Image.reduce(self, factor, box=reduce_box) + self = ( + self.reduce(factor, box=reduce_box) + if callable(self.reduce) + else Image.reduce(self, factor, box=reduce_box) + ) box = ( (box[0] - reduce_box[0]) / factor_x, (box[1] - reduce_box[1]) / factor_y, @@ -2818,7 +2818,7 @@ class Image: self.im.transform2(box, image.im, method, data, resample, fill) - def transpose(self, method): + def transpose(self, method: Transpose) -> Image: """ Transpose image (flip or rotate in 90 degree steps) @@ -2870,7 +2870,9 @@ class ImagePointHandler: (for use with :py:meth:`~PIL.Image.Image.point`) """ - pass + @abc.abstractmethod + def point(self, im: Image) -> Image: + pass class ImageTransformHandler: @@ -3690,6 +3692,7 @@ class Exif(_ExifBase): endian = None bigtiff = False + _loaded = False def __init__(self): self._data = {} @@ -3805,7 +3808,7 @@ class Exif(_ExifBase): return merged_dict - def tobytes(self, offset=8): + def tobytes(self, offset: int = 8) -> bytes: from . import TiffImagePlugin head = self._get_head() @@ -3960,7 +3963,7 @@ class Exif(_ExifBase): del self._info[tag] self._data[tag] = value - def __delitem__(self, tag): + def __delitem__(self, tag: int) -> None: if self._info is not None and tag in self._info: del self._info[tag] else: diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index ad59b0667..5fb80b753 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -124,7 +124,7 @@ def getrgb(color): @lru_cache -def getcolor(color, mode): +def getcolor(color, mode: str) -> tuple[int, ...]: """ Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index a9e626b2b..33db8fa50 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,6 +21,7 @@ from __future__ import annotations import functools import operator import re +from typing import Protocol, Sequence, cast from . import ExifTags, Image, ImagePalette @@ -28,7 +29,7 @@ from . import ExifTags, Image, ImagePalette # helpers -def _border(border): +def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: if isinstance(border, tuple): if len(border) == 2: left, top = right, bottom = border @@ -39,7 +40,7 @@ def _border(border): return left, top, right, bottom -def _color(color, mode): +def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]: if isinstance(color, str): from . import ImageColor @@ -47,7 +48,7 @@ def _color(color, mode): return color -def _lut(image, lut): +def _lut(image: Image.Image, lut: list[int]) -> Image.Image: if image.mode == "P": # FIXME: apply to lookup table, not image data msg = "mode P support coming soon" @@ -65,7 +66,13 @@ def _lut(image, lut): # actions -def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): +def autocontrast( + image: Image.Image, + cutoff: float | tuple[float, float] = 0, + ignore: int | Sequence[int] | None = None, + mask: Image.Image | None = None, + preserve_tone: bool = False, +) -> Image.Image: """ Maximize (normalize) image contrast. This function calculates a histogram of the input image (or mask region), removes ``cutoff`` percent of the @@ -97,10 +104,9 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): h = histogram[layer : layer + 256] if ignore is not None: # get rid of outliers - try: + if isinstance(ignore, int): h[ignore] = 0 - except TypeError: - # assume sequence + else: for ix in ignore: h[ix] = 0 if cutoff: @@ -112,7 +118,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): for ix in range(256): n = n + h[ix] # remove cutoff% pixels from the low end - cut = n * cutoff[0] // 100 + cut = int(n * cutoff[0] // 100) for lo in range(256): if cut > h[lo]: cut = cut - h[lo] @@ -123,7 +129,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): if cut <= 0: break # remove cutoff% samples from the high end - cut = n * cutoff[1] // 100 + cut = int(n * cutoff[1] // 100) for hi in range(255, -1, -1): if cut > h[hi]: cut = cut - h[hi] @@ -156,7 +162,15 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): return _lut(image, lut) -def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127): +def colorize( + image: Image.Image, + black: str | tuple[int, ...], + white: str | tuple[int, ...], + mid: str | int | tuple[int, ...] | None = None, + blackpoint: int = 0, + whitepoint: int = 255, + midpoint: int = 127, +) -> Image.Image: """ Colorize grayscale image. This function calculates a color wedge which maps all black pixels in @@ -188,10 +202,9 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 # Define colors from arguments - black = _color(black, "RGB") - white = _color(white, "RGB") - if mid is not None: - mid = _color(mid, "RGB") + rgb_black = cast(Sequence[int], _color(black, "RGB")) + rgb_white = cast(Sequence[int], _color(white, "RGB")) + rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None # Empty lists for the mapping red = [] @@ -200,18 +213,24 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the low-end values for i in range(0, blackpoint): - red.append(black[0]) - green.append(black[1]) - blue.append(black[2]) + red.append(rgb_black[0]) + green.append(rgb_black[1]) + blue.append(rgb_black[2]) # Create the mapping (2-color) - if mid is None: + if rgb_mid is None: range_map = range(0, whitepoint - blackpoint) for i in range_map: - red.append(black[0] + i * (white[0] - black[0]) // len(range_map)) - green.append(black[1] + i * (white[1] - black[1]) // len(range_map)) - blue.append(black[2] + i * (white[2] - black[2]) // len(range_map)) + red.append( + rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map) + ) + green.append( + rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map) + ) + blue.append( + rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map) + ) # Create the mapping (3-color) else: @@ -219,26 +238,36 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi range_map2 = range(0, whitepoint - midpoint) for i in range_map1: - red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1)) - green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1)) - blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1)) + red.append( + rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1) + ) + green.append( + rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1) + ) + blue.append( + rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1) + ) for i in range_map2: - red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2)) - green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2)) - blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2)) + red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2)) + green.append( + rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2) + ) + blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2)) # Create the high-end values for i in range(0, 256 - whitepoint): - red.append(white[0]) - green.append(white[1]) - blue.append(white[2]) + red.append(rgb_white[0]) + green.append(rgb_white[1]) + blue.append(rgb_white[2]) # Return converted image image = image.convert("RGB") return _lut(image, red + green + blue) -def contain(image, size, method=Image.Resampling.BICUBIC): +def contain( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a resized version of the image, set to the maximum width and height within the requested size, while maintaining the original aspect ratio. @@ -267,7 +296,9 @@ def contain(image, size, method=Image.Resampling.BICUBIC): return image.resize(size, resample=method) -def cover(image, size, method=Image.Resampling.BICUBIC): +def cover( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a resized version of the image, so that the requested size is covered, while maintaining the original aspect ratio. @@ -296,7 +327,13 @@ def cover(image, size, method=Image.Resampling.BICUBIC): return image.resize(size, resample=method) -def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)): +def pad( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + color: str | int | tuple[int, ...] | None = None, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: """ Returns a resized and padded version of the image, expanded to fill the requested aspect ratio and size. @@ -334,7 +371,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 return out -def crop(image, border=0): +def crop(image: Image.Image, border: int = 0) -> Image.Image: """ Remove border from image. The same amount of pixels are removed from all four sides. This function works on all image modes. @@ -349,7 +386,9 @@ def crop(image, border=0): return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) -def scale(image, factor, resample=Image.Resampling.BICUBIC): +def scale( + image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a rescaled image by a specific factor given in parameter. A factor greater than 1 expands the image, between 0 and 1 contracts the @@ -372,7 +411,27 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): return image.resize(size, resample) -def deform(image, deformer, resample=Image.Resampling.BILINEAR): +class SupportsGetMesh(Protocol): + """ + An object that supports the ``getmesh`` method, taking an image as an + argument, and returning a list of tuples. Each tuple contains two tuples, + the source box as a tuple of 4 integers, and a tuple of 8 integers for the + final quadrilateral, in order of top left, bottom left, bottom right, top + right. + """ + + def getmesh( + self, image: Image.Image + ) -> list[ + tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] + ]: ... + + +def deform( + image: Image.Image, + deformer: SupportsGetMesh, + resample: int = Image.Resampling.BILINEAR, +) -> Image.Image: """ Deform the image. @@ -388,7 +447,7 @@ def deform(image, deformer, resample=Image.Resampling.BILINEAR): ) -def equalize(image, mask=None): +def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image: """ Equalize the image histogram. This function applies a non-linear mapping to the input image, in order to create a uniform @@ -419,7 +478,11 @@ def equalize(image, mask=None): return _lut(image, lut) -def expand(image, border=0, fill=0): +def expand( + image: Image.Image, + border: int | tuple[int, ...] = 0, + fill: str | int | tuple[int, ...] = 0, +) -> Image.Image: """ Add border to the image @@ -445,7 +508,13 @@ def expand(image, border=0, fill=0): return out -def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)): +def fit( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + bleed: float = 0.0, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: """ Returns a resized and cropped version of the image, cropped to the requested aspect ratio and size. @@ -479,13 +548,12 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, # kevin@cazabon.com # https://www.cazabon.com - # ensure centering is mutable - centering = list(centering) + centering_x, centering_y = centering - if not 0.0 <= centering[0] <= 1.0: - centering[0] = 0.5 - if not 0.0 <= centering[1] <= 1.0: - centering[1] = 0.5 + if not 0.0 <= centering_x <= 1.0: + centering_x = 0.5 + if not 0.0 <= centering_y <= 1.0: + centering_y = 0.5 if not 0.0 <= bleed < 0.5: bleed = 0.0 @@ -522,8 +590,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, crop_height = live_size[0] / output_ratio # make the crop - crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0] - crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1] + crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x + crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) @@ -531,7 +599,7 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, return image.resize(size, method, box=crop) -def flip(image): +def flip(image: Image.Image) -> Image.Image: """ Flip the image vertically (top to bottom). @@ -541,7 +609,7 @@ def flip(image): return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) -def grayscale(image): +def grayscale(image: Image.Image) -> Image.Image: """ Convert the image to grayscale. @@ -551,7 +619,7 @@ def grayscale(image): return image.convert("L") -def invert(image): +def invert(image: Image.Image) -> Image.Image: """ Invert (negate) the image. @@ -562,7 +630,7 @@ def invert(image): return image.point(lut) if image.mode == "1" else _lut(image, lut) -def mirror(image): +def mirror(image: Image.Image) -> Image.Image: """ Flip image horizontally (left to right). @@ -572,7 +640,7 @@ def mirror(image): return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) -def posterize(image, bits): +def posterize(image: Image.Image, bits: int) -> Image.Image: """ Reduce the number of bits for each color channel. @@ -585,7 +653,7 @@ def posterize(image, bits): return _lut(image, lut) -def solarize(image, threshold=128): +def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: """ Invert all pixel values above a threshold. @@ -602,7 +670,7 @@ def solarize(image, threshold=128): return _lut(image, lut) -def exif_transpose(image, *, in_place=False): +def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: """ If an image has an EXIF Orientation tag, other than 1, transpose the image accordingly, and remove the orientation data. @@ -616,7 +684,7 @@ def exif_transpose(image, *, in_place=False): """ image.load() image_exif = image.getexif() - orientation = image_exif.get(ExifTags.Base.Orientation) + orientation = image_exif.get(ExifTags.Base.Orientation, 1) method = { 2: Image.Transpose.FLIP_LEFT_RIGHT, 3: Image.Transpose.ROTATE_180, @@ -653,3 +721,4 @@ def exif_transpose(image, *, in_place=False): return transposed_image elif not in_place: return image.copy() + return None diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 2b6cecc61..770d10025 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -18,6 +18,7 @@ from __future__ import annotations import array +from typing import Sequence from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile @@ -34,11 +35,11 @@ class ImagePalette: Defaults to an empty palette. """ - def __init__(self, mode="RGB", palette=None): + def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None: self.mode = mode self.rawmode = None # if set, palette contains raw data self.palette = palette or bytearray() - self.dirty = None + self.dirty: int | None = None @property def palette(self): @@ -127,7 +128,7 @@ class ImagePalette: raise ValueError(msg) from e return index - def getcolor(self, color, image=None): + def getcolor(self, color, image=None) -> int: """Given an rgb tuple, allocate palette entry. .. warning:: This method is experimental. diff --git a/src/PIL/py.typed b/src/PIL/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/_webp.c b/src/_webp.c index a1b4dbc1a..47592547c 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -448,11 +448,16 @@ PyObject * _anim_decoder_get_next(PyObject *self) { uint8_t *buf; int timestamp; + int ok; PyObject *bytes; PyObject *ret; + ImagingSectionCookie cookie; WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; - if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) { + ImagingSectionEnter(&cookie); + ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp); + ImagingSectionLeave(&cookie); + if (!ok) { PyErr_SetString(PyExc_OSError, "failed to read next frame"); return NULL; } diff --git a/tox.ini b/tox.ini index 8c818df7a..85a2020d6 100644 --- a/tox.ini +++ b/tox.ini @@ -33,14 +33,15 @@ commands = [testenv:mypy] skip_install = true deps = + -r .ci/requirements-mypy.txt IceSpringPySideStubs-PyQt6 IceSpringPySideStubs-PySide6 ipython - mypy==1.7.1 numpy packaging types-cffi types-defusedxml + types-olefile extras = typing commands = diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index df33ea493..2ee9872e6 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -308,21 +308,16 @@ DEPS = { "libs": [r"Lib\MS\*.lib"], }, "openjpeg": { - "url": "https://github.com/uclouvain/openjpeg/archive/v2.5.0.tar.gz", - "filename": "openjpeg-2.5.0.tar.gz", - "dir": "openjpeg-2.5.0", + "url": "https://github.com/uclouvain/openjpeg/archive/v2.5.2.tar.gz", + "filename": "openjpeg-2.5.2.tar.gz", + "dir": "openjpeg-2.5.2", "license": "LICENSE", - "patch": { - r"src\lib\openjp2\ht_dec.c": { - "#ifdef OPJ_COMPILER_MSVC\n return (OPJ_UINT32)__popcnt(val);": "#if defined(OPJ_COMPILER_MSVC) && (defined(_M_IX86) || defined(_M_AMD64))\n return (OPJ_UINT32)__popcnt(val);", # noqa: E501 - } - }, "build": [ *cmds_cmake( "openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF" ), - cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"), - cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.0"), + cmd_mkdir(r"{inc_dir}\openjpeg-2.5.2"), + cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.2"), ], "libs": [r"bin\*.lib"], },