mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-10-31 16:07:30 +03:00 
			
		
		
		
	Merge branch 'main' into progress
This commit is contained in:
		
						commit
						8b4b7ce7dd
					
				|  | @ -37,12 +37,18 @@ python3 -m pip install -U pytest-timeout | |||
| python3 -m pip install pyroma | ||||
| 
 | ||||
| if [[ $(uname) != CYGWIN* ]]; then | ||||
|     # TODO Update condition when NumPy supports free-threading | ||||
|     if [[ "$PYTHON_GIL" == "0" ]]; then | ||||
|         python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple | ||||
|     else | ||||
|         python3 -m pip install numpy | ||||
|     fi | ||||
| 
 | ||||
|     # PyQt6 doesn't support PyPy3 | ||||
|     if [[ $GHA_PYTHON_VERSION == 3.* ]]; then | ||||
|         sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 | ||||
|         python3 -m pip install pyqt6 | ||||
|         # TODO Update condition when pyqt6 supports free-threading | ||||
|         if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi | ||||
|     fi | ||||
| 
 | ||||
|     # Pyroma uses non-isolated build and fails with old setuptools | ||||
|  |  | |||
							
								
								
									
										30
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										30
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -50,26 +50,24 @@ jobs: | |||
|           "3.9", | ||||
|         ] | ||||
|         include: | ||||
|         - python-version: "3.11" | ||||
|           PYTHONOPTIMIZE: 1 | ||||
|           REVERSE: "--reverse" | ||||
|         - python-version: "3.10" | ||||
|           PYTHONOPTIMIZE: 2 | ||||
|         - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } | ||||
|         - { python-version: "3.10", PYTHONOPTIMIZE: 2 } | ||||
|         # Free-threaded | ||||
|         - { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true } | ||||
|         # M1 only available for 3.10+ | ||||
|         - os: "macos-13" | ||||
|           python-version: "3.9" | ||||
|         - { os: "macos-13", python-version: "3.9" } | ||||
|         exclude: | ||||
|         - os: "macos-14" | ||||
|           python-version: "3.9" | ||||
|         - { os: "macos-14", python-version: "3.9" } | ||||
| 
 | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     name: ${{ matrix.os }} Python ${{ matrix.python-version }} | ||||
|     name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }} | ||||
| 
 | ||||
|     steps: | ||||
|     - uses: actions/checkout@v4 | ||||
| 
 | ||||
|     - name: Set up Python ${{ matrix.python-version }} | ||||
|       uses: actions/setup-python@v5 | ||||
|       if: "${{ !matrix.disable-gil }}" | ||||
|       with: | ||||
|         python-version: ${{ matrix.python-version }} | ||||
|         allow-prereleases: true | ||||
|  | @ -78,6 +76,18 @@ jobs: | |||
|           ".ci/*.sh" | ||||
|           "pyproject.toml" | ||||
| 
 | ||||
|     - name: Set up Python ${{ matrix.python-version }} (free-threaded) | ||||
|       uses: deadsnakes/action@v3.1.0 | ||||
|       if: "${{ matrix.disable-gil }}" | ||||
|       with: | ||||
|         python-version: ${{ matrix.python-version }} | ||||
|         nogil: ${{ matrix.disable-gil }} | ||||
| 
 | ||||
|     - name: Set PYTHON_GIL | ||||
|       if: "${{ matrix.disable-gil }}" | ||||
|       run: | | ||||
|         echo "PYTHON_GIL=0" >> $GITHUB_ENV | ||||
| 
 | ||||
|     - name: Build system information | ||||
|       run: python3 .github/workflows/system-info.py | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										6
									
								
								.github/workflows/wheels-test.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/wheels-test.sh
									
									
									
									
										vendored
									
									
								
							|  | @ -12,9 +12,15 @@ elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then | |||
| else | ||||
|     yum install -y fribidi | ||||
| fi | ||||
| 
 | ||||
| if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then | ||||
|   # TODO Update condition when NumPy supports free-threading | ||||
|   if [ $(python3 -c "import sysconfig;print(sysconfig.get_config_var('Py_GIL_DISABLED'))") == "1" ]; then | ||||
|     python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple | ||||
|   else | ||||
|     python3 -m pip install numpy | ||||
|   fi | ||||
| fi | ||||
| 
 | ||||
| if [ ! -d "test-images-main" ]; then | ||||
|     curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip | ||||
|  |  | |||
							
								
								
									
										9
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -41,11 +41,8 @@ jobs: | |||
|         python-version: | ||||
|           - pp39 | ||||
|           - pp310 | ||||
|           - cp39 | ||||
|           - cp310 | ||||
|           - cp311 | ||||
|           - cp312 | ||||
|           - cp313 | ||||
|           - cp3{9,10,11} | ||||
|           - cp3{12,13} | ||||
|         spec: | ||||
|           - manylinux2014 | ||||
|           - manylinux_2_28 | ||||
|  | @ -132,6 +129,7 @@ jobs: | |||
|         env: | ||||
|           CIBW_ARCHS: ${{ matrix.cibw_arch }} | ||||
|           CIBW_BUILD: ${{ matrix.build }} | ||||
|           CIBW_FREE_THREADED_SUPPORT: True | ||||
|           CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} | ||||
|           CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} | ||||
|           CIBW_PRERELEASE_PYTHONS: True | ||||
|  | @ -204,6 +202,7 @@ jobs: | |||
|           CIBW_ARCHS: ${{ matrix.cibw_arch }} | ||||
|           CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" | ||||
|           CIBW_CACHE_PATH: "C:\\cibw" | ||||
|           CIBW_FREE_THREADED_SUPPORT: True | ||||
|           CIBW_PRERELEASE_PYTHONS: True | ||||
|           CIBW_TEST_SKIP: "*-win_arm64" | ||||
|           CIBW_TEST_COMMAND: 'docker run --rm | ||||
|  |  | |||
							
								
								
									
										15
									
								
								CHANGES.rst
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								CHANGES.rst
									
									
									
									
									
								
							|  | @ -2,6 +2,21 @@ | |||
| Changelog (Pillow) | ||||
| ================== | ||||
| 
 | ||||
| 11.0.0 (unreleased) | ||||
| ------------------- | ||||
| 
 | ||||
| - Drop support for Python 3.8 #8183 | ||||
|   [hugovk, radarhere] | ||||
| 
 | ||||
| - Add support for Python 3.13 #8181 | ||||
|   [hugovk, radarhere] | ||||
| 
 | ||||
| - Fix incompatibility with NumPy 1.20 #8187 | ||||
|   [neutrinoceros, radarhere] | ||||
| 
 | ||||
| - Remove PSFile, PyAccess and USE_CFFI_ACCESS #8182 | ||||
|   [hugovk, radarhere] | ||||
| 
 | ||||
| 10.4.0 (2024-07-01) | ||||
| ------------------- | ||||
| 
 | ||||
|  |  | |||
|  | @ -60,9 +60,7 @@ def convert_to_comparable( | |||
|     return new_a, new_b | ||||
| 
 | ||||
| 
 | ||||
| def assert_deep_equal( | ||||
|     a: Sequence[Any], b: Sequence[Any], msg: str | None = None | ||||
| ) -> None: | ||||
| def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None: | ||||
|     try: | ||||
|         assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" | ||||
|     except Exception: | ||||
|  |  | |||
|  | @ -401,7 +401,7 @@ def test_palette_434(tmp_path: Path) -> None: | |||
| 
 | ||||
|     def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: | ||||
|         out = str(tmp_path / "temp.gif") | ||||
|         im.copy().save(out, **kwargs) | ||||
|         im.copy().save(out, "GIF", **kwargs) | ||||
|         reloaded = Image.open(out) | ||||
| 
 | ||||
|         return reloaded | ||||
|  |  | |||
|  | @ -92,11 +92,22 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|     def test_g4_non_disk_file_object(self, tmp_path: Path) -> None: | ||||
|         """Testing loading from non-disk non-BytesIO file object""" | ||||
|         test_file = "Tests/images/hopper_g4_500.tif" | ||||
|         s = io.BytesIO() | ||||
|         with open(test_file, "rb") as f: | ||||
|             s.write(f.read()) | ||||
|             s.seek(0) | ||||
|         r = io.BufferedReader(s) | ||||
|             data = f.read() | ||||
| 
 | ||||
|         class NonBytesIO(io.RawIOBase): | ||||
|             def read(self, size: int = -1) -> bytes: | ||||
|                 nonlocal data | ||||
|                 if size == -1: | ||||
|                     size = len(data) | ||||
|                 result = data[:size] | ||||
|                 data = data[size:] | ||||
|                 return result | ||||
| 
 | ||||
|             def readable(self) -> bool: | ||||
|                 return True | ||||
| 
 | ||||
|         r = io.BufferedReader(NonBytesIO()) | ||||
|         with Image.open(r) as im: | ||||
|             assert im.size == (500, 500) | ||||
|             self._assert_noerr(tmp_path, im) | ||||
|  | @ -1048,7 +1059,11 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|         ], | ||||
|     ) | ||||
|     def test_wrong_bits_per_sample( | ||||
|         self, file_name: str, mode: str, size: tuple[int, int], tile | ||||
|         self, | ||||
|         file_name: str, | ||||
|         mode: str, | ||||
|         size: tuple[int, int], | ||||
|         tile: list[tuple[str, tuple[int, int, int, int], int, tuple[Any, ...]]], | ||||
|     ) -> None: | ||||
|         with Image.open("Tests/images/" + file_name) as im: | ||||
|             assert im.mode == mode | ||||
|  | @ -1135,7 +1150,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|             arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} | ||||
|             if argument: | ||||
|                 arguments["strip_size"] = 2**18 | ||||
|             im.save(out, **arguments) | ||||
|             im.save(out, "TIFF", **arguments) | ||||
| 
 | ||||
|             with Image.open(out) as im: | ||||
|                 assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|  |  | |||
|  | @ -2,11 +2,11 @@ from __future__ import annotations | |||
| 
 | ||||
| import warnings | ||||
| from io import BytesIO | ||||
| from typing import Any, cast | ||||
| from typing import Any | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, MpoImagePlugin | ||||
| from PIL import Image, ImageFile, MpoImagePlugin | ||||
| 
 | ||||
| from .helper import ( | ||||
|     assert_image_equal, | ||||
|  | @ -20,11 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] | |||
| pytestmark = skip_unless_feature("jpg") | ||||
| 
 | ||||
| 
 | ||||
| def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile: | ||||
| def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile: | ||||
|     out = BytesIO() | ||||
|     im.save(out, "MPO", **options) | ||||
|     out.seek(0) | ||||
|     return cast(MpoImagePlugin.MpoImageFile, Image.open(out)) | ||||
|     return Image.open(out) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("test_file", test_files) | ||||
|  | @ -226,6 +226,12 @@ def test_eoferror() -> None: | |||
|         im.seek(n_frames - 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_adopt_jpeg() -> None: | ||||
|     with Image.open("Tests/images/hopper.jpg") as im: | ||||
|         with pytest.raises(ValueError): | ||||
|             MpoImagePlugin.MpoImageFile.adopt(im) | ||||
| 
 | ||||
| 
 | ||||
| def test_ultra_hdr() -> None: | ||||
|     with Image.open("Tests/images/ultrahdr.jpg") as im: | ||||
|         assert im.format == "JPEG" | ||||
|  | @ -275,6 +281,8 @@ def test_save_all() -> None: | |||
|     im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) | ||||
| 
 | ||||
|     assert_image_equal(im, im_reloaded) | ||||
|     assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile) | ||||
|     assert im_reloaded.mpinfo is not None | ||||
|     assert im_reloaded.mpinfo[45056] == b"0100" | ||||
| 
 | ||||
|     im_reloaded.seek(1) | ||||
|  |  | |||
|  | @ -118,7 +118,7 @@ def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: | |||
|     im = hopper() | ||||
| 
 | ||||
|     outfile = str(tmp_path / "temp.pdf") | ||||
|     im.save(outfile, **params) | ||||
|     im.save(outfile, "PDF", **params) | ||||
| 
 | ||||
|     with open(outfile, "rb") as fp: | ||||
|         contents = fp.read() | ||||
|  | @ -271,6 +271,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None: | |||
| 
 | ||||
| 
 | ||||
| def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None: | ||||
|     assert pdf.pages_ref is not None | ||||
|     pages_info = pdf.read_indirect(pdf.pages_ref) | ||||
|     assert b"Parent" not in pages_info | ||||
|     assert b"Kids" in pages_info | ||||
|  |  | |||
|  | @ -41,7 +41,7 @@ MAGIC = PngImagePlugin._MAGIC | |||
| 
 | ||||
| def chunk(cid: bytes, *data: bytes) -> bytes: | ||||
|     test_file = BytesIO() | ||||
|     PngImagePlugin.putchunk(*(test_file, cid) + data) | ||||
|     PngImagePlugin.putchunk(test_file, cid, *data) | ||||
|     return test_file.getvalue() | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -78,6 +78,7 @@ class TestFileTiff: | |||
| 
 | ||||
|     def test_seek_after_close(self) -> None: | ||||
|         im = Image.open("Tests/images/multipage.tiff") | ||||
|         assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|         im.close() | ||||
| 
 | ||||
|         with pytest.raises(ValueError): | ||||
|  | @ -424,13 +425,13 @@ class TestFileTiff: | |||
|     def test_load_float(self) -> None: | ||||
|         ifd = TiffImagePlugin.ImageFileDirectory_v2() | ||||
|         data = b"abcdabcd" | ||||
|         ret = ifd.load_float(data, False) | ||||
|         ret = getattr(ifd, "load_float")(data, False) | ||||
|         assert ret == (1.6777999408082104e22, 1.6777999408082104e22) | ||||
| 
 | ||||
|     def test_load_double(self) -> None: | ||||
|         ifd = TiffImagePlugin.ImageFileDirectory_v2() | ||||
|         data = b"abcdefghabcdefgh" | ||||
|         ret = ifd.load_double(data, False) | ||||
|         ret = getattr(ifd, "load_double")(data, False) | ||||
|         assert ret == (8.540883223036124e194, 8.540883223036124e194) | ||||
| 
 | ||||
|     def test_ifd_tag_type(self) -> None: | ||||
|  | @ -599,7 +600,7 @@ class TestFileTiff: | |||
|     def test_with_underscores(self, tmp_path: Path) -> None: | ||||
|         kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} | ||||
|         filename = str(tmp_path / "temp.tif") | ||||
|         hopper("RGB").save(filename, **kwargs) | ||||
|         hopper("RGB").save(filename, "TIFF", **kwargs) | ||||
|         with Image.open(filename) as im: | ||||
|             # legacy interface | ||||
|             assert im.tag[X_RESOLUTION][0][0] == 72 | ||||
|  | @ -624,7 +625,9 @@ class TestFileTiff: | |||
|     def test_iptc(self, tmp_path: Path) -> None: | ||||
|         # Do not preserve IPTC_NAA_CHUNK by default if type is LONG | ||||
|         outfile = str(tmp_path / "temp.tif") | ||||
|         im = hopper() | ||||
|         with Image.open("Tests/images/hopper.tif") as im: | ||||
|             im.load() | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             ifd = TiffImagePlugin.ImageFileDirectory_v2() | ||||
|             ifd[33723] = 1 | ||||
|             ifd.tagtype[33723] = 4 | ||||
|  | @ -632,6 +635,7 @@ class TestFileTiff: | |||
|             im.save(outfile) | ||||
| 
 | ||||
|         with Image.open(outfile) as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             assert 33723 not in im.tag_v2 | ||||
| 
 | ||||
|     def test_rowsperstrip(self, tmp_path: Path) -> None: | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| from collections.abc import Generator | ||||
| from pathlib import Path | ||||
| 
 | ||||
| import pytest | ||||
|  | @ -96,7 +97,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None: | |||
|             check(temp_file1) | ||||
| 
 | ||||
|             # Tests appending using a generator | ||||
|             def im_generator(ims): | ||||
|             def im_generator( | ||||
|                 ims: list[Image.Image], | ||||
|             ) -> Generator[Image.Image, None, None]: | ||||
|                 yield from ims | ||||
| 
 | ||||
|             temp_file2 = str(tmp_path / "temp_generator.webp") | ||||
|  |  | |||
|  | @ -372,8 +372,9 @@ class TestImage: | |||
|         img = Image.alpha_composite(dst, src) | ||||
| 
 | ||||
|         # Assert | ||||
|         img_colors = sorted(img.getcolors()) | ||||
|         assert img_colors == expected_colors | ||||
|         img_colors = img.getcolors() | ||||
|         assert img_colors is not None | ||||
|         assert sorted(img_colors) == expected_colors | ||||
| 
 | ||||
|     def test_alpha_inplace(self) -> None: | ||||
|         src = Image.new("RGBA", (128, 128), "blue") | ||||
|  | @ -670,7 +671,9 @@ class TestImage: | |||
| 
 | ||||
|         im_remapped = im.remap_palette([1, 0]) | ||||
|         assert im_remapped.info["transparency"] == 1 | ||||
|         assert len(im_remapped.getpalette()) == 6 | ||||
|         palette = im_remapped.getpalette() | ||||
|         assert palette is not None | ||||
|         assert len(palette) == 6 | ||||
| 
 | ||||
|         # Test unused transparency | ||||
|         im.info["transparency"] = 2 | ||||
|  | @ -701,7 +704,7 @@ class TestImage: | |||
|             else: | ||||
|                 assert new_image.palette is None | ||||
| 
 | ||||
|         _make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3)) | ||||
|         _make_new(im, im_p, ImagePalette.ImagePalette("RGB")) | ||||
|         _make_new(im_p, im, None) | ||||
|         _make_new(im, blank_p, ImagePalette.ImagePalette()) | ||||
|         _make_new(im, blank_pa, ImagePalette.ImagePalette()) | ||||
|  |  | |||
|  | @ -27,7 +27,9 @@ class TestImagePutPixel: | |||
|         for y in range(im1.size[1]): | ||||
|             for x in range(im1.size[0]): | ||||
|                 pos = x, y | ||||
|                 im2.putpixel(pos, im1.getpixel(pos)) | ||||
|                 value = im1.getpixel(pos) | ||||
|                 assert value is not None | ||||
|                 im2.putpixel(pos, value) | ||||
| 
 | ||||
|         assert_image_equal(im1, im2) | ||||
| 
 | ||||
|  | @ -37,7 +39,9 @@ class TestImagePutPixel: | |||
|         for y in range(im1.size[1]): | ||||
|             for x in range(im1.size[0]): | ||||
|                 pos = x, y | ||||
|                 im2.putpixel(pos, im1.getpixel(pos)) | ||||
|                 value = im1.getpixel(pos) | ||||
|                 assert value is not None | ||||
|                 im2.putpixel(pos, value) | ||||
| 
 | ||||
|         assert not im2.readonly | ||||
|         assert_image_equal(im1, im2) | ||||
|  | @ -50,9 +54,9 @@ class TestImagePutPixel: | |||
|         assert pix1 is not None | ||||
|         assert pix2 is not None | ||||
|         with pytest.raises(TypeError): | ||||
|             pix1[0, "0"] | ||||
|             pix1[0, "0"]  # type: ignore[index] | ||||
|         with pytest.raises(TypeError): | ||||
|             pix1["0", 0] | ||||
|             pix1["0", 0]  # type: ignore[index] | ||||
| 
 | ||||
|         for y in range(im1.size[1]): | ||||
|             for x in range(im1.size[0]): | ||||
|  | @ -71,7 +75,9 @@ class TestImagePutPixel: | |||
|         for y in range(-1, -im1.size[1] - 1, -1): | ||||
|             for x in range(-1, -im1.size[0] - 1, -1): | ||||
|                 pos = x, y | ||||
|                 im2.putpixel(pos, im1.getpixel(pos)) | ||||
|                 value = im1.getpixel(pos) | ||||
|                 assert value is not None | ||||
|                 im2.putpixel(pos, value) | ||||
| 
 | ||||
|         assert_image_equal(im1, im2) | ||||
| 
 | ||||
|  | @ -81,7 +87,9 @@ class TestImagePutPixel: | |||
|         for y in range(-1, -im1.size[1] - 1, -1): | ||||
|             for x in range(-1, -im1.size[0] - 1, -1): | ||||
|                 pos = x, y | ||||
|                 im2.putpixel(pos, im1.getpixel(pos)) | ||||
|                 value = im1.getpixel(pos) | ||||
|                 assert value is not None | ||||
|                 im2.putpixel(pos, value) | ||||
| 
 | ||||
|         assert not im2.readonly | ||||
|         assert_image_equal(im1, im2) | ||||
|  | @ -219,7 +227,7 @@ class TestImagePutPixelError: | |||
|         im = hopper(mode) | ||||
|         for v in self.INVALID_TYPES: | ||||
|             with pytest.raises(TypeError, match="color must be int or tuple"): | ||||
|                 im.putpixel((0, 0), v) | ||||
|                 im.putpixel((0, 0), v)  # type: ignore[arg-type] | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         ("mode", "band_numbers", "match"), | ||||
|  | @ -253,7 +261,7 @@ class TestImagePutPixelError: | |||
|             with pytest.raises( | ||||
|                 TypeError, match="color must be int or single-element tuple" | ||||
|             ): | ||||
|                 im.putpixel((0, 0), v) | ||||
|                 im.putpixel((0, 0), v)  # type: ignore[arg-type] | ||||
| 
 | ||||
|     @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) | ||||
|     def test_putpixel_overflow_error(self, mode: str) -> None: | ||||
|  |  | |||
|  | @ -225,7 +225,7 @@ def test_l_macro_rounding(convert_mode: str) -> None: | |||
|         assert px is not None | ||||
|         converted_color = px[0, 0] | ||||
|         if convert_mode == "LA": | ||||
|             assert converted_color is not None | ||||
|             assert isinstance(converted_color, tuple) | ||||
|             converted_color = converted_color[0] | ||||
|         assert converted_color == 1 | ||||
| 
 | ||||
|  |  | |||
|  | @ -54,17 +54,21 @@ def test_pack() -> None: | |||
|     assert A is None | ||||
| 
 | ||||
|     A = im.getcolors(maxcolors=3) | ||||
|     assert A is not None | ||||
|     A.sort() | ||||
|     assert A == expected | ||||
| 
 | ||||
|     A = im.getcolors(maxcolors=4) | ||||
|     assert A is not None | ||||
|     A.sort() | ||||
|     assert A == expected | ||||
| 
 | ||||
|     A = im.getcolors(maxcolors=8) | ||||
|     assert A is not None | ||||
|     A.sort() | ||||
|     assert A == expected | ||||
| 
 | ||||
|     A = im.getcolors(maxcolors=16) | ||||
|     assert A is not None | ||||
|     A.sort() | ||||
|     assert A == expected | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ def test_sanity() -> None: | |||
| 
 | ||||
| def test_long_integers() -> None: | ||||
|     # see bug-200802-systemerror | ||||
|     def put(value: int) -> tuple[int, int, int, int]: | ||||
|     def put(value: int) -> float | tuple[int, ...] | None: | ||||
|         im = Image.new("RGBA", (1, 1)) | ||||
|         im.putdata([value]) | ||||
|         return im.getpixel((0, 0)) | ||||
|  |  | |||
|  | @ -31,7 +31,9 @@ def test_libimagequant_quantize() -> None: | |||
|     converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) | ||||
|     assert converted.mode == "P" | ||||
|     assert_image_similar(converted.convert("RGB"), image, 15) | ||||
|     assert len(converted.getcolors()) == 100 | ||||
|     colors = converted.getcolors() | ||||
|     assert colors is not None | ||||
|     assert len(colors) == 100 | ||||
| 
 | ||||
| 
 | ||||
| def test_octree_quantize() -> None: | ||||
|  | @ -39,7 +41,9 @@ def test_octree_quantize() -> None: | |||
|     converted = image.quantize(100, Image.Quantize.FASTOCTREE) | ||||
|     assert converted.mode == "P" | ||||
|     assert_image_similar(converted.convert("RGB"), image, 20) | ||||
|     assert len(converted.getcolors()) == 100 | ||||
|     colors = converted.getcolors() | ||||
|     assert colors is not None | ||||
|     assert len(colors) == 100 | ||||
| 
 | ||||
| 
 | ||||
| def test_rgba_quantize() -> None: | ||||
|  | @ -158,4 +162,6 @@ def test_small_palette() -> None: | |||
|     im = im.quantize(palette=p) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert len(im.getcolors()) == 2 | ||||
|     quantized_colors = im.getcolors() | ||||
|     assert quantized_colors is not None | ||||
|     assert len(quantized_colors) == 2 | ||||
|  |  | |||
|  | @ -237,13 +237,13 @@ class TestImagingCoreResampleAccuracy: | |||
| class TestCoreResampleConsistency: | ||||
|     def make_case( | ||||
|         self, mode: str, fill: tuple[int, int, int] | float | ||||
|     ) -> tuple[Image.Image, tuple[int, ...]]: | ||||
|     ) -> tuple[Image.Image, float | tuple[int, ...]]: | ||||
|         im = Image.new(mode, (512, 9), fill) | ||||
|         px = im.load() | ||||
|         assert px is not None | ||||
|         return im.resize((9, 512), Image.Resampling.LANCZOS), px[0, 0] | ||||
| 
 | ||||
|     def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None: | ||||
|     def run_case(self, case: tuple[Image.Image, float | tuple[int, ...]]) -> None: | ||||
|         channel, color = case | ||||
|         px = channel.load() | ||||
|         assert px is not None | ||||
|  | @ -256,6 +256,7 @@ class TestCoreResampleConsistency: | |||
|     def test_8u(self) -> None: | ||||
|         im, color = self.make_case("RGB", (0, 64, 255)) | ||||
|         r, g, b = im.split() | ||||
|         assert isinstance(color, tuple) | ||||
|         self.run_case((r, color[0])) | ||||
|         self.run_case((g, color[1])) | ||||
|         self.run_case((b, color[2])) | ||||
|  | @ -290,7 +291,11 @@ class TestCoreResampleAlphaCorrect: | |||
|         px = i.load() | ||||
|         assert px is not None | ||||
|         for y in range(i.size[1]): | ||||
|             used_colors = {px[x, y][0] for x in range(i.size[0])} | ||||
|             used_colors = set() | ||||
|             for x in range(i.size[0]): | ||||
|                 value = px[x, y] | ||||
|                 assert isinstance(value, tuple) | ||||
|                 used_colors.add(value[0]) | ||||
|             assert 256 == len(used_colors), ( | ||||
|                 "All colors should be present in resized image. " | ||||
|                 f"Only {len(used_colors)} on line {y}." | ||||
|  | @ -332,12 +337,13 @@ class TestCoreResampleAlphaCorrect: | |||
|         assert px is not None | ||||
|         for y in range(i.size[1]): | ||||
|             for x in range(i.size[0]): | ||||
|                 if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel: | ||||
|                 value = px[x, y] | ||||
|                 assert isinstance(value, tuple) | ||||
|                 if value[-1] != 0 and value[:-1] != clean_pixel: | ||||
|                     message = ( | ||||
|                         f"pixel at ({x}, {y}) is different:\n" | ||||
|                         f"{px[x, y]}\n{clean_pixel}" | ||||
|                         f"pixel at ({x}, {y}) is different:\n{value}\n{clean_pixel}" | ||||
|                     ) | ||||
|                     assert px[x, y][:3] == clean_pixel, message | ||||
|                     assert value[:3] == clean_pixel, message | ||||
| 
 | ||||
|     def test_dirty_pixels_rgba(self) -> None: | ||||
|         case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) | ||||
|  |  | |||
|  | @ -285,14 +285,14 @@ class TestReducingGapResize: | |||
| 
 | ||||
| class TestImageResize: | ||||
|     def test_resize(self) -> None: | ||||
|         def resize(mode: str, size: tuple[int, int]) -> None: | ||||
|         def resize(mode: str, size: tuple[int, int] | list[int]) -> None: | ||||
|             out = hopper(mode).resize(size) | ||||
|             assert out.mode == mode | ||||
|             assert out.size == size | ||||
|             assert out.size == tuple(size) | ||||
| 
 | ||||
|         for mode in "1", "P", "L", "RGB", "I", "F": | ||||
|             resize(mode, (112, 103)) | ||||
|             resize(mode, (188, 214)) | ||||
|             resize(mode, [188, 214]) | ||||
| 
 | ||||
|         # Test unknown resampling filter | ||||
|         with hopper() as im: | ||||
|  |  | |||
|  | @ -192,8 +192,9 @@ class TestImageTransform: | |||
| 
 | ||||
|         im = op(im, (40, 10)) | ||||
| 
 | ||||
|         colors = sorted(im.getcolors()) | ||||
|         assert colors == sorted( | ||||
|         colors = im.getcolors() | ||||
|         assert colors is not None | ||||
|         assert sorted(colors) == sorted( | ||||
|             ( | ||||
|                 (20 * 10, opaque), | ||||
|                 (20 * 10, transparent), | ||||
|  |  | |||
|  | @ -391,23 +391,25 @@ def test_overlay() -> None: | |||
| def test_logical() -> None: | ||||
|     def table( | ||||
|         op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int | ||||
|     ) -> tuple[int, int, int, int]: | ||||
|     ) -> list[float]: | ||||
|         out = [] | ||||
|         for x in (a, b): | ||||
|             imx = Image.new("1", (1, 1), x) | ||||
|             for y in (a, b): | ||||
|                 imy = Image.new("1", (1, 1), y) | ||||
|                 out.append(op(imx, imy).getpixel((0, 0))) | ||||
|         return tuple(out) | ||||
|                 value = op(imx, imy).getpixel((0, 0)) | ||||
|                 assert not isinstance(value, tuple) and value is not None | ||||
|                 out.append(value) | ||||
|         return out | ||||
| 
 | ||||
|     assert table(ImageChops.logical_and, 0, 1) == (0, 0, 0, 255) | ||||
|     assert table(ImageChops.logical_or, 0, 1) == (0, 255, 255, 255) | ||||
|     assert table(ImageChops.logical_xor, 0, 1) == (0, 255, 255, 0) | ||||
|     assert table(ImageChops.logical_and, 0, 1) == [0, 0, 0, 255] | ||||
|     assert table(ImageChops.logical_or, 0, 1) == [0, 255, 255, 255] | ||||
|     assert table(ImageChops.logical_xor, 0, 1) == [0, 255, 255, 0] | ||||
| 
 | ||||
|     assert table(ImageChops.logical_and, 0, 128) == (0, 0, 0, 255) | ||||
|     assert table(ImageChops.logical_or, 0, 128) == (0, 255, 255, 255) | ||||
|     assert table(ImageChops.logical_xor, 0, 128) == (0, 255, 255, 0) | ||||
|     assert table(ImageChops.logical_and, 0, 128) == [0, 0, 0, 255] | ||||
|     assert table(ImageChops.logical_or, 0, 128) == [0, 255, 255, 255] | ||||
|     assert table(ImageChops.logical_xor, 0, 128) == [0, 255, 255, 0] | ||||
| 
 | ||||
|     assert table(ImageChops.logical_and, 0, 255) == (0, 0, 0, 255) | ||||
|     assert table(ImageChops.logical_or, 0, 255) == (0, 255, 255, 255) | ||||
|     assert table(ImageChops.logical_xor, 0, 255) == (0, 255, 255, 0) | ||||
|     assert table(ImageChops.logical_and, 0, 255) == [0, 0, 0, 255] | ||||
|     assert table(ImageChops.logical_or, 0, 255) == [0, 255, 255, 255] | ||||
|     assert table(ImageChops.logical_xor, 0, 255) == [0, 255, 255, 0] | ||||
|  |  | |||
|  | @ -691,7 +691,9 @@ def test_rgb_lab(mode: str) -> None: | |||
| 
 | ||||
|     im = Image.new("LAB", (1, 1), (255, 0, 0)) | ||||
|     converted_im = im.convert(mode) | ||||
|     assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) | ||||
|     value = converted_im.getpixel((0, 0)) | ||||
|     assert isinstance(value, tuple) | ||||
|     assert value[:3] == (0, 255, 255) | ||||
| 
 | ||||
| 
 | ||||
| def test_deprecation() -> None: | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| import contextlib | ||||
| import os.path | ||||
| from collections.abc import Sequence | ||||
| from typing import Callable | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
|  | @ -1422,25 +1422,44 @@ def test_default_font_size() -> None: | |||
| 
 | ||||
|     im = Image.new("RGB", (220, 25)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): | ||||
| 
 | ||||
|     def check(func: Callable[[], None]) -> None: | ||||
|         if freetype_support: | ||||
|             func() | ||||
|         else: | ||||
|             with pytest.raises(ImportError): | ||||
|                 func() | ||||
| 
 | ||||
|     def draw_text() -> None: | ||||
|         draw.text((0, 0), text, font_size=16) | ||||
|         assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") | ||||
| 
 | ||||
|     with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): | ||||
|     check(draw_text) | ||||
| 
 | ||||
|     def draw_textlength() -> None: | ||||
|         assert draw.textlength(text, font_size=16) == 216 | ||||
| 
 | ||||
|     with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): | ||||
|     check(draw_textlength) | ||||
| 
 | ||||
|     def draw_textbbox() -> None: | ||||
|         assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19) | ||||
| 
 | ||||
|     check(draw_textbbox) | ||||
| 
 | ||||
|     im = Image.new("RGB", (220, 25)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): | ||||
| 
 | ||||
|     def draw_multiline_text() -> None: | ||||
|         draw.multiline_text((0, 0), text, font_size=16) | ||||
|         assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png") | ||||
| 
 | ||||
|     with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError): | ||||
|     check(draw_multiline_text) | ||||
| 
 | ||||
|     def draw_multiline_textbbox() -> None: | ||||
|         assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19) | ||||
| 
 | ||||
|     check(draw_multiline_textbbox) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("bbox", BBOX) | ||||
| def test_same_color_outline(bbox: Coords) -> None: | ||||
|  |  | |||
|  | @ -90,6 +90,7 @@ class TestImageFile: | |||
|             data = f.read() | ||||
|         with ImageFile.Parser() as p: | ||||
|             p.feed(data) | ||||
|             assert p.image is not None | ||||
|             assert (48, 48) == p.image.size | ||||
| 
 | ||||
|     @skip_unless_feature("webp") | ||||
|  | @ -103,6 +104,7 @@ class TestImageFile: | |||
|                 assert not p.image | ||||
| 
 | ||||
|                 p.feed(f.read()) | ||||
|             assert p.image is not None | ||||
|             assert (128, 128) == p.image.size | ||||
| 
 | ||||
|     @skip_unless_feature("zlib") | ||||
|  | @ -125,7 +127,7 @@ class TestImageFile: | |||
|     def test_raise_typeerror(self) -> None: | ||||
|         with pytest.raises(TypeError): | ||||
|             parser = ImageFile.Parser() | ||||
|             parser.feed(1) | ||||
|             parser.feed(1)  # type: ignore[arg-type] | ||||
| 
 | ||||
|     def test_negative_stride(self) -> None: | ||||
|         with open("Tests/images/raw_negative_stride.bin", "rb") as f: | ||||
|  | @ -303,9 +305,9 @@ class TestPyDecoder(CodecsTest): | |||
|             im.load() | ||||
| 
 | ||||
|     def test_decode(self) -> None: | ||||
|         decoder = ImageFile.PyDecoder(None) | ||||
|         decoder = ImageFile.PyDecoder("") | ||||
|         with pytest.raises(NotImplementedError): | ||||
|             decoder.decode(None) | ||||
|             decoder.decode(b"") | ||||
| 
 | ||||
| 
 | ||||
| class TestPyEncoder(CodecsTest): | ||||
|  | @ -381,7 +383,7 @@ class TestPyEncoder(CodecsTest): | |||
|             ) | ||||
| 
 | ||||
|     def test_encode(self) -> None: | ||||
|         encoder = ImageFile.PyEncoder(None) | ||||
|         encoder = ImageFile.PyEncoder("") | ||||
|         with pytest.raises(NotImplementedError): | ||||
|             encoder.encode(0) | ||||
| 
 | ||||
|  | @ -393,8 +395,9 @@ class TestPyEncoder(CodecsTest): | |||
|         with pytest.raises(NotImplementedError): | ||||
|             encoder.encode_to_pyfd() | ||||
| 
 | ||||
|         fh = BytesIO() | ||||
|         with pytest.raises(NotImplementedError): | ||||
|             encoder.encode_to_file(None, None) | ||||
|             encoder.encode_to_file(fh, 0) | ||||
| 
 | ||||
|     def test_zero_height(self) -> None: | ||||
|         with pytest.raises(UnidentifiedImageError): | ||||
|  |  | |||
|  | @ -60,6 +60,8 @@ class TestImageGrab: | |||
|     def test_grabclipboard(self) -> None: | ||||
|         if sys.platform == "darwin": | ||||
|             subprocess.call(["screencapture", "-cx"]) | ||||
| 
 | ||||
|             ImageGrab.grabclipboard() | ||||
|         elif sys.platform == "win32": | ||||
|             p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) | ||||
|             p.stdin.write( | ||||
|  | @ -69,6 +71,8 @@ $bmp = New-Object Drawing.Bitmap 200, 200 | |||
| [Windows.Forms.Clipboard]::SetImage($bmp)""" | ||||
|             ) | ||||
|             p.communicate() | ||||
| 
 | ||||
|             ImageGrab.grabclipboard() | ||||
|         else: | ||||
|             if not shutil.which("wl-paste") and not shutil.which("xclip"): | ||||
|                 with pytest.raises( | ||||
|  | @ -77,9 +81,6 @@ $bmp = New-Object Drawing.Bitmap 200, 200 | |||
|                     r" ImageGrab.grabclipboard\(\) on Linux", | ||||
|                 ): | ||||
|                     ImageGrab.grabclipboard() | ||||
|             return | ||||
| 
 | ||||
|         ImageGrab.grabclipboard() | ||||
| 
 | ||||
|     @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") | ||||
|     def test_grabclipboard_file(self) -> None: | ||||
|  |  | |||
|  | @ -41,11 +41,15 @@ A = string_to_img( | |||
| def img_to_string(im: Image.Image) -> str: | ||||
|     """Turn a (small) binary image into a string representation""" | ||||
|     chars = ".1" | ||||
|     width, height = im.size | ||||
|     return "\n".join( | ||||
|         "".join(chars[im.getpixel((c, r)) > 0] for c in range(width)) | ||||
|         for r in range(height) | ||||
|     ) | ||||
|     result = [] | ||||
|     for r in range(im.height): | ||||
|         line = "" | ||||
|         for c in range(im.width): | ||||
|             value = im.getpixel((c, r)) | ||||
|             assert not isinstance(value, tuple) and value is not None | ||||
|             line += chars[value > 0] | ||||
|         result.append(line) | ||||
|     return "\n".join(result) | ||||
| 
 | ||||
| 
 | ||||
| def img_string_normalize(im: str) -> str: | ||||
|  |  | |||
|  | @ -259,20 +259,26 @@ def test_colorize_2color() -> None: | |||
|     left = (0, 1) | ||||
|     middle = (127, 1) | ||||
|     right = (255, 1) | ||||
|     value = im_test.getpixel(left) | ||||
|     assert isinstance(value, tuple) | ||||
|     assert_tuple_approx_equal( | ||||
|         im_test.getpixel(left), | ||||
|         value, | ||||
|         (255, 0, 0), | ||||
|         threshold=1, | ||||
|         msg="black test pixel incorrect", | ||||
|     ) | ||||
|     value = im_test.getpixel(middle) | ||||
|     assert isinstance(value, tuple) | ||||
|     assert_tuple_approx_equal( | ||||
|         im_test.getpixel(middle), | ||||
|         value, | ||||
|         (127, 63, 0), | ||||
|         threshold=1, | ||||
|         msg="mid test pixel incorrect", | ||||
|     ) | ||||
|     value = im_test.getpixel(right) | ||||
|     assert isinstance(value, tuple) | ||||
|     assert_tuple_approx_equal( | ||||
|         im_test.getpixel(right), | ||||
|         value, | ||||
|         (0, 127, 0), | ||||
|         threshold=1, | ||||
|         msg="white test pixel incorrect", | ||||
|  | @ -295,20 +301,26 @@ def test_colorize_2color_offset() -> None: | |||
|     left = (25, 1) | ||||
|     middle = (75, 1) | ||||
|     right = (125, 1) | ||||
|     value = im_test.getpixel(left) | ||||
|     assert isinstance(value, tuple) | ||||
|     assert_tuple_approx_equal( | ||||
|         im_test.getpixel(left), | ||||
|         value, | ||||
|         (255, 0, 0), | ||||
|         threshold=1, | ||||
|         msg="black test pixel incorrect", | ||||
|     ) | ||||
|     value = im_test.getpixel(middle) | ||||
|     assert isinstance(value, tuple) | ||||
|     assert_tuple_approx_equal( | ||||
|         im_test.getpixel(middle), | ||||
|         value, | ||||
|         (127, 63, 0), | ||||
|         threshold=1, | ||||
|         msg="mid test pixel incorrect", | ||||
|     ) | ||||
|     value = im_test.getpixel(right) | ||||
|     assert isinstance(value, tuple) | ||||
|     assert_tuple_approx_equal( | ||||
|         im_test.getpixel(right), | ||||
|         value, | ||||
|         (0, 127, 0), | ||||
|         threshold=1, | ||||
|         msg="white test pixel incorrect", | ||||
|  | @ -339,29 +351,37 @@ def test_colorize_3color_offset() -> None: | |||
|     middle = (100, 1) | ||||
|     right_middle = (150, 1) | ||||
|     right = (225, 1) | ||||
|     value = im_test.getpixel(left) | ||||
|     assert isinstance(value, tuple) | ||||
|     assert_tuple_approx_equal( | ||||
|         im_test.getpixel(left), | ||||
|         value, | ||||
|         (255, 0, 0), | ||||
|         threshold=1, | ||||
|         msg="black test pixel incorrect", | ||||
|     ) | ||||
|     value = im_test.getpixel(left_middle) | ||||
|     assert isinstance(value, tuple) | ||||
|     assert_tuple_approx_equal( | ||||
|         im_test.getpixel(left_middle), | ||||
|         value, | ||||
|         (127, 0, 127), | ||||
|         threshold=1, | ||||
|         msg="low-mid test pixel incorrect", | ||||
|     ) | ||||
|     value = im_test.getpixel(middle) | ||||
|     assert isinstance(value, tuple) | ||||
|     assert_tuple_approx_equal(value, (0, 0, 255), threshold=1, msg="mid incorrect") | ||||
|     value = im_test.getpixel(right_middle) | ||||
|     assert isinstance(value, tuple) | ||||
|     assert_tuple_approx_equal( | ||||
|         im_test.getpixel(middle), (0, 0, 255), threshold=1, msg="mid incorrect" | ||||
|     ) | ||||
|     assert_tuple_approx_equal( | ||||
|         im_test.getpixel(right_middle), | ||||
|         value, | ||||
|         (0, 63, 127), | ||||
|         threshold=1, | ||||
|         msg="high-mid test pixel incorrect", | ||||
|     ) | ||||
|     value = im_test.getpixel(right) | ||||
|     assert isinstance(value, tuple) | ||||
|     assert_tuple_approx_equal( | ||||
|         im_test.getpixel(right), | ||||
|         value, | ||||
|         (0, 127, 0), | ||||
|         threshold=1, | ||||
|         msg="white test pixel incorrect", | ||||
|  | @ -444,6 +464,7 @@ def test_exif_transpose_xml_without_xmp() -> None: | |||
| 
 | ||||
|         del im.info["xmp"] | ||||
|         transposed_im = ImageOps.exif_transpose(im) | ||||
|         assert transposed_im is not None | ||||
|         assert 0x0112 not in transposed_im.getexif() | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,8 +16,11 @@ def test_sanity() -> None: | |||
| 
 | ||||
| 
 | ||||
| def test_register() -> None: | ||||
|     # Test registering a viewer that is not a class | ||||
|     ImageShow.register("not a class") | ||||
|     # Test registering a viewer that is an instance | ||||
|     class TestViewer(ImageShow.Viewer): | ||||
|         pass | ||||
| 
 | ||||
|     ImageShow.register(TestViewer()) | ||||
| 
 | ||||
|     # Restore original state | ||||
|     ImageShow._viewers.pop() | ||||
|  |  | |||
|  | @ -45,10 +45,12 @@ def test_kw() -> None: | |||
| 
 | ||||
|             # Test "file" | ||||
|             im = ImageTk._get_image_from_kw(kw) | ||||
|             assert im is not None | ||||
|             assert_image_equal(im, im1) | ||||
| 
 | ||||
|             # Test "data" | ||||
|             im = ImageTk._get_image_from_kw(kw) | ||||
|             assert im is not None | ||||
|             assert_image_equal(im, im2) | ||||
| 
 | ||||
|     # Test no relevant entry | ||||
|  | @ -107,3 +109,6 @@ def test_bitmapimage() -> None: | |||
| 
 | ||||
|     # reloaded = ImageTk.getimage(im_tk) | ||||
|     # assert_image_equal(reloaded, im) | ||||
| 
 | ||||
|     with pytest.raises(ValueError): | ||||
|         ImageTk.BitmapImage() | ||||
|  |  | |||
|  | @ -57,6 +57,9 @@ class TestImageWinDib: | |||
|         # Assert | ||||
|         assert dib.size == (128, 128) | ||||
| 
 | ||||
|         with pytest.raises(ValueError): | ||||
|             ImageWin.Dib(mode) | ||||
| 
 | ||||
|     def test_dib_paste(self) -> None: | ||||
|         # Arrange | ||||
|         im = hopper() | ||||
|  |  | |||
|  | @ -198,6 +198,15 @@ def test_putdata() -> None: | |||
|     assert len(im.getdata()) == len(arr) | ||||
| 
 | ||||
| 
 | ||||
| def test_resize() -> None: | ||||
|     im = hopper() | ||||
|     size = (64, 64) | ||||
| 
 | ||||
|     im_resized = im.resize(numpy.array(size)) | ||||
| 
 | ||||
|     assert im_resized.size == size | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "dtype", | ||||
|     ( | ||||
|  |  | |||
|  | @ -11,7 +11,11 @@ from PIL.TiffImagePlugin import IFDRational | |||
| from .helper import hopper, skip_unless_feature | ||||
| 
 | ||||
| 
 | ||||
| def _test_equal(num, denom, target) -> None: | ||||
| def _test_equal( | ||||
|     num: float | Fraction | IFDRational, | ||||
|     denom: int, | ||||
|     target: float | Fraction | IFDRational, | ||||
| ) -> None: | ||||
|     t = IFDRational(num, denom) | ||||
| 
 | ||||
|     assert target == t | ||||
|  |  | |||
|  | @ -75,7 +75,7 @@ These platforms have been reported to work at the versions mentioned. | |||
| | Operating system                 | | Tested Python            | | Latest tested  | | Tested     | | ||||
| |                                  | | versions                 | | Pillow version | | processors | | ||||
| +==================================+============================+==================+==============+ | ||||
| | macOS 14 Sonoma                  | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.3.0           |arm           | | ||||
| | macOS 14 Sonoma                  | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0           |arm           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 13 Ventura                 | 3.8, 3.9, 3.10, 3.11       | 10.0.1           |arm           | | ||||
| |                                  +----------------------------+------------------+              | | ||||
|  |  | |||
|  | @ -155,3 +155,11 @@ follow_imports = "silent" | |||
| warn_redundant_casts = true | ||||
| warn_unreachable = true | ||||
| warn_unused_ignores = true | ||||
| exclude = [ | ||||
|   '^Tests/oss-fuzz/fuzz_font.py$', | ||||
|   '^Tests/oss-fuzz/fuzz_pillow.py$', | ||||
|   '^Tests/test_qt_image_qapplication.py$', | ||||
|   '^Tests/test_font_pcf_charsets.py$', | ||||
|   '^Tests/test_font_pcf.py$', | ||||
|   '^Tests/test_file_tar.py$', | ||||
| ] | ||||
|  |  | |||
|  | @ -313,6 +313,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): | |||
|         self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4)) | ||||
| 
 | ||||
|     def _safe_read(self, length: int) -> bytes: | ||||
|         assert self.fd is not None | ||||
|         return ImageFile._safe_read(self.fd, length) | ||||
| 
 | ||||
|     def _read_palette(self) -> list[tuple[int, int, int, int]]: | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| import os | ||||
| from typing import IO | ||||
| from typing import IO, Any | ||||
| 
 | ||||
| from . import Image, ImageFile, ImagePalette | ||||
| from ._binary import i16le as i16 | ||||
|  | @ -72,16 +72,20 @@ class BmpImageFile(ImageFile.ImageFile): | |||
|     for k, v in COMPRESSIONS.items(): | ||||
|         vars()[k] = v | ||||
| 
 | ||||
|     def _bitmap(self, header=0, offset=0): | ||||
|     def _bitmap(self, header: int = 0, offset: int = 0) -> None: | ||||
|         """Read relevant info about the BMP""" | ||||
|         read, seek = self.fp.read, self.fp.seek | ||||
|         if header: | ||||
|             seek(header) | ||||
|         # read bmp header size @offset 14 (this is part of the header size) | ||||
|         file_info = {"header_size": i32(read(4)), "direction": -1} | ||||
|         file_info: dict[str, bool | int | tuple[int, ...]] = { | ||||
|             "header_size": i32(read(4)), | ||||
|             "direction": -1, | ||||
|         } | ||||
| 
 | ||||
|         # -------------------- If requested, read header at a specific position | ||||
|         # read the rest of the bmp header, without its size | ||||
|         assert isinstance(file_info["header_size"], int) | ||||
|         header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4) | ||||
| 
 | ||||
|         # ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1 | ||||
|  | @ -92,7 +96,7 @@ class BmpImageFile(ImageFile.ImageFile): | |||
|             file_info["height"] = i16(header_data, 2) | ||||
|             file_info["planes"] = i16(header_data, 4) | ||||
|             file_info["bits"] = i16(header_data, 6) | ||||
|             file_info["compression"] = self.RAW | ||||
|             file_info["compression"] = self.COMPRESSIONS["RAW"] | ||||
|             file_info["palette_padding"] = 3 | ||||
| 
 | ||||
|         # --------------------------------------------- Windows Bitmap v3 to v5 | ||||
|  | @ -122,8 +126,9 @@ class BmpImageFile(ImageFile.ImageFile): | |||
|             ) | ||||
|             file_info["colors"] = i32(header_data, 28) | ||||
|             file_info["palette_padding"] = 4 | ||||
|             assert isinstance(file_info["pixels_per_meter"], tuple) | ||||
|             self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"]) | ||||
|             if file_info["compression"] == self.BITFIELDS: | ||||
|             if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: | ||||
|                 masks = ["r_mask", "g_mask", "b_mask"] | ||||
|                 if len(header_data) >= 48: | ||||
|                     if len(header_data) >= 52: | ||||
|  | @ -144,6 +149,10 @@ class BmpImageFile(ImageFile.ImageFile): | |||
|                     file_info["a_mask"] = 0x0 | ||||
|                     for mask in masks: | ||||
|                         file_info[mask] = i32(read(4)) | ||||
|                 assert isinstance(file_info["r_mask"], int) | ||||
|                 assert isinstance(file_info["g_mask"], int) | ||||
|                 assert isinstance(file_info["b_mask"], int) | ||||
|                 assert isinstance(file_info["a_mask"], int) | ||||
|                 file_info["rgb_mask"] = ( | ||||
|                     file_info["r_mask"], | ||||
|                     file_info["g_mask"], | ||||
|  | @ -164,24 +173,26 @@ class BmpImageFile(ImageFile.ImageFile): | |||
|         self._size = file_info["width"], file_info["height"] | ||||
| 
 | ||||
|         # ------- If color count was not found in the header, compute from bits | ||||
|         assert isinstance(file_info["bits"], int) | ||||
|         file_info["colors"] = ( | ||||
|             file_info["colors"] | ||||
|             if file_info.get("colors", 0) | ||||
|             else (1 << file_info["bits"]) | ||||
|         ) | ||||
|         assert isinstance(file_info["colors"], int) | ||||
|         if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8: | ||||
|             offset += 4 * file_info["colors"] | ||||
| 
 | ||||
|         # ---------------------- Check bit depth for unusual unsupported values | ||||
|         self._mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) | ||||
|         if self.mode is None: | ||||
|         self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", "")) | ||||
|         if not self.mode: | ||||
|             msg = f"Unsupported BMP pixel depth ({file_info['bits']})" | ||||
|             raise OSError(msg) | ||||
| 
 | ||||
|         # ---------------- Process BMP with Bitfields compression (not palette) | ||||
|         decoder_name = "raw" | ||||
|         if file_info["compression"] == self.BITFIELDS: | ||||
|             SUPPORTED = { | ||||
|         if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]: | ||||
|             SUPPORTED: dict[int, list[tuple[int, ...]]] = { | ||||
|                 32: [ | ||||
|                     (0xFF0000, 0xFF00, 0xFF, 0x0), | ||||
|                     (0xFF000000, 0xFF0000, 0xFF00, 0x0), | ||||
|  | @ -213,12 +224,14 @@ class BmpImageFile(ImageFile.ImageFile): | |||
|                     file_info["bits"] == 32 | ||||
|                     and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]] | ||||
|                 ): | ||||
|                     assert isinstance(file_info["rgba_mask"], tuple) | ||||
|                     raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])] | ||||
|                     self._mode = "RGBA" if "A" in raw_mode else self.mode | ||||
|                 elif ( | ||||
|                     file_info["bits"] in (24, 16) | ||||
|                     and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]] | ||||
|                 ): | ||||
|                     assert isinstance(file_info["rgb_mask"], tuple) | ||||
|                     raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])] | ||||
|                 else: | ||||
|                     msg = "Unsupported BMP bitfields layout" | ||||
|  | @ -226,10 +239,13 @@ class BmpImageFile(ImageFile.ImageFile): | |||
|             else: | ||||
|                 msg = "Unsupported BMP bitfields layout" | ||||
|                 raise OSError(msg) | ||||
|         elif file_info["compression"] == self.RAW: | ||||
|         elif file_info["compression"] == self.COMPRESSIONS["RAW"]: | ||||
|             if file_info["bits"] == 32 and header == 22:  # 32-bit .cur offset | ||||
|                 raw_mode, self._mode = "BGRA", "RGBA" | ||||
|         elif file_info["compression"] in (self.RLE8, self.RLE4): | ||||
|         elif file_info["compression"] in ( | ||||
|             self.COMPRESSIONS["RLE8"], | ||||
|             self.COMPRESSIONS["RLE4"], | ||||
|         ): | ||||
|             decoder_name = "bmp_rle" | ||||
|         else: | ||||
|             msg = f"Unsupported BMP compression ({file_info['compression']})" | ||||
|  | @ -242,6 +258,7 @@ class BmpImageFile(ImageFile.ImageFile): | |||
|                 msg = f"Unsupported BMP Palette size ({file_info['colors']})" | ||||
|                 raise OSError(msg) | ||||
|             else: | ||||
|                 assert isinstance(file_info["palette_padding"], int) | ||||
|                 padding = file_info["palette_padding"] | ||||
|                 palette = read(padding * file_info["colors"]) | ||||
|                 grayscale = True | ||||
|  | @ -269,10 +286,11 @@ class BmpImageFile(ImageFile.ImageFile): | |||
| 
 | ||||
|         # ---------------------------- Finally set the tile data for the plugin | ||||
|         self.info["compression"] = file_info["compression"] | ||||
|         args = [raw_mode] | ||||
|         args: list[Any] = [raw_mode] | ||||
|         if decoder_name == "bmp_rle": | ||||
|             args.append(file_info["compression"] == self.RLE4) | ||||
|             args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"]) | ||||
|         else: | ||||
|             assert isinstance(file_info["width"], int) | ||||
|             args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3)) | ||||
|         args.append(file_info["direction"]) | ||||
|         self.tile = [ | ||||
|  |  | |||
|  | @ -65,7 +65,7 @@ def has_ghostscript() -> bool: | |||
|     return gs_binary is not False | ||||
| 
 | ||||
| 
 | ||||
| def Ghostscript(tile, size, fp, scale=1, transparency=False): | ||||
| def Ghostscript(tile, size, fp, scale=1, transparency: bool = False) -> Image.Image: | ||||
|     """Render an image using Ghostscript""" | ||||
|     global gs_binary | ||||
|     if not has_ghostscript(): | ||||
|  | @ -338,7 +338,7 @@ class EpsImageFile(ImageFile.ImageFile): | |||
|             msg = "cannot determine EPS bounding box" | ||||
|             raise OSError(msg) | ||||
| 
 | ||||
|     def _find_offset(self, fp): | ||||
|     def _find_offset(self, fp: IO[bytes]) -> tuple[int, int]: | ||||
|         s = fp.read(4) | ||||
| 
 | ||||
|         if s == b"%!PS": | ||||
|  | @ -361,7 +361,9 @@ class EpsImageFile(ImageFile.ImageFile): | |||
| 
 | ||||
|         return length, offset | ||||
| 
 | ||||
|     def load(self, scale=1, transparency=False): | ||||
|     def load( | ||||
|         self, scale: int = 1, transparency: bool = False | ||||
|     ) -> Image.core.PixelAccess | None: | ||||
|         # Load EPS via Ghostscript | ||||
|         if self.tile: | ||||
|             self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) | ||||
|  |  | |||
|  | @ -45,7 +45,7 @@ class FliImageFile(ImageFile.ImageFile): | |||
|     format_description = "Autodesk FLI/FLC Animation" | ||||
|     _close_exclusive_fp_after_loading = False | ||||
| 
 | ||||
|     def _open(self): | ||||
|     def _open(self) -> None: | ||||
|         # HEAD | ||||
|         s = self.fp.read(128) | ||||
|         if not (_accept(s) and s[20:22] == b"\x00\x00"): | ||||
|  | @ -83,7 +83,7 @@ class FliImageFile(ImageFile.ImageFile): | |||
|         if i16(s, 4) == 0xF1FA: | ||||
|             # look for palette chunk | ||||
|             number_of_subchunks = i16(s, 6) | ||||
|             chunk_size = None | ||||
|             chunk_size: int | None = None | ||||
|             for _ in range(number_of_subchunks): | ||||
|                 if chunk_size is not None: | ||||
|                     self.fp.seek(chunk_size - 6, os.SEEK_CUR) | ||||
|  | @ -96,8 +96,9 @@ class FliImageFile(ImageFile.ImageFile): | |||
|                 if not chunk_size: | ||||
|                     break | ||||
| 
 | ||||
|         palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette] | ||||
|         self.palette = ImagePalette.raw("RGB", b"".join(palette)) | ||||
|         self.palette = ImagePalette.raw( | ||||
|             "RGB", b"".join(o8(r) + o8(g) + o8(b) for (r, g, b) in palette) | ||||
|         ) | ||||
| 
 | ||||
|         # set things up to decode first frame | ||||
|         self.__frame = -1 | ||||
|  | @ -105,7 +106,7 @@ class FliImageFile(ImageFile.ImageFile): | |||
|         self.__rewind = self.fp.tell() | ||||
|         self.seek(0) | ||||
| 
 | ||||
|     def _palette(self, palette, shift): | ||||
|     def _palette(self, palette: list[tuple[int, int, int]], shift: int) -> None: | ||||
|         # load palette | ||||
| 
 | ||||
|         i = 0 | ||||
|  |  | |||
|  | @ -231,7 +231,7 @@ class FpxImageFile(ImageFile.ImageFile): | |||
|         self._fp = self.fp | ||||
|         self.fp = None | ||||
| 
 | ||||
|     def load(self): | ||||
|     def load(self) -> Image.core.PixelAccess | None: | ||||
|         if not self.fp: | ||||
|             self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) | ||||
| 
 | ||||
|  |  | |||
|  | @ -88,7 +88,7 @@ class GbrImageFile(ImageFile.ImageFile): | |||
|         # Data is an uncompressed block of w * h * bytes/pixel | ||||
|         self._data_size = width * height * color_depth | ||||
| 
 | ||||
|     def load(self): | ||||
|     def load(self) -> Image.core.PixelAccess | None: | ||||
|         if not self.im: | ||||
|             self.im = Image.core.new(self.mode, self.size) | ||||
|             self.frombytes(self.fp.read(self._data_size)) | ||||
|  |  | |||
|  | @ -34,11 +34,13 @@ MAGIC = b"icns" | |||
| HEADERSIZE = 8 | ||||
| 
 | ||||
| 
 | ||||
| def nextheader(fobj): | ||||
| def nextheader(fobj: IO[bytes]) -> tuple[bytes, int]: | ||||
|     return struct.unpack(">4sI", fobj.read(HEADERSIZE)) | ||||
| 
 | ||||
| 
 | ||||
| def read_32t(fobj, start_length, size): | ||||
| def read_32t( | ||||
|     fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] | ||||
| ) -> dict[str, Image.Image]: | ||||
|     # The 128x128 icon seems to have an extra header for some reason. | ||||
|     (start, length) = start_length | ||||
|     fobj.seek(start) | ||||
|  | @ -49,7 +51,9 @@ def read_32t(fobj, start_length, size): | |||
|     return read_32(fobj, (start + 4, length - 4), size) | ||||
| 
 | ||||
| 
 | ||||
| def read_32(fobj, start_length, size): | ||||
| def read_32( | ||||
|     fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] | ||||
| ) -> dict[str, Image.Image]: | ||||
|     """ | ||||
|     Read a 32bit RGB icon resource.  Seems to be either uncompressed or | ||||
|     an RLE packbits-like scheme. | ||||
|  | @ -72,14 +76,14 @@ def read_32(fobj, start_length, size): | |||
|                 byte = fobj.read(1) | ||||
|                 if not byte: | ||||
|                     break | ||||
|                 byte = byte[0] | ||||
|                 if byte & 0x80: | ||||
|                     blocksize = byte - 125 | ||||
|                 byte_int = byte[0] | ||||
|                 if byte_int & 0x80: | ||||
|                     blocksize = byte_int - 125 | ||||
|                     byte = fobj.read(1) | ||||
|                     for i in range(blocksize): | ||||
|                         data.append(byte) | ||||
|                 else: | ||||
|                     blocksize = byte + 1 | ||||
|                     blocksize = byte_int + 1 | ||||
|                     data.append(fobj.read(blocksize)) | ||||
|                 bytesleft -= blocksize | ||||
|                 if bytesleft <= 0: | ||||
|  | @ -92,7 +96,9 @@ def read_32(fobj, start_length, size): | |||
|     return {"RGB": im} | ||||
| 
 | ||||
| 
 | ||||
| def read_mk(fobj, start_length, size): | ||||
| def read_mk( | ||||
|     fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] | ||||
| ) -> dict[str, Image.Image]: | ||||
|     # Alpha masks seem to be uncompressed | ||||
|     start = start_length[0] | ||||
|     fobj.seek(start) | ||||
|  | @ -102,10 +108,14 @@ def read_mk(fobj, start_length, size): | |||
|     return {"A": band} | ||||
| 
 | ||||
| 
 | ||||
| def read_png_or_jpeg2000(fobj, start_length, size): | ||||
| def read_png_or_jpeg2000( | ||||
|     fobj: IO[bytes], start_length: tuple[int, int], size: tuple[int, int, int] | ||||
| ) -> dict[str, Image.Image]: | ||||
|     (start, length) = start_length | ||||
|     fobj.seek(start) | ||||
|     sig = fobj.read(12) | ||||
| 
 | ||||
|     im: Image.Image | ||||
|     if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a": | ||||
|         fobj.seek(start) | ||||
|         im = PngImagePlugin.PngImageFile(fobj) | ||||
|  | @ -164,12 +174,12 @@ class IcnsFile: | |||
|         ], | ||||
|     } | ||||
| 
 | ||||
|     def __init__(self, fobj): | ||||
|     def __init__(self, fobj: IO[bytes]) -> None: | ||||
|         """ | ||||
|         fobj is a file-like object as an icns resource | ||||
|         """ | ||||
|         # signature : (start, length) | ||||
|         self.dct = dct = {} | ||||
|         self.dct = {} | ||||
|         self.fobj = fobj | ||||
|         sig, filesize = nextheader(fobj) | ||||
|         if not _accept(sig): | ||||
|  | @ -183,11 +193,11 @@ class IcnsFile: | |||
|                 raise SyntaxError(msg) | ||||
|             i += HEADERSIZE | ||||
|             blocksize -= HEADERSIZE | ||||
|             dct[sig] = (i, blocksize) | ||||
|             self.dct[sig] = (i, blocksize) | ||||
|             fobj.seek(blocksize, io.SEEK_CUR) | ||||
|             i += blocksize | ||||
| 
 | ||||
|     def itersizes(self): | ||||
|     def itersizes(self) -> list[tuple[int, int, int]]: | ||||
|         sizes = [] | ||||
|         for size, fmts in self.SIZES.items(): | ||||
|             for fmt, reader in fmts: | ||||
|  | @ -196,14 +206,14 @@ class IcnsFile: | |||
|                     break | ||||
|         return sizes | ||||
| 
 | ||||
|     def bestsize(self): | ||||
|     def bestsize(self) -> tuple[int, int, int]: | ||||
|         sizes = self.itersizes() | ||||
|         if not sizes: | ||||
|             msg = "No 32bit icon resources found" | ||||
|             raise SyntaxError(msg) | ||||
|         return max(sizes) | ||||
| 
 | ||||
|     def dataforsize(self, size): | ||||
|     def dataforsize(self, size: tuple[int, int, int]) -> dict[str, Image.Image]: | ||||
|         """ | ||||
|         Get an icon resource as {channel: array}.  Note that | ||||
|         the arrays are bottom-up like windows bitmaps and will likely | ||||
|  | @ -216,18 +226,20 @@ class IcnsFile: | |||
|                 dct.update(reader(self.fobj, desc, size)) | ||||
|         return dct | ||||
| 
 | ||||
|     def getimage(self, size=None): | ||||
|     def getimage( | ||||
|         self, size: tuple[int, int] | tuple[int, int, int] | None = None | ||||
|     ) -> Image.Image: | ||||
|         if size is None: | ||||
|             size = self.bestsize() | ||||
|         if len(size) == 2: | ||||
|         elif len(size) == 2: | ||||
|             size = (size[0], size[1], 1) | ||||
|         channels = self.dataforsize(size) | ||||
| 
 | ||||
|         im = channels.get("RGBA", None) | ||||
|         im = channels.get("RGBA") | ||||
|         if im: | ||||
|             return im | ||||
| 
 | ||||
|         im = channels.get("RGB").copy() | ||||
|         im = channels["RGB"].copy() | ||||
|         try: | ||||
|             im.putalpha(channels["A"]) | ||||
|         except KeyError: | ||||
|  | @ -268,7 +280,7 @@ class IcnsImageFile(ImageFile.ImageFile): | |||
|         return self._size | ||||
| 
 | ||||
|     @size.setter | ||||
|     def size(self, value): | ||||
|     def size(self, value) -> None: | ||||
|         info_size = value | ||||
|         if info_size not in self.info["sizes"] and len(info_size) == 2: | ||||
|             info_size = (info_size[0], info_size[1], 1) | ||||
|  | @ -287,7 +299,7 @@ class IcnsImageFile(ImageFile.ImageFile): | |||
|             raise ValueError(msg) | ||||
|         self._size = value | ||||
| 
 | ||||
|     def load(self): | ||||
|     def load(self) -> Image.core.PixelAccess | None: | ||||
|         if len(self.size) == 3: | ||||
|             self.best_size = self.size | ||||
|             self.size = ( | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ from __future__ import annotations | |||
| import warnings | ||||
| from io import BytesIO | ||||
| from math import ceil, log | ||||
| from typing import IO | ||||
| from typing import IO, NamedTuple | ||||
| 
 | ||||
| from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin | ||||
| from ._binary import i16le as i16 | ||||
|  | @ -119,8 +119,22 @@ def _accept(prefix: bytes) -> bool: | |||
|     return prefix[:4] == _MAGIC | ||||
| 
 | ||||
| 
 | ||||
| class IconHeader(NamedTuple): | ||||
|     width: int | ||||
|     height: int | ||||
|     nb_color: int | ||||
|     reserved: int | ||||
|     planes: int | ||||
|     bpp: int | ||||
|     size: int | ||||
|     offset: int | ||||
|     dim: tuple[int, int] | ||||
|     square: int | ||||
|     color_depth: int | ||||
| 
 | ||||
| 
 | ||||
| class IcoFile: | ||||
|     def __init__(self, buf): | ||||
|     def __init__(self, buf: IO[bytes]) -> None: | ||||
|         """ | ||||
|         Parse image from file-like object containing ico file data | ||||
|         """ | ||||
|  | @ -141,55 +155,48 @@ class IcoFile: | |||
|         for i in range(self.nb_items): | ||||
|             s = buf.read(16) | ||||
| 
 | ||||
|             icon_header = { | ||||
|                 "width": s[0], | ||||
|                 "height": s[1], | ||||
|                 "nb_color": s[2],  # No. of colors in image (0 if >=8bpp) | ||||
|                 "reserved": s[3], | ||||
|                 "planes": i16(s, 4), | ||||
|                 "bpp": i16(s, 6), | ||||
|                 "size": i32(s, 8), | ||||
|                 "offset": i32(s, 12), | ||||
|             } | ||||
| 
 | ||||
|             # See Wikipedia | ||||
|             for j in ("width", "height"): | ||||
|                 if not icon_header[j]: | ||||
|                     icon_header[j] = 256 | ||||
|             width = s[0] or 256 | ||||
|             height = s[1] or 256 | ||||
| 
 | ||||
|             # No. of colors in image (0 if >=8bpp) | ||||
|             nb_color = s[2] | ||||
|             bpp = i16(s, 6) | ||||
|             icon_header = IconHeader( | ||||
|                 width=width, | ||||
|                 height=height, | ||||
|                 nb_color=nb_color, | ||||
|                 reserved=s[3], | ||||
|                 planes=i16(s, 4), | ||||
|                 bpp=i16(s, 6), | ||||
|                 size=i32(s, 8), | ||||
|                 offset=i32(s, 12), | ||||
|                 dim=(width, height), | ||||
|                 square=width * height, | ||||
|                 # See Wikipedia notes about color depth. | ||||
|                 # We need this just to differ images with equal sizes | ||||
|             icon_header["color_depth"] = ( | ||||
|                 icon_header["bpp"] | ||||
|                 or ( | ||||
|                     icon_header["nb_color"] != 0 | ||||
|                     and ceil(log(icon_header["nb_color"], 2)) | ||||
|                 color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256, | ||||
|             ) | ||||
|                 or 256 | ||||
|             ) | ||||
| 
 | ||||
|             icon_header["dim"] = (icon_header["width"], icon_header["height"]) | ||||
|             icon_header["square"] = icon_header["width"] * icon_header["height"] | ||||
| 
 | ||||
|             self.entry.append(icon_header) | ||||
| 
 | ||||
|         self.entry = sorted(self.entry, key=lambda x: x["color_depth"]) | ||||
|         self.entry = sorted(self.entry, key=lambda x: x.color_depth) | ||||
|         # ICO images are usually squares | ||||
|         self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True) | ||||
|         self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True) | ||||
| 
 | ||||
|     def sizes(self): | ||||
|     def sizes(self) -> set[tuple[int, int]]: | ||||
|         """ | ||||
|         Get a list of all available icon sizes and color depths. | ||||
|         Get a set of all available icon sizes and color depths. | ||||
|         """ | ||||
|         return {(h["width"], h["height"]) for h in self.entry} | ||||
|         return {(h.width, h.height) for h in self.entry} | ||||
| 
 | ||||
|     def getentryindex(self, size, bpp=False): | ||||
|     def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int: | ||||
|         for i, h in enumerate(self.entry): | ||||
|             if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): | ||||
|             if size == h.dim and (bpp is False or bpp == h.color_depth): | ||||
|                 return i | ||||
|         return 0 | ||||
| 
 | ||||
|     def getimage(self, size, bpp=False): | ||||
|     def getimage(self, size: tuple[int, int], bpp: int | bool = False) -> Image.Image: | ||||
|         """ | ||||
|         Get an image from the icon | ||||
|         """ | ||||
|  | @ -202,9 +209,9 @@ class IcoFile: | |||
| 
 | ||||
|         header = self.entry[idx] | ||||
| 
 | ||||
|         self.buf.seek(header["offset"]) | ||||
|         self.buf.seek(header.offset) | ||||
|         data = self.buf.read(8) | ||||
|         self.buf.seek(header["offset"]) | ||||
|         self.buf.seek(header.offset) | ||||
| 
 | ||||
|         im: Image.Image | ||||
|         if data[:8] == PngImagePlugin._MAGIC: | ||||
|  | @ -222,8 +229,7 @@ class IcoFile: | |||
|             im.tile[0] = d, (0, 0) + im.size, o, a | ||||
| 
 | ||||
|             # figure out where AND mask image starts | ||||
|             bpp = header["bpp"] | ||||
|             if 32 == bpp: | ||||
|             if header.bpp == 32: | ||||
|                 # 32-bit color depth icon image allows semitransparent areas | ||||
|                 # PIL's DIB format ignores transparency bits, recover them. | ||||
|                 # The DIB is packed in BGRX byte order where X is the alpha | ||||
|  | @ -253,7 +259,7 @@ class IcoFile: | |||
|                 # padded row size * height / bits per char | ||||
| 
 | ||||
|                 total_bytes = int((w * im.size[1]) / 8) | ||||
|                 and_mask_offset = header["offset"] + header["size"] - total_bytes | ||||
|                 and_mask_offset = header.offset + header.size - total_bytes | ||||
| 
 | ||||
|                 self.buf.seek(and_mask_offset) | ||||
|                 mask_data = self.buf.read(total_bytes) | ||||
|  | @ -307,7 +313,7 @@ class IcoImageFile(ImageFile.ImageFile): | |||
|     def _open(self) -> None: | ||||
|         self.ico = IcoFile(self.fp) | ||||
|         self.info["sizes"] = self.ico.sizes() | ||||
|         self.size = self.ico.entry[0]["dim"] | ||||
|         self.size = self.ico.entry[0].dim | ||||
|         self.load() | ||||
| 
 | ||||
|     @property | ||||
|  | @ -321,7 +327,7 @@ class IcoImageFile(ImageFile.ImageFile): | |||
|             raise ValueError(msg) | ||||
|         self._size = value | ||||
| 
 | ||||
|     def load(self): | ||||
|     def load(self) -> Image.core.PixelAccess | None: | ||||
|         if self.im is not None and self.im.size == self.size: | ||||
|             # Already loaded | ||||
|             return Image.Image.load(self) | ||||
|  | @ -341,6 +347,7 @@ class IcoImageFile(ImageFile.ImageFile): | |||
|             self.info["sizes"] = set(sizes) | ||||
| 
 | ||||
|             self.size = im.size | ||||
|         return None | ||||
| 
 | ||||
|     def load_seek(self, pos: int) -> None: | ||||
|         # Flag the ImageFile.Parser so that it | ||||
|  |  | |||
|  | @ -63,7 +63,6 @@ from . import ( | |||
| ) | ||||
| from ._binary import i32le, o32be, o32le | ||||
| from ._deprecate import deprecate | ||||
| from ._typing import StrOrBytesPath, TypeGuard | ||||
| from ._util import DeferredError, is_path | ||||
| 
 | ||||
| ElementTree: ModuleType | None | ||||
|  | @ -220,6 +219,7 @@ if hasattr(core, "DEFAULT_STRATEGY"): | |||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     from . import ImageFile, ImagePalette | ||||
|     from ._typing import NumpyArray, StrOrBytesPath, TypeGuard | ||||
| ID: list[str] = [] | ||||
| OPEN: dict[ | ||||
|     str, | ||||
|  | @ -1395,7 +1395,7 @@ class Image: | |||
| 
 | ||||
|     def getcolors( | ||||
|         self, maxcolors: int = 256 | ||||
|     ) -> list[tuple[int, int]] | list[tuple[int, float]] | None: | ||||
|     ) -> list[tuple[int, tuple[int, ...]]] | list[tuple[int, float]] | None: | ||||
|         """ | ||||
|         Returns a list of colors used in this image. | ||||
| 
 | ||||
|  | @ -1412,7 +1412,7 @@ class Image: | |||
|         self.load() | ||||
|         if self.mode in ("1", "L", "P"): | ||||
|             h = self.im.histogram() | ||||
|             out = [(h[i], i) for i in range(256) if h[i]] | ||||
|             out: list[tuple[int, float]] = [(h[i], i) for i in range(256) if h[i]] | ||||
|             if len(out) > maxcolors: | ||||
|                 return None | ||||
|             return out | ||||
|  | @ -1886,7 +1886,7 @@ class Image: | |||
| 
 | ||||
|     def point( | ||||
|         self, | ||||
|         lut: Sequence[float] | Callable[[int], float] | ImagePointHandler, | ||||
|         lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler, | ||||
|         mode: str | None = None, | ||||
|     ) -> Image: | ||||
|         """ | ||||
|  | @ -1996,7 +1996,7 @@ class Image: | |||
| 
 | ||||
|     def putdata( | ||||
|         self, | ||||
|         data: Sequence[float] | Sequence[Sequence[int]], | ||||
|         data: Sequence[float] | Sequence[Sequence[int]] | NumpyArray, | ||||
|         scale: float = 1.0, | ||||
|         offset: float = 0.0, | ||||
|     ) -> None: | ||||
|  | @ -2203,7 +2203,7 @@ class Image: | |||
| 
 | ||||
|     def resize( | ||||
|         self, | ||||
|         size: tuple[int, int], | ||||
|         size: tuple[int, int] | list[int] | NumpyArray, | ||||
|         resample: int | None = None, | ||||
|         box: tuple[float, float, float, float] | None = None, | ||||
|         reducing_gap: float | None = None, | ||||
|  | @ -2211,7 +2211,7 @@ class Image: | |||
|         """ | ||||
|         Returns a resized copy of this image. | ||||
| 
 | ||||
|         :param size: The requested size in pixels, as a 2-tuple: | ||||
|         :param size: The requested size in pixels, as a tuple or array: | ||||
|            (width, height). | ||||
|         :param resample: An optional resampling filter.  This can be | ||||
|            one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, | ||||
|  | @ -2276,6 +2276,7 @@ class Image: | |||
|         if box is None: | ||||
|             box = (0, 0) + self.size | ||||
| 
 | ||||
|         size = tuple(size) | ||||
|         if self.size == size and box == (0, 0) + self.size: | ||||
|             return self.copy() | ||||
| 
 | ||||
|  | @ -3302,7 +3303,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: | |||
|     return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) | ||||
| 
 | ||||
| 
 | ||||
| def fromqimage(im): | ||||
| def fromqimage(im) -> ImageFile.ImageFile: | ||||
|     """Creates an image instance from a QImage image""" | ||||
|     from . import ImageQt | ||||
| 
 | ||||
|  | @ -3312,7 +3313,7 @@ def fromqimage(im): | |||
|     return ImageQt.fromqimage(im) | ||||
| 
 | ||||
| 
 | ||||
| def fromqpixmap(im): | ||||
| def fromqpixmap(im) -> ImageFile.ImageFile: | ||||
|     """Creates an image instance from a QPixmap image""" | ||||
|     from . import ImageQt | ||||
| 
 | ||||
|  | @ -3883,7 +3884,7 @@ class Exif(_ExifBase): | |||
|         # returns a dict with any single item tuples/lists as individual values | ||||
|         return {k: self._fixup(v) for k, v in src_dict.items()} | ||||
| 
 | ||||
|     def _get_ifd_dict(self, offset, group=None): | ||||
|     def _get_ifd_dict(self, offset: int, group=None): | ||||
|         try: | ||||
|             # an offset pointer to the location of the nested embedded IFD. | ||||
|             # It should be a long, but may be corrupted. | ||||
|  | @ -3897,7 +3898,7 @@ class Exif(_ExifBase): | |||
|             info.load(self.fp) | ||||
|             return self._fixup_dict(info) | ||||
| 
 | ||||
|     def _get_head(self): | ||||
|     def _get_head(self) -> bytes: | ||||
|         version = b"\x2B" if self.bigtiff else b"\x2A" | ||||
|         if self.endian == "<": | ||||
|             head = b"II" + version + b"\x00" + o32le(8) | ||||
|  | @ -4118,16 +4119,16 @@ class Exif(_ExifBase): | |||
|             keys.update(self._info) | ||||
|         return len(keys) | ||||
| 
 | ||||
|     def __getitem__(self, tag): | ||||
|     def __getitem__(self, tag: int): | ||||
|         if self._info is not None and tag not in self._data and tag in self._info: | ||||
|             self._data[tag] = self._fixup(self._info[tag]) | ||||
|             del self._info[tag] | ||||
|         return self._data[tag] | ||||
| 
 | ||||
|     def __contains__(self, tag) -> bool: | ||||
|     def __contains__(self, tag: object) -> bool: | ||||
|         return tag in self._data or (self._info is not None and tag in self._info) | ||||
| 
 | ||||
|     def __setitem__(self, tag, value) -> None: | ||||
|     def __setitem__(self, tag: int, value) -> None: | ||||
|         if self._info is not None and tag in self._info: | ||||
|             del self._info[tag] | ||||
|         self._data[tag] = value | ||||
|  |  | |||
|  | @ -86,7 +86,7 @@ def raise_oserror(error: int) -> OSError: | |||
|     raise _get_oserror(error, encoder=False) | ||||
| 
 | ||||
| 
 | ||||
| def _tilesort(t): | ||||
| def _tilesort(t) -> int: | ||||
|     # sort on offset | ||||
|     return t[2] | ||||
| 
 | ||||
|  | @ -161,7 +161,7 @@ class ImageFile(Image.Image): | |||
|             return Image.MIME.get(self.format.upper()) | ||||
|         return None | ||||
| 
 | ||||
|     def __setstate__(self, state): | ||||
|     def __setstate__(self, state) -> None: | ||||
|         self.tile = [] | ||||
|         super().__setstate__(state) | ||||
| 
 | ||||
|  | @ -333,14 +333,14 @@ class ImageFile(Image.Image): | |||
|     # def load_read(self, read_bytes: int) -> bytes: | ||||
|     #     pass | ||||
| 
 | ||||
|     def _seek_check(self, frame): | ||||
|     def _seek_check(self, frame: int) -> bool: | ||||
|         if ( | ||||
|             frame < self._min_frame | ||||
|             # Only check upper limit on frames if additional seek operations | ||||
|             # are not required to do so | ||||
|             or ( | ||||
|                 not (hasattr(self, "_n_frames") and self._n_frames is None) | ||||
|                 and frame >= self.n_frames + self._min_frame | ||||
|                 and frame >= getattr(self, "n_frames") + self._min_frame | ||||
|             ) | ||||
|         ): | ||||
|             msg = "attempt to seek outside sequence" | ||||
|  | @ -370,7 +370,7 @@ class StubImageFile(ImageFile): | |||
|         msg = "StubImageFile subclass must implement _open" | ||||
|         raise NotImplementedError(msg) | ||||
| 
 | ||||
|     def load(self): | ||||
|     def load(self) -> Image.core.PixelAccess | None: | ||||
|         loader = self._load() | ||||
|         if loader is None: | ||||
|             msg = f"cannot find loader for this {self.format} file" | ||||
|  | @ -378,7 +378,7 @@ class StubImageFile(ImageFile): | |||
|         image = loader.load(self) | ||||
|         assert image is not None | ||||
|         # become the other object (!) | ||||
|         self.__class__ = image.__class__ | ||||
|         self.__class__ = image.__class__  # type: ignore[assignment] | ||||
|         self.__dict__ = image.__dict__ | ||||
|         return image.load() | ||||
| 
 | ||||
|  | @ -396,8 +396,8 @@ class Parser: | |||
| 
 | ||||
|     incremental = None | ||||
|     image: Image.Image | None = None | ||||
|     data = None | ||||
|     decoder = None | ||||
|     data: bytes | None = None | ||||
|     decoder: Image.core.ImagingDecoder | PyDecoder | None = None | ||||
|     offset = 0 | ||||
|     finished = 0 | ||||
| 
 | ||||
|  | @ -409,7 +409,7 @@ class Parser: | |||
|         """ | ||||
|         assert self.data is None, "cannot reuse parsers" | ||||
| 
 | ||||
|     def feed(self, data): | ||||
|     def feed(self, data: bytes) -> None: | ||||
|         """ | ||||
|         (Consumer) Feed data to the parser. | ||||
| 
 | ||||
|  | @ -485,13 +485,13 @@ class Parser: | |||
| 
 | ||||
|                 self.image = im | ||||
| 
 | ||||
|     def __enter__(self): | ||||
|     def __enter__(self) -> Parser: | ||||
|         return self | ||||
| 
 | ||||
|     def __exit__(self, *args: object) -> None: | ||||
|         self.close() | ||||
| 
 | ||||
|     def close(self): | ||||
|     def close(self) -> Image.Image: | ||||
|         """ | ||||
|         (Consumer) Close the stream. | ||||
| 
 | ||||
|  | @ -525,7 +525,7 @@ class Parser: | |||
| # -------------------------------------------------------------------- | ||||
| 
 | ||||
| 
 | ||||
| def _save(im, fp, tile, bufsize=0) -> None: | ||||
| def _save(im, fp, tile, bufsize: int = 0) -> None: | ||||
|     """Helper to save image based on tile list | ||||
| 
 | ||||
|     :param im: Image object. | ||||
|  | @ -553,7 +553,9 @@ def _save(im, fp, tile, bufsize=0) -> None: | |||
|         fp.flush() | ||||
| 
 | ||||
| 
 | ||||
| def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None): | ||||
| def _encode_tile( | ||||
|     im, fp: IO[bytes], tile: list[_Tile], bufsize: int, fh, exc=None | ||||
| ) -> None: | ||||
|     for encoder_name, extents, offset, args in tile: | ||||
|         if offset > 0: | ||||
|             fp.seek(offset) | ||||
|  | @ -580,7 +582,7 @@ def _encode_tile(im, fp, tile: list[_Tile], bufsize, fh, exc=None): | |||
|             encoder.cleanup() | ||||
| 
 | ||||
| 
 | ||||
| def _safe_read(fp, size): | ||||
| def _safe_read(fp: IO[bytes], size: int) -> bytes: | ||||
|     """ | ||||
|     Reads large blocks in a safe way.  Unlike fp.read(n), this function | ||||
|     doesn't trust the user.  If the requested size is larger than | ||||
|  | @ -601,18 +603,18 @@ def _safe_read(fp, size): | |||
|             msg = "Truncated File Read" | ||||
|             raise OSError(msg) | ||||
|         return data | ||||
|     data = [] | ||||
|     blocks: list[bytes] = [] | ||||
|     remaining_size = size | ||||
|     while remaining_size > 0: | ||||
|         block = fp.read(min(remaining_size, SAFEBLOCK)) | ||||
|         if not block: | ||||
|             break | ||||
|         data.append(block) | ||||
|         blocks.append(block) | ||||
|         remaining_size -= len(block) | ||||
|     if sum(len(d) for d in data) < size: | ||||
|     if sum(len(block) for block in blocks) < size: | ||||
|         msg = "Truncated File Read" | ||||
|         raise OSError(msg) | ||||
|     return b"".join(data) | ||||
|     return b"".join(blocks) | ||||
| 
 | ||||
| 
 | ||||
| class PyCodecState: | ||||
|  | @ -629,18 +631,18 @@ class PyCodecState: | |||
| class PyCodec: | ||||
|     fd: IO[bytes] | None | ||||
| 
 | ||||
|     def __init__(self, mode, *args): | ||||
|         self.im = None | ||||
|     def __init__(self, mode: str, *args: Any) -> None: | ||||
|         self.im: Image.core.ImagingCore | None = None | ||||
|         self.state = PyCodecState() | ||||
|         self.fd = None | ||||
|         self.mode = mode | ||||
|         self.init(args) | ||||
| 
 | ||||
|     def init(self, args): | ||||
|     def init(self, args: tuple[Any, ...]) -> None: | ||||
|         """ | ||||
|         Override to perform codec specific initialization | ||||
| 
 | ||||
|         :param args: Array of args items from the tile entry | ||||
|         :param args: Tuple of arg items from the tile entry | ||||
|         :returns: None | ||||
|         """ | ||||
|         self.args = args | ||||
|  | @ -653,7 +655,7 @@ class PyCodec: | |||
|         """ | ||||
|         pass | ||||
| 
 | ||||
|     def setfd(self, fd): | ||||
|     def setfd(self, fd: IO[bytes]) -> None: | ||||
|         """ | ||||
|         Called from ImageFile to set the Python file-like object | ||||
| 
 | ||||
|  | @ -662,7 +664,7 @@ class PyCodec: | |||
|         """ | ||||
|         self.fd = fd | ||||
| 
 | ||||
|     def setimage(self, im, extents: tuple[int, int, int, int] | None = None) -> None: | ||||
|     def setimage(self, im, extents=None): | ||||
|         """ | ||||
|         Called from ImageFile to set the core output image for the codec | ||||
| 
 | ||||
|  | @ -793,7 +795,7 @@ class PyEncoder(PyCodec): | |||
|             self.fd.write(data) | ||||
|         return bytes_consumed, errcode | ||||
| 
 | ||||
|     def encode_to_file(self, fh, bufsize): | ||||
|     def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int: | ||||
|         """ | ||||
|         :param fh: File handle. | ||||
|         :param bufsize: Buffer size. | ||||
|  |  | |||
|  | @ -19,11 +19,14 @@ from __future__ import annotations | |||
| 
 | ||||
| import sys | ||||
| from io import BytesIO | ||||
| from typing import Callable | ||||
| from typing import TYPE_CHECKING, Callable | ||||
| 
 | ||||
| from . import Image | ||||
| from ._util import is_path | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     from . import ImageFile | ||||
| 
 | ||||
| qt_version: str | None | ||||
| qt_versions = [ | ||||
|     ["6", "PyQt6"], | ||||
|  | @ -90,11 +93,11 @@ def fromqimage(im): | |||
|     return Image.open(b) | ||||
| 
 | ||||
| 
 | ||||
| def fromqpixmap(im): | ||||
| def fromqpixmap(im) -> ImageFile.ImageFile: | ||||
|     return fromqimage(im) | ||||
| 
 | ||||
| 
 | ||||
| def align8to32(bytes, width, mode): | ||||
| def align8to32(bytes: bytes, width: int, mode: str) -> bytes: | ||||
|     """ | ||||
|     converts each scanline of data from 8 bit to 32 bit aligned | ||||
|     """ | ||||
|  | @ -172,7 +175,7 @@ def _toqclass_helper(im): | |||
| if qt_is_installed: | ||||
| 
 | ||||
|     class ImageQt(QImage): | ||||
|         def __init__(self, im): | ||||
|         def __init__(self, im) -> None: | ||||
|             """ | ||||
|             An PIL image wrapper for Qt.  This is a subclass of PyQt's QImage | ||||
|             class. | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ class Iterator: | |||
|     :param im: An image object. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, im: Image.Image): | ||||
|     def __init__(self, im: Image.Image) -> None: | ||||
|         if not hasattr(im, "seek"): | ||||
|             msg = "im must have seek method" | ||||
|             raise AttributeError(msg) | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ from . import Image | |||
| _viewers = [] | ||||
| 
 | ||||
| 
 | ||||
| def register(viewer, order: int = 1) -> None: | ||||
| def register(viewer: type[Viewer] | Viewer, order: int = 1) -> None: | ||||
|     """ | ||||
|     The :py:func:`register` function is used to register additional viewers:: | ||||
| 
 | ||||
|  | @ -40,11 +40,8 @@ def register(viewer, order: int = 1) -> None: | |||
|         Zero or a negative integer to prepend this viewer to the list, | ||||
|         a positive integer to append it. | ||||
|     """ | ||||
|     try: | ||||
|         if issubclass(viewer, Viewer): | ||||
|     if isinstance(viewer, type) and issubclass(viewer, Viewer): | ||||
|         viewer = viewer() | ||||
|     except TypeError: | ||||
|         pass  # raised if viewer wasn't a class | ||||
|     if order > 0: | ||||
|         _viewers.append(viewer) | ||||
|     else: | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ from __future__ import annotations | |||
| 
 | ||||
| import tkinter | ||||
| from io import BytesIO | ||||
| from typing import Any | ||||
| from typing import TYPE_CHECKING, Any, cast | ||||
| 
 | ||||
| from . import Image, ImageFile | ||||
| 
 | ||||
|  | @ -61,7 +61,9 @@ def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None: | |||
|     return Image.open(source) | ||||
| 
 | ||||
| 
 | ||||
| def _pyimagingtkcall(command, photo, id): | ||||
| def _pyimagingtkcall( | ||||
|     command: str, photo: PhotoImage | tkinter.PhotoImage, id: int | ||||
| ) -> None: | ||||
|     tk = photo.tk | ||||
|     try: | ||||
|         tk.call(command, photo, id) | ||||
|  | @ -215,11 +217,14 @@ class BitmapImage: | |||
|     :param image: A PIL image. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, image=None, **kw): | ||||
|     def __init__(self, image: Image.Image | None = None, **kw: Any) -> None: | ||||
|         # Tk compatibility: file or data | ||||
|         if image is None: | ||||
|             image = _get_image_from_kw(kw) | ||||
| 
 | ||||
|         if image is None: | ||||
|             msg = "Image is required" | ||||
|             raise ValueError(msg) | ||||
|         self.__mode = image.mode | ||||
|         self.__size = image.size | ||||
| 
 | ||||
|  | @ -278,18 +283,23 @@ def getimage(photo: PhotoImage) -> Image.Image: | |||
|     return im | ||||
| 
 | ||||
| 
 | ||||
| def _show(image, title): | ||||
| def _show(image: Image.Image, title: str | None) -> None: | ||||
|     """Helper for the Image.show method.""" | ||||
| 
 | ||||
|     class UI(tkinter.Label): | ||||
|         def __init__(self, master, im): | ||||
|         def __init__(self, master: tkinter.Toplevel, im: Image.Image) -> None: | ||||
|             self.image: BitmapImage | PhotoImage | ||||
|             if im.mode == "1": | ||||
|                 self.image = BitmapImage(im, foreground="white", master=master) | ||||
|             else: | ||||
|                 self.image = PhotoImage(im, master=master) | ||||
|             super().__init__(master, image=self.image, bg="black", bd=0) | ||||
|             if TYPE_CHECKING: | ||||
|                 image = cast(tkinter._Image, self.image) | ||||
|             else: | ||||
|                 image = self.image | ||||
|             super().__init__(master, image=image, bg="black", bd=0) | ||||
| 
 | ||||
|     if not tkinter._default_root: | ||||
|     if not getattr(tkinter, "_default_root"): | ||||
|         msg = "tkinter not initialized" | ||||
|         raise OSError(msg) | ||||
|     top = tkinter.Toplevel() | ||||
|  |  | |||
|  | @ -70,11 +70,14 @@ class Dib: | |||
|     """ | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, image: Image.Image | str, size: tuple[int, int] | list[int] | None = None | ||||
|         self, image: Image.Image | str, size: tuple[int, int] | None = None | ||||
|     ) -> None: | ||||
|         if isinstance(image, str): | ||||
|             mode = image | ||||
|             image = "" | ||||
|             if size is None: | ||||
|                 msg = "If first argument is mode, size is required" | ||||
|                 raise ValueError(msg) | ||||
|         else: | ||||
|             mode = image.mode | ||||
|             size = image.size | ||||
|  | @ -105,7 +108,12 @@ class Dib: | |||
|             result = self.image.expose(handle) | ||||
|         return result | ||||
| 
 | ||||
|     def draw(self, handle, dst, src=None): | ||||
|     def draw( | ||||
|         self, | ||||
|         handle, | ||||
|         dst: tuple[int, int, int, int], | ||||
|         src: tuple[int, int, int, int] | None = None, | ||||
|     ): | ||||
|         """ | ||||
|         Same as expose, but allows you to specify where to draw the image, and | ||||
|         what part of it to draw. | ||||
|  | @ -115,7 +123,7 @@ class Dib: | |||
|         the destination have different sizes, the image is resized as | ||||
|         necessary. | ||||
|         """ | ||||
|         if not src: | ||||
|         if src is None: | ||||
|             src = (0, 0) + self.size | ||||
|         if isinstance(handle, HWND): | ||||
|             dc = self.image.getdc(handle) | ||||
|  | @ -202,22 +210,22 @@ class Window: | |||
|             title, self.__dispatcher, width or 0, height or 0 | ||||
|         ) | ||||
| 
 | ||||
|     def __dispatcher(self, action, *args): | ||||
|     def __dispatcher(self, action: str, *args): | ||||
|         return getattr(self, f"ui_handle_{action}")(*args) | ||||
| 
 | ||||
|     def ui_handle_clear(self, dc, x0, y0, x1, y1): | ||||
|     def ui_handle_clear(self, dc, x0, y0, x1, y1) -> None: | ||||
|         pass | ||||
| 
 | ||||
|     def ui_handle_damage(self, x0, y0, x1, y1): | ||||
|     def ui_handle_damage(self, x0, y0, x1, y1) -> None: | ||||
|         pass | ||||
| 
 | ||||
|     def ui_handle_destroy(self) -> None: | ||||
|         pass | ||||
| 
 | ||||
|     def ui_handle_repair(self, dc, x0, y0, x1, y1): | ||||
|     def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None: | ||||
|         pass | ||||
| 
 | ||||
|     def ui_handle_resize(self, width, height): | ||||
|     def ui_handle_resize(self, width, height) -> None: | ||||
|         pass | ||||
| 
 | ||||
|     def mainloop(self) -> None: | ||||
|  | @ -227,12 +235,12 @@ class Window: | |||
| class ImageWindow(Window): | ||||
|     """Create an image window which displays the given image.""" | ||||
| 
 | ||||
|     def __init__(self, image, title="PIL"): | ||||
|     def __init__(self, image, title: str = "PIL") -> None: | ||||
|         if not isinstance(image, Dib): | ||||
|             image = Dib(image) | ||||
|         self.image = image | ||||
|         width, height = image.size | ||||
|         super().__init__(title, width=width, height=height) | ||||
| 
 | ||||
|     def ui_handle_repair(self, dc, x0, y0, x1, y1): | ||||
|     def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None: | ||||
|         self.image.draw(dc, (x0, y0, x1, y1)) | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ from __future__ import annotations | |||
| 
 | ||||
| from collections.abc import Sequence | ||||
| from io import BytesIO | ||||
| from typing import cast | ||||
| 
 | ||||
| from . import Image, ImageFile | ||||
| from ._binary import i16be as i16 | ||||
|  | @ -148,7 +149,7 @@ class IptcImageFile(ImageFile.ImageFile): | |||
|         if tag == (8, 10): | ||||
|             self.tile = [("iptc", (0, 0) + self.size, offset, compression)] | ||||
| 
 | ||||
|     def load(self): | ||||
|     def load(self) -> Image.core.PixelAccess | None: | ||||
|         if len(self.tile) != 1 or self.tile[0][0] != "iptc": | ||||
|             return ImageFile.ImageFile.load(self) | ||||
| 
 | ||||
|  | @ -176,6 +177,7 @@ class IptcImageFile(ImageFile.ImageFile): | |||
|         with Image.open(o) as _im: | ||||
|             _im.load() | ||||
|             self.im = _im.im | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
| Image.register_open(IptcImageFile.format, IptcImageFile) | ||||
|  | @ -183,7 +185,7 @@ Image.register_open(IptcImageFile.format, IptcImageFile) | |||
| Image.register_extension(IptcImageFile.format, ".iim") | ||||
| 
 | ||||
| 
 | ||||
| def getiptcinfo(im): | ||||
| def getiptcinfo(im: ImageFile.ImageFile): | ||||
|     """ | ||||
|     Get IPTC information from TIFF, JPEG, or IPTC file. | ||||
| 
 | ||||
|  | @ -220,16 +222,17 @@ def getiptcinfo(im): | |||
|     class FakeImage: | ||||
|         pass | ||||
| 
 | ||||
|     im = FakeImage() | ||||
|     im.__class__ = IptcImageFile | ||||
|     fake_im = FakeImage() | ||||
|     fake_im.__class__ = IptcImageFile  # type: ignore[assignment] | ||||
|     iptc_im = cast(IptcImageFile, fake_im) | ||||
| 
 | ||||
|     # parse the IPTC information chunk | ||||
|     im.info = {} | ||||
|     im.fp = BytesIO(data) | ||||
|     iptc_im.info = {} | ||||
|     iptc_im.fp = BytesIO(data) | ||||
| 
 | ||||
|     try: | ||||
|         im._open() | ||||
|         iptc_im._open() | ||||
|     except (IndexError, KeyError): | ||||
|         pass  # expected failure | ||||
| 
 | ||||
|     return im.info | ||||
|     return iptc_im.info | ||||
|  |  | |||
|  | @ -29,7 +29,7 @@ class BoxReader: | |||
|     and to easily step into and read sub-boxes. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, fp, length=-1): | ||||
|     def __init__(self, fp: IO[bytes], length: int = -1) -> None: | ||||
|         self.fp = fp | ||||
|         self.has_length = length >= 0 | ||||
|         self.length = length | ||||
|  | @ -97,7 +97,7 @@ class BoxReader: | |||
|         return tbox | ||||
| 
 | ||||
| 
 | ||||
| def _parse_codestream(fp) -> tuple[tuple[int, int], str]: | ||||
| def _parse_codestream(fp: IO[bytes]) -> tuple[tuple[int, int], str]: | ||||
|     """Parse the JPEG 2000 codestream to extract the size and component | ||||
|     count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" | ||||
| 
 | ||||
|  | @ -137,7 +137,15 @@ def _res_to_dpi(num: int, denom: int, exp: int) -> float | None: | |||
|     return (254 * num * (10**exp)) / (10000 * denom) | ||||
| 
 | ||||
| 
 | ||||
| def _parse_jp2_header(fp): | ||||
| def _parse_jp2_header( | ||||
|     fp: IO[bytes], | ||||
| ) -> tuple[ | ||||
|     tuple[int, int], | ||||
|     str, | ||||
|     str | None, | ||||
|     tuple[float, float] | None, | ||||
|     ImagePalette.ImagePalette | None, | ||||
| ]: | ||||
|     """Parse the JP2 header box to extract size, component count, | ||||
|     color space information, and optionally DPI information, | ||||
|     returning a (size, mode, mimetype, dpi) tuple.""" | ||||
|  | @ -155,6 +163,7 @@ def _parse_jp2_header(fp): | |||
|         elif tbox == b"ftyp": | ||||
|             if reader.read_fields(">4s")[0] == b"jpx ": | ||||
|                 mimetype = "image/jpx" | ||||
|     assert header is not None | ||||
| 
 | ||||
|     size = None | ||||
|     mode = None | ||||
|  | @ -168,6 +177,9 @@ def _parse_jp2_header(fp): | |||
| 
 | ||||
|         if tbox == b"ihdr": | ||||
|             height, width, nc, bpc = header.read_fields(">IIHB") | ||||
|             assert isinstance(height, int) | ||||
|             assert isinstance(width, int) | ||||
|             assert isinstance(bpc, int) | ||||
|             size = (width, height) | ||||
|             if nc == 1 and (bpc & 0x7F) > 8: | ||||
|                 mode = "I;16" | ||||
|  | @ -185,11 +197,21 @@ def _parse_jp2_header(fp): | |||
|                 mode = "CMYK" | ||||
|         elif tbox == b"pclr" and mode in ("L", "LA"): | ||||
|             ne, npc = header.read_fields(">HB") | ||||
|             bitdepths = header.read_fields(">" + ("B" * npc)) | ||||
|             if max(bitdepths) <= 8: | ||||
|             assert isinstance(ne, int) | ||||
|             assert isinstance(npc, int) | ||||
|             max_bitdepth = 0 | ||||
|             for bitdepth in header.read_fields(">" + ("B" * npc)): | ||||
|                 assert isinstance(bitdepth, int) | ||||
|                 if bitdepth > max_bitdepth: | ||||
|                     max_bitdepth = bitdepth | ||||
|             if max_bitdepth <= 8: | ||||
|                 palette = ImagePalette.ImagePalette() | ||||
|                 for i in range(ne): | ||||
|                     palette.getcolor(header.read_fields(">" + ("B" * npc))) | ||||
|                     color: list[int] = [] | ||||
|                     for value in header.read_fields(">" + ("B" * npc)): | ||||
|                         assert isinstance(value, int) | ||||
|                         color.append(value) | ||||
|                     palette.getcolor(tuple(color)) | ||||
|                 mode = "P" if mode == "L" else "PA" | ||||
|         elif tbox == b"res ": | ||||
|             res = header.read_boxes() | ||||
|  | @ -197,6 +219,12 @@ def _parse_jp2_header(fp): | |||
|                 tres = res.next_box_type() | ||||
|                 if tres == b"resc": | ||||
|                     vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB") | ||||
|                     assert isinstance(vrcn, int) | ||||
|                     assert isinstance(vrcd, int) | ||||
|                     assert isinstance(hrcn, int) | ||||
|                     assert isinstance(hrcd, int) | ||||
|                     assert isinstance(vrce, int) | ||||
|                     assert isinstance(hrce, int) | ||||
|                     hres = _res_to_dpi(hrcn, hrcd, hrce) | ||||
|                     vres = _res_to_dpi(vrcn, vrcd, vrce) | ||||
|                     if hres is not None and vres is not None: | ||||
|  | @ -299,7 +327,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): | |||
|     def reduce(self, value): | ||||
|         self._reduce = value | ||||
| 
 | ||||
|     def load(self): | ||||
|     def load(self) -> Image.core.PixelAccess | None: | ||||
|         if self.tile and self._reduce: | ||||
|             power = 1 << self._reduce | ||||
|             adjust = power >> 1 | ||||
|  |  | |||
|  | @ -60,7 +60,7 @@ def Skip(self: JpegImageFile, marker: int) -> None: | |||
|     ImageFile._safe_read(self.fp, n) | ||||
| 
 | ||||
| 
 | ||||
| def APP(self, marker): | ||||
| def APP(self: JpegImageFile, marker: int) -> None: | ||||
|     # | ||||
|     # Application marker.  Store these in the APP dictionary. | ||||
|     # Also look for well-known application markers. | ||||
|  | @ -133,12 +133,13 @@ def APP(self, marker): | |||
|                 offset += 4 | ||||
|                 data = s[offset : offset + size] | ||||
|                 if code == 0x03ED:  # ResolutionInfo | ||||
|                     data = { | ||||
|                     photoshop[code] = { | ||||
|                         "XResolution": i32(data, 0) / 65536, | ||||
|                         "DisplayedUnitsX": i16(data, 4), | ||||
|                         "YResolution": i32(data, 8) / 65536, | ||||
|                         "DisplayedUnitsY": i16(data, 12), | ||||
|                     } | ||||
|                 else: | ||||
|                     photoshop[code] = data | ||||
|                 offset += size | ||||
|                 offset += offset & 1  # align | ||||
|  | @ -338,6 +339,7 @@ class JpegImageFile(ImageFile.ImageFile): | |||
| 
 | ||||
|         # Create attributes | ||||
|         self.bits = self.layers = 0 | ||||
|         self._exif_offset = 0 | ||||
| 
 | ||||
|         # JPEG specifics (internal) | ||||
|         self.layer = [] | ||||
|  | @ -498,17 +500,17 @@ class JpegImageFile(ImageFile.ImageFile): | |||
|         ): | ||||
|             self.info["dpi"] = 72, 72 | ||||
| 
 | ||||
|     def _getmp(self): | ||||
|     def _getmp(self) -> dict[int, Any] | None: | ||||
|         return _getmp(self) | ||||
| 
 | ||||
| 
 | ||||
| def _getexif(self) -> dict[str, Any] | None: | ||||
| def _getexif(self: JpegImageFile) -> dict[str, Any] | None: | ||||
|     if "exif" not in self.info: | ||||
|         return None | ||||
|     return self.getexif()._get_merged_dict() | ||||
| 
 | ||||
| 
 | ||||
| def _getmp(self): | ||||
| def _getmp(self: JpegImageFile) -> dict[int, Any] | None: | ||||
|     # Extract MP information.  This method was inspired by the "highly | ||||
|     # experimental" _getexif version that's been in use for years now, | ||||
|     # itself based on the ImageFileDirectory class in the TIFF plugin. | ||||
|  | @ -616,7 +618,7 @@ samplings = { | |||
| # fmt: on | ||||
| 
 | ||||
| 
 | ||||
| def get_sampling(im): | ||||
| def get_sampling(im: Image.Image) -> int: | ||||
|     # There's no subsampling when images have only 1 layer | ||||
|     # (grayscale images) or when they are CMYK (4 layers), | ||||
|     # so set subsampling to the default value. | ||||
|  | @ -624,7 +626,7 @@ def get_sampling(im): | |||
|     # NOTE: currently Pillow can't encode JPEG to YCCK format. | ||||
|     # If YCCK support is added in the future, subsampling code will have | ||||
|     # to be updated (here and in JpegEncode.c) to deal with 4 layers. | ||||
|     if not hasattr(im, "layers") or im.layers in (1, 4): | ||||
|     if not isinstance(im, JpegImageFile) or im.layers in (1, 4): | ||||
|         return -1 | ||||
|     sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3] | ||||
|     return samplings.get(sampling, -1) | ||||
|  | @ -683,7 +685,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | |||
|             raise ValueError(msg) | ||||
|         subsampling = get_sampling(im) | ||||
| 
 | ||||
|     def validate_qtables(qtables): | ||||
|     def validate_qtables( | ||||
|         qtables: ( | ||||
|             str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None | ||||
|         ) | ||||
|     ) -> list[list[int]] | None: | ||||
|         if qtables is None: | ||||
|             return qtables | ||||
|         if isinstance(qtables, str): | ||||
|  | @ -713,12 +719,12 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | |||
|                     if len(table) != 64: | ||||
|                         msg = "Invalid quantization table" | ||||
|                         raise TypeError(msg) | ||||
|                     table = array.array("H", table) | ||||
|                     table_array = array.array("H", table) | ||||
|                 except TypeError as e: | ||||
|                     msg = "Invalid quantization table" | ||||
|                     raise ValueError(msg) from e | ||||
|                 else: | ||||
|                     qtables[idx] = list(table) | ||||
|                     qtables[idx] = list(table_array) | ||||
|             return qtables | ||||
| 
 | ||||
|     if qtables == "keep": | ||||
|  | @ -825,11 +831,11 @@ def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | |||
| 
 | ||||
| ## | ||||
| # Factory for making JPEG and MPO instances | ||||
| def jpeg_factory(fp=None, filename=None): | ||||
| def jpeg_factory(fp: IO[bytes] | None = None, filename: str | bytes | None = None): | ||||
|     im = JpegImageFile(fp, filename) | ||||
|     try: | ||||
|         mpheader = im._getmp() | ||||
|         if mpheader[45057] > 1: | ||||
|         if mpheader is not None and mpheader[45057] > 1: | ||||
|             for segment, content in im.applist: | ||||
|                 if segment == "APP1" and b' hdrgm:Version="' in content: | ||||
|                     # Ultra HDR images are not yet supported | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ from __future__ import annotations | |||
| 
 | ||||
| import os | ||||
| import struct | ||||
| from typing import IO | ||||
| from typing import IO, Any, cast | ||||
| 
 | ||||
| from . import ( | ||||
|     Image, | ||||
|  | @ -111,8 +111,11 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): | |||
|         JpegImagePlugin.JpegImageFile._open(self) | ||||
|         self._after_jpeg_open() | ||||
| 
 | ||||
|     def _after_jpeg_open(self, mpheader=None): | ||||
|     def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None: | ||||
|         self.mpinfo = mpheader if mpheader is not None else self._getmp() | ||||
|         if self.mpinfo is None: | ||||
|             msg = "Image appears to be a malformed MPO file" | ||||
|             raise ValueError(msg) | ||||
|         self.n_frames = self.mpinfo[0xB001] | ||||
|         self.__mpoffsets = [ | ||||
|             mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002] | ||||
|  | @ -159,7 +162,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): | |||
|         return self.__frame | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def adopt(jpeg_instance, mpheader=None): | ||||
|     def adopt( | ||||
|         jpeg_instance: JpegImagePlugin.JpegImageFile, | ||||
|         mpheader: dict[int, Any] | None = None, | ||||
|     ) -> MpoImageFile: | ||||
|         """ | ||||
|         Transform the instance of JpegImageFile into | ||||
|         an instance of MpoImageFile. | ||||
|  | @ -171,8 +177,9 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): | |||
|         double call to _open. | ||||
|         """ | ||||
|         jpeg_instance.__class__ = MpoImageFile | ||||
|         jpeg_instance._after_jpeg_open(mpheader) | ||||
|         return jpeg_instance | ||||
|         mpo_instance = cast(MpoImageFile, jpeg_instance) | ||||
|         mpo_instance._after_jpeg_open(mpheader) | ||||
|         return mpo_instance | ||||
| 
 | ||||
| 
 | ||||
| # --------------------------------------------------------------------- | ||||
|  |  | |||
|  | @ -174,12 +174,15 @@ def _write_image(im, filename, existing_pdf, image_refs): | |||
|     return image_ref, procset | ||||
| 
 | ||||
| 
 | ||||
| def _save(im, fp, filename, save_all=False): | ||||
| def _save( | ||||
|     im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False | ||||
| ) -> None: | ||||
|     is_appending = im.encoderinfo.get("append", False) | ||||
|     filename_str = filename.decode() if isinstance(filename, bytes) else filename | ||||
|     if is_appending: | ||||
|         existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="r+b") | ||||
|         existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="r+b") | ||||
|     else: | ||||
|         existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") | ||||
|         existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="w+b") | ||||
| 
 | ||||
|     dpi = im.encoderinfo.get("dpi") | ||||
|     if dpi: | ||||
|  | @ -228,12 +231,7 @@ def _save(im, fp, filename, save_all=False): | |||
|     for im in ims: | ||||
|         im_number_of_pages = 1 | ||||
|         if save_all: | ||||
|             try: | ||||
|                 im_number_of_pages = im.n_frames | ||||
|             except AttributeError: | ||||
|                 # Image format does not have n_frames. | ||||
|                 # It is a single frame image | ||||
|                 pass | ||||
|             im_number_of_pages = getattr(im, "n_frames", 1) | ||||
|         number_of_pages += im_number_of_pages | ||||
|         for i in range(im_number_of_pages): | ||||
|             image_refs.append(existing_pdf.next_object_id(0)) | ||||
|  | @ -250,7 +248,9 @@ def _save(im, fp, filename, save_all=False): | |||
| 
 | ||||
|     page_number = 0 | ||||
|     for i, im_sequence in enumerate(ims): | ||||
|         im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] | ||||
|         im_pages: ImageSequence.Iterator | list[Image.Image] = ( | ||||
|             ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] | ||||
|         ) | ||||
|         for im in im_pages: | ||||
|             image_ref, procset = _write_image(im, filename, existing_pdf, image_refs) | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import os | |||
| import re | ||||
| import time | ||||
| import zlib | ||||
| from typing import TYPE_CHECKING, Any, NamedTuple, Union | ||||
| from typing import IO, TYPE_CHECKING, Any, NamedTuple, Union | ||||
| 
 | ||||
| 
 | ||||
| # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set | ||||
|  | @ -62,7 +62,7 @@ PDFDocEncoding = { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| def decode_text(b): | ||||
| def decode_text(b: bytes) -> str: | ||||
|     if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE: | ||||
|         return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be") | ||||
|     else: | ||||
|  | @ -99,7 +99,7 @@ class IndirectReference(IndirectReferenceTuple): | |||
|         assert isinstance(other, IndirectReference) | ||||
|         return other.object_id == self.object_id and other.generation == self.generation | ||||
| 
 | ||||
|     def __ne__(self, other): | ||||
|     def __ne__(self, other: object) -> bool: | ||||
|         return not (self == other) | ||||
| 
 | ||||
|     def __hash__(self) -> int: | ||||
|  | @ -112,13 +112,17 @@ class IndirectObjectDef(IndirectReference): | |||
| 
 | ||||
| 
 | ||||
| class XrefTable: | ||||
|     def __init__(self): | ||||
|         self.existing_entries = {}  # object ID => (offset, generation) | ||||
|         self.new_entries = {}  # object ID => (offset, generation) | ||||
|     def __init__(self) -> None: | ||||
|         self.existing_entries: dict[int, tuple[int, int]] = ( | ||||
|             {} | ||||
|         )  # object ID => (offset, generation) | ||||
|         self.new_entries: dict[int, tuple[int, int]] = ( | ||||
|             {} | ||||
|         )  # object ID => (offset, generation) | ||||
|         self.deleted_entries = {0: 65536}  # object ID => generation | ||||
|         self.reading_finished = False | ||||
| 
 | ||||
|     def __setitem__(self, key, value): | ||||
|     def __setitem__(self, key: int, value: tuple[int, int]) -> None: | ||||
|         if self.reading_finished: | ||||
|             self.new_entries[key] = value | ||||
|         else: | ||||
|  | @ -126,13 +130,13 @@ class XrefTable: | |||
|         if key in self.deleted_entries: | ||||
|             del self.deleted_entries[key] | ||||
| 
 | ||||
|     def __getitem__(self, key): | ||||
|     def __getitem__(self, key: int) -> tuple[int, int]: | ||||
|         try: | ||||
|             return self.new_entries[key] | ||||
|         except KeyError: | ||||
|             return self.existing_entries[key] | ||||
| 
 | ||||
|     def __delitem__(self, key): | ||||
|     def __delitem__(self, key: int) -> None: | ||||
|         if key in self.new_entries: | ||||
|             generation = self.new_entries[key][1] + 1 | ||||
|             del self.new_entries[key] | ||||
|  | @ -146,7 +150,7 @@ class XrefTable: | |||
|             msg = f"object ID {key} cannot be deleted because it doesn't exist" | ||||
|             raise IndexError(msg) | ||||
| 
 | ||||
|     def __contains__(self, key): | ||||
|     def __contains__(self, key: int) -> bool: | ||||
|         return key in self.existing_entries or key in self.new_entries | ||||
| 
 | ||||
|     def __len__(self) -> int: | ||||
|  | @ -156,19 +160,19 @@ class XrefTable: | |||
|             | set(self.deleted_entries.keys()) | ||||
|         ) | ||||
| 
 | ||||
|     def keys(self): | ||||
|     def keys(self) -> set[int]: | ||||
|         return ( | ||||
|             set(self.existing_entries.keys()) - set(self.deleted_entries.keys()) | ||||
|         ) | set(self.new_entries.keys()) | ||||
| 
 | ||||
|     def write(self, f): | ||||
|     def write(self, f: IO[bytes]) -> int: | ||||
|         keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys())) | ||||
|         deleted_keys = sorted(set(self.deleted_entries.keys())) | ||||
|         startxref = f.tell() | ||||
|         f.write(b"xref\n") | ||||
|         while keys: | ||||
|             # find a contiguous sequence of object IDs | ||||
|             prev = None | ||||
|             prev: int | None = None | ||||
|             for index, key in enumerate(keys): | ||||
|                 if prev is None or prev + 1 == key: | ||||
|                     prev = key | ||||
|  | @ -178,7 +182,7 @@ class XrefTable: | |||
|                     break | ||||
|             else: | ||||
|                 contiguous_keys = keys | ||||
|                 keys = None | ||||
|                 keys = [] | ||||
|             f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys))) | ||||
|             for object_id in contiguous_keys: | ||||
|                 if object_id in self.new_entries: | ||||
|  | @ -202,7 +206,9 @@ class XrefTable: | |||
| 
 | ||||
| 
 | ||||
| class PdfName: | ||||
|     def __init__(self, name): | ||||
|     name: bytes | ||||
| 
 | ||||
|     def __init__(self, name: PdfName | bytes | str) -> None: | ||||
|         if isinstance(name, PdfName): | ||||
|             self.name = name.name | ||||
|         elif isinstance(name, bytes): | ||||
|  | @ -213,7 +219,7 @@ class PdfName: | |||
|     def name_as_str(self) -> str: | ||||
|         return self.name.decode("us-ascii") | ||||
| 
 | ||||
|     def __eq__(self, other): | ||||
|     def __eq__(self, other: object) -> bool: | ||||
|         return ( | ||||
|             isinstance(other, PdfName) and other.name == self.name | ||||
|         ) or other == self.name | ||||
|  | @ -225,7 +231,7 @@ class PdfName: | |||
|         return f"{self.__class__.__name__}({repr(self.name)})" | ||||
| 
 | ||||
|     @classmethod | ||||
|     def from_pdf_stream(cls, data): | ||||
|     def from_pdf_stream(cls, data: bytes) -> PdfName: | ||||
|         return cls(PdfParser.interpret_name(data)) | ||||
| 
 | ||||
|     allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"} | ||||
|  | @ -252,13 +258,13 @@ else: | |||
| 
 | ||||
| 
 | ||||
| class PdfDict(_DictBase): | ||||
|     def __setattr__(self, key, value): | ||||
|     def __setattr__(self, key: str, value: Any) -> None: | ||||
|         if key == "data": | ||||
|             collections.UserDict.__setattr__(self, key, value) | ||||
|         else: | ||||
|             self[key.encode("us-ascii")] = value | ||||
| 
 | ||||
|     def __getattr__(self, key): | ||||
|     def __getattr__(self, key: str) -> str | time.struct_time: | ||||
|         try: | ||||
|             value = self[key.encode("us-ascii")] | ||||
|         except KeyError as e: | ||||
|  | @ -300,7 +306,7 @@ class PdfDict(_DictBase): | |||
| 
 | ||||
| 
 | ||||
| class PdfBinary: | ||||
|     def __init__(self, data): | ||||
|     def __init__(self, data: list[int] | bytes) -> None: | ||||
|         self.data = data | ||||
| 
 | ||||
|     def __bytes__(self) -> bytes: | ||||
|  | @ -308,27 +314,27 @@ class PdfBinary: | |||
| 
 | ||||
| 
 | ||||
| class PdfStream: | ||||
|     def __init__(self, dictionary, buf): | ||||
|     def __init__(self, dictionary: PdfDict, buf: bytes) -> None: | ||||
|         self.dictionary = dictionary | ||||
|         self.buf = buf | ||||
| 
 | ||||
|     def decode(self): | ||||
|     def decode(self) -> bytes: | ||||
|         try: | ||||
|             filter = self.dictionary.Filter | ||||
|         except AttributeError: | ||||
|             filter = self.dictionary[b"Filter"] | ||||
|         except KeyError: | ||||
|             return self.buf | ||||
|         if filter == b"FlateDecode": | ||||
|             try: | ||||
|                 expected_length = self.dictionary.DL | ||||
|             except AttributeError: | ||||
|                 expected_length = self.dictionary.Length | ||||
|                 expected_length = self.dictionary[b"DL"] | ||||
|             except KeyError: | ||||
|                 expected_length = self.dictionary[b"Length"] | ||||
|             return zlib.decompress(self.buf, bufsize=int(expected_length)) | ||||
|         else: | ||||
|             msg = f"stream filter {repr(self.dictionary.Filter)} unknown/unsupported" | ||||
|             msg = f"stream filter {repr(filter)} unknown/unsupported" | ||||
|             raise NotImplementedError(msg) | ||||
| 
 | ||||
| 
 | ||||
| def pdf_repr(x): | ||||
| def pdf_repr(x: Any) -> bytes: | ||||
|     if x is True: | ||||
|         return b"true" | ||||
|     elif x is False: | ||||
|  | @ -363,12 +369,19 @@ class PdfParser: | |||
|     Supports PDF up to 1.4 | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"): | ||||
|     def __init__( | ||||
|         self, | ||||
|         filename: str | None = None, | ||||
|         f: IO[bytes] | None = None, | ||||
|         buf: bytes | bytearray | None = None, | ||||
|         start_offset: int = 0, | ||||
|         mode: str = "rb", | ||||
|     ) -> None: | ||||
|         if buf and f: | ||||
|             msg = "specify buf or f or filename, but not both buf and f" | ||||
|             raise RuntimeError(msg) | ||||
|         self.filename = filename | ||||
|         self.buf = buf | ||||
|         self.buf: bytes | bytearray | mmap.mmap | None = buf | ||||
|         self.f = f | ||||
|         self.start_offset = start_offset | ||||
|         self.should_close_buf = False | ||||
|  | @ -377,12 +390,16 @@ class PdfParser: | |||
|             self.f = f = open(filename, mode) | ||||
|             self.should_close_file = True | ||||
|         if f is not None: | ||||
|             self.buf = buf = self.get_buf_from_file(f) | ||||
|             self.buf = self.get_buf_from_file(f) | ||||
|             self.should_close_buf = True | ||||
|             if not filename and hasattr(f, "name"): | ||||
|                 self.filename = f.name | ||||
|         self.cached_objects = {} | ||||
|         if buf: | ||||
|         self.cached_objects: dict[IndirectReference, Any] = {} | ||||
|         self.root_ref: IndirectReference | None | ||||
|         self.info_ref: IndirectReference | None | ||||
|         self.pages_ref: IndirectReference | None | ||||
|         self.last_xref_section_offset: int | None | ||||
|         if self.buf: | ||||
|             self.read_pdf_info() | ||||
|         else: | ||||
|             self.file_size_total = self.file_size_this = 0 | ||||
|  | @ -390,12 +407,12 @@ class PdfParser: | |||
|             self.root_ref = None | ||||
|             self.info = PdfDict() | ||||
|             self.info_ref = None | ||||
|             self.page_tree_root = {} | ||||
|             self.pages = [] | ||||
|             self.orig_pages = [] | ||||
|             self.page_tree_root = PdfDict() | ||||
|             self.pages: list[IndirectReference] = [] | ||||
|             self.orig_pages: list[IndirectReference] = [] | ||||
|             self.pages_ref = None | ||||
|             self.last_xref_section_offset = None | ||||
|             self.trailer_dict = {} | ||||
|             self.trailer_dict: dict[bytes, Any] = {} | ||||
|             self.xref_table = XrefTable() | ||||
|         self.xref_table.reading_finished = True | ||||
|         if f: | ||||
|  | @ -412,10 +429,8 @@ class PdfParser: | |||
|         self.seek_end() | ||||
| 
 | ||||
|     def close_buf(self) -> None: | ||||
|         try: | ||||
|         if isinstance(self.buf, mmap.mmap): | ||||
|             self.buf.close() | ||||
|         except AttributeError: | ||||
|             pass | ||||
|         self.buf = None | ||||
| 
 | ||||
|     def close(self) -> None: | ||||
|  | @ -426,15 +441,19 @@ class PdfParser: | |||
|             self.f = None | ||||
| 
 | ||||
|     def seek_end(self) -> None: | ||||
|         assert self.f is not None | ||||
|         self.f.seek(0, os.SEEK_END) | ||||
| 
 | ||||
|     def write_header(self) -> None: | ||||
|         assert self.f is not None | ||||
|         self.f.write(b"%PDF-1.4\n") | ||||
| 
 | ||||
|     def write_comment(self, s): | ||||
|     def write_comment(self, s: str) -> None: | ||||
|         assert self.f is not None | ||||
|         self.f.write(f"% {s}\n".encode()) | ||||
| 
 | ||||
|     def write_catalog(self) -> IndirectReference: | ||||
|         assert self.f is not None | ||||
|         self.del_root() | ||||
|         self.root_ref = self.next_object_id(self.f.tell()) | ||||
|         self.pages_ref = self.next_object_id(0) | ||||
|  | @ -477,7 +496,10 @@ class PdfParser: | |||
|                 pages_tree_node_ref = pages_tree_node.get(b"Parent", None) | ||||
|         self.orig_pages = [] | ||||
| 
 | ||||
|     def write_xref_and_trailer(self, new_root_ref=None): | ||||
|     def write_xref_and_trailer( | ||||
|         self, new_root_ref: IndirectReference | None = None | ||||
|     ) -> None: | ||||
|         assert self.f is not None | ||||
|         if new_root_ref: | ||||
|             self.del_root() | ||||
|             self.root_ref = new_root_ref | ||||
|  | @ -485,7 +507,10 @@ class PdfParser: | |||
|             self.info_ref = self.write_obj(None, self.info) | ||||
|         start_xref = self.xref_table.write(self.f) | ||||
|         num_entries = len(self.xref_table) | ||||
|         trailer_dict = {b"Root": self.root_ref, b"Size": num_entries} | ||||
|         trailer_dict: dict[str | bytes, Any] = { | ||||
|             b"Root": self.root_ref, | ||||
|             b"Size": num_entries, | ||||
|         } | ||||
|         if self.last_xref_section_offset is not None: | ||||
|             trailer_dict[b"Prev"] = self.last_xref_section_offset | ||||
|         if self.info: | ||||
|  | @ -497,16 +522,20 @@ class PdfParser: | |||
|             + b"\nstartxref\n%d\n%%%%EOF" % start_xref | ||||
|         ) | ||||
| 
 | ||||
|     def write_page(self, ref, *objs, **dict_obj): | ||||
|         if isinstance(ref, int): | ||||
|             ref = self.pages[ref] | ||||
|     def write_page( | ||||
|         self, ref: int | IndirectReference | None, *objs: Any, **dict_obj: Any | ||||
|     ) -> IndirectReference: | ||||
|         obj_ref = self.pages[ref] if isinstance(ref, int) else ref | ||||
|         if "Type" not in dict_obj: | ||||
|             dict_obj["Type"] = PdfName(b"Page") | ||||
|         if "Parent" not in dict_obj: | ||||
|             dict_obj["Parent"] = self.pages_ref | ||||
|         return self.write_obj(ref, *objs, **dict_obj) | ||||
|         return self.write_obj(obj_ref, *objs, **dict_obj) | ||||
| 
 | ||||
|     def write_obj(self, ref, *objs, **dict_obj): | ||||
|     def write_obj( | ||||
|         self, ref: IndirectReference | None, *objs: Any, **dict_obj: Any | ||||
|     ) -> IndirectReference: | ||||
|         assert self.f is not None | ||||
|         f = self.f | ||||
|         if ref is None: | ||||
|             ref = self.next_object_id(f.tell()) | ||||
|  | @ -534,7 +563,7 @@ class PdfParser: | |||
|         del self.xref_table[self.root[b"Pages"].object_id] | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def get_buf_from_file(f): | ||||
|     def get_buf_from_file(f: IO[bytes]) -> bytes | mmap.mmap: | ||||
|         if hasattr(f, "getbuffer"): | ||||
|             return f.getbuffer() | ||||
|         elif hasattr(f, "getvalue"): | ||||
|  | @ -546,10 +575,15 @@ class PdfParser: | |||
|                 return b"" | ||||
| 
 | ||||
|     def read_pdf_info(self) -> None: | ||||
|         assert self.buf is not None | ||||
|         self.file_size_total = len(self.buf) | ||||
|         self.file_size_this = self.file_size_total - self.start_offset | ||||
|         self.read_trailer() | ||||
|         check_format_condition( | ||||
|             self.trailer_dict.get(b"Root") is not None, "Root is missing" | ||||
|         ) | ||||
|         self.root_ref = self.trailer_dict[b"Root"] | ||||
|         assert self.root_ref is not None | ||||
|         self.info_ref = self.trailer_dict.get(b"Info", None) | ||||
|         self.root = PdfDict(self.read_indirect(self.root_ref)) | ||||
|         if self.info_ref is None: | ||||
|  | @ -560,12 +594,15 @@ class PdfParser: | |||
|         check_format_condition( | ||||
|             self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog" | ||||
|         ) | ||||
|         check_format_condition(b"Pages" in self.root, "/Pages missing in Root") | ||||
|         check_format_condition( | ||||
|             self.root.get(b"Pages") is not None, "/Pages missing in Root" | ||||
|         ) | ||||
|         check_format_condition( | ||||
|             isinstance(self.root[b"Pages"], IndirectReference), | ||||
|             "/Pages in Root is not an indirect reference", | ||||
|         ) | ||||
|         self.pages_ref = self.root[b"Pages"] | ||||
|         assert self.pages_ref is not None | ||||
|         self.page_tree_root = self.read_indirect(self.pages_ref) | ||||
|         self.pages = self.linearize_page_tree(self.page_tree_root) | ||||
|         # save the original list of page references | ||||
|  | @ -573,7 +610,7 @@ class PdfParser: | |||
|         # and we need to rewrite the pages and their list | ||||
|         self.orig_pages = self.pages[:] | ||||
| 
 | ||||
|     def next_object_id(self, offset=None): | ||||
|     def next_object_id(self, offset: int | None = None) -> IndirectReference: | ||||
|         try: | ||||
|             # TODO: support reuse of deleted objects | ||||
|             reference = IndirectReference(max(self.xref_table.keys()) + 1, 0) | ||||
|  | @ -623,12 +660,13 @@ class PdfParser: | |||
|         re.DOTALL, | ||||
|     ) | ||||
| 
 | ||||
|     def read_trailer(self): | ||||
|     def read_trailer(self) -> None: | ||||
|         assert self.buf is not None | ||||
|         search_start_offset = len(self.buf) - 16384 | ||||
|         if search_start_offset < self.start_offset: | ||||
|             search_start_offset = self.start_offset | ||||
|         m = self.re_trailer_end.search(self.buf, search_start_offset) | ||||
|         check_format_condition(m, "trailer end not found") | ||||
|         check_format_condition(m is not None, "trailer end not found") | ||||
|         # make sure we found the LAST trailer | ||||
|         last_match = m | ||||
|         while m: | ||||
|  | @ -636,6 +674,7 @@ class PdfParser: | |||
|             m = self.re_trailer_end.search(self.buf, m.start() + 16) | ||||
|         if not m: | ||||
|             m = last_match | ||||
|         assert m is not None | ||||
|         trailer_data = m.group(1) | ||||
|         self.last_xref_section_offset = int(m.group(2)) | ||||
|         self.trailer_dict = self.interpret_trailer(trailer_data) | ||||
|  | @ -644,12 +683,14 @@ class PdfParser: | |||
|         if b"Prev" in self.trailer_dict: | ||||
|             self.read_prev_trailer(self.trailer_dict[b"Prev"]) | ||||
| 
 | ||||
|     def read_prev_trailer(self, xref_section_offset): | ||||
|     def read_prev_trailer(self, xref_section_offset: int) -> None: | ||||
|         assert self.buf is not None | ||||
|         trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset) | ||||
|         m = self.re_trailer_prev.search( | ||||
|             self.buf[trailer_offset : trailer_offset + 16384] | ||||
|         ) | ||||
|         check_format_condition(m, "previous trailer not found") | ||||
|         check_format_condition(m is not None, "previous trailer not found") | ||||
|         assert m is not None | ||||
|         trailer_data = m.group(1) | ||||
|         check_format_condition( | ||||
|             int(m.group(2)) == xref_section_offset, | ||||
|  | @ -670,7 +711,7 @@ class PdfParser: | |||
|     re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def interpret_trailer(cls, trailer_data): | ||||
|     def interpret_trailer(cls, trailer_data: bytes) -> dict[bytes, Any]: | ||||
|         trailer = {} | ||||
|         offset = 0 | ||||
|         while True: | ||||
|  | @ -678,14 +719,18 @@ class PdfParser: | |||
|             if not m: | ||||
|                 m = cls.re_dict_end.match(trailer_data, offset) | ||||
|                 check_format_condition( | ||||
|                     m and m.end() == len(trailer_data), | ||||
|                     m is not None and m.end() == len(trailer_data), | ||||
|                     "name not found in trailer, remaining data: " | ||||
|                     + repr(trailer_data[offset:]), | ||||
|                 ) | ||||
|                 break | ||||
|             key = cls.interpret_name(m.group(1)) | ||||
|             value, offset = cls.get_value(trailer_data, m.end()) | ||||
|             assert isinstance(key, bytes) | ||||
|             value, value_offset = cls.get_value(trailer_data, m.end()) | ||||
|             trailer[key] = value | ||||
|             if value_offset is None: | ||||
|                 break | ||||
|             offset = value_offset | ||||
|         check_format_condition( | ||||
|             b"Size" in trailer and isinstance(trailer[b"Size"], int), | ||||
|             "/Size not in trailer or not an integer", | ||||
|  | @ -699,7 +744,7 @@ class PdfParser: | |||
|     re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?") | ||||
| 
 | ||||
|     @classmethod | ||||
|     def interpret_name(cls, raw, as_text=False): | ||||
|     def interpret_name(cls, raw: bytes, as_text: bool = False) -> str | bytes: | ||||
|         name = b"" | ||||
|         for m in cls.re_hashes_in_name.finditer(raw): | ||||
|             if m.group(3): | ||||
|  | @ -761,7 +806,13 @@ class PdfParser: | |||
|     ) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1): | ||||
|     def get_value( | ||||
|         cls, | ||||
|         data: bytes | bytearray | mmap.mmap, | ||||
|         offset: int, | ||||
|         expect_indirect: IndirectReference | None = None, | ||||
|         max_nesting: int = -1, | ||||
|     ) -> tuple[Any, int | None]: | ||||
|         if max_nesting == 0: | ||||
|             return None, None | ||||
|         m = cls.re_comment.match(data, offset) | ||||
|  | @ -783,11 +834,16 @@ class PdfParser: | |||
|                 == IndirectReference(int(m.group(1)), int(m.group(2))), | ||||
|                 "indirect object definition different than expected", | ||||
|             ) | ||||
|             object, offset = cls.get_value(data, m.end(), max_nesting=max_nesting - 1) | ||||
|             if offset is None: | ||||
|             object, object_offset = cls.get_value( | ||||
|                 data, m.end(), max_nesting=max_nesting - 1 | ||||
|             ) | ||||
|             if object_offset is None: | ||||
|                 return object, None | ||||
|             m = cls.re_indirect_def_end.match(data, offset) | ||||
|             check_format_condition(m, "indirect object definition end not found") | ||||
|             m = cls.re_indirect_def_end.match(data, object_offset) | ||||
|             check_format_condition( | ||||
|                 m is not None, "indirect object definition end not found" | ||||
|             ) | ||||
|             assert m is not None | ||||
|             return object, m.end() | ||||
|         check_format_condition( | ||||
|             not expect_indirect, "indirect object definition not found" | ||||
|  | @ -806,46 +862,53 @@ class PdfParser: | |||
|         m = cls.re_dict_start.match(data, offset) | ||||
|         if m: | ||||
|             offset = m.end() | ||||
|             result = {} | ||||
|             result: dict[Any, Any] = {} | ||||
|             m = cls.re_dict_end.match(data, offset) | ||||
|             current_offset: int | None = offset | ||||
|             while not m: | ||||
|                 key, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) | ||||
|                 if offset is None: | ||||
|                 assert current_offset is not None | ||||
|                 key, current_offset = cls.get_value( | ||||
|                     data, current_offset, max_nesting=max_nesting - 1 | ||||
|                 ) | ||||
|                 if current_offset is None: | ||||
|                     return result, None | ||||
|                 value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) | ||||
|                 value, current_offset = cls.get_value( | ||||
|                     data, current_offset, max_nesting=max_nesting - 1 | ||||
|                 ) | ||||
|                 result[key] = value | ||||
|                 if offset is None: | ||||
|                 if current_offset is None: | ||||
|                     return result, None | ||||
|                 m = cls.re_dict_end.match(data, offset) | ||||
|             offset = m.end() | ||||
|             m = cls.re_stream_start.match(data, offset) | ||||
|                 m = cls.re_dict_end.match(data, current_offset) | ||||
|             current_offset = m.end() | ||||
|             m = cls.re_stream_start.match(data, current_offset) | ||||
|             if m: | ||||
|                 try: | ||||
|                     stream_len_str = result.get(b"Length") | ||||
|                     stream_len = int(stream_len_str) | ||||
|                 except (TypeError, ValueError) as e: | ||||
|                     msg = f"bad or missing Length in stream dict ({stream_len_str})" | ||||
|                     raise PdfFormatError(msg) from e | ||||
|                 stream_len = result.get(b"Length") | ||||
|                 if stream_len is None or not isinstance(stream_len, int): | ||||
|                     msg = f"bad or missing Length in stream dict ({stream_len})" | ||||
|                     raise PdfFormatError(msg) | ||||
|                 stream_data = data[m.end() : m.end() + stream_len] | ||||
|                 m = cls.re_stream_end.match(data, m.end() + stream_len) | ||||
|                 check_format_condition(m, "stream end not found") | ||||
|                 offset = m.end() | ||||
|                 result = PdfStream(PdfDict(result), stream_data) | ||||
|             else: | ||||
|                 result = PdfDict(result) | ||||
|             return result, offset | ||||
|                 check_format_condition(m is not None, "stream end not found") | ||||
|                 assert m is not None | ||||
|                 current_offset = m.end() | ||||
|                 return PdfStream(PdfDict(result), stream_data), current_offset | ||||
|             return PdfDict(result), current_offset | ||||
|         m = cls.re_array_start.match(data, offset) | ||||
|         if m: | ||||
|             offset = m.end() | ||||
|             result = [] | ||||
|             results = [] | ||||
|             m = cls.re_array_end.match(data, offset) | ||||
|             current_offset = offset | ||||
|             while not m: | ||||
|                 value, offset = cls.get_value(data, offset, max_nesting=max_nesting - 1) | ||||
|                 result.append(value) | ||||
|                 if offset is None: | ||||
|                     return result, None | ||||
|                 m = cls.re_array_end.match(data, offset) | ||||
|             return result, m.end() | ||||
|                 assert current_offset is not None | ||||
|                 value, current_offset = cls.get_value( | ||||
|                     data, current_offset, max_nesting=max_nesting - 1 | ||||
|                 ) | ||||
|                 results.append(value) | ||||
|                 if current_offset is None: | ||||
|                     return results, None | ||||
|                 m = cls.re_array_end.match(data, current_offset) | ||||
|             return results, m.end() | ||||
|         m = cls.re_null.match(data, offset) | ||||
|         if m: | ||||
|             return None, m.end() | ||||
|  | @ -905,7 +968,9 @@ class PdfParser: | |||
|     } | ||||
| 
 | ||||
|     @classmethod | ||||
|     def get_literal_string(cls, data, offset): | ||||
|     def get_literal_string( | ||||
|         cls, data: bytes | bytearray | mmap.mmap, offset: int | ||||
|     ) -> tuple[bytes, int]: | ||||
|         nesting_depth = 0 | ||||
|         result = bytearray() | ||||
|         for m in cls.re_lit_str_token.finditer(data, offset): | ||||
|  | @ -941,12 +1006,14 @@ class PdfParser: | |||
|     ) | ||||
|     re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)") | ||||
| 
 | ||||
|     def read_xref_table(self, xref_section_offset): | ||||
|     def read_xref_table(self, xref_section_offset: int) -> int: | ||||
|         assert self.buf is not None | ||||
|         subsection_found = False | ||||
|         m = self.re_xref_section_start.match( | ||||
|             self.buf, xref_section_offset + self.start_offset | ||||
|         ) | ||||
|         check_format_condition(m, "xref section start not found") | ||||
|         check_format_condition(m is not None, "xref section start not found") | ||||
|         assert m is not None | ||||
|         offset = m.end() | ||||
|         while True: | ||||
|             m = self.re_xref_subsection_start.match(self.buf, offset) | ||||
|  | @ -961,7 +1028,8 @@ class PdfParser: | |||
|             num_objects = int(m.group(2)) | ||||
|             for i in range(first_object, first_object + num_objects): | ||||
|                 m = self.re_xref_entry.match(self.buf, offset) | ||||
|                 check_format_condition(m, "xref entry not found") | ||||
|                 check_format_condition(m is not None, "xref entry not found") | ||||
|                 assert m is not None | ||||
|                 offset = m.end() | ||||
|                 is_free = m.group(3) == b"f" | ||||
|                 if not is_free: | ||||
|  | @ -971,13 +1039,14 @@ class PdfParser: | |||
|                         self.xref_table[i] = new_entry | ||||
|         return offset | ||||
| 
 | ||||
|     def read_indirect(self, ref, max_nesting=-1): | ||||
|     def read_indirect(self, ref: IndirectReference, max_nesting: int = -1) -> Any: | ||||
|         offset, generation = self.xref_table[ref[0]] | ||||
|         check_format_condition( | ||||
|             generation == ref[1], | ||||
|             f"expected to find generation {ref[1]} for object ID {ref[0]} in xref " | ||||
|             f"table, instead found generation {generation} at offset {offset}", | ||||
|         ) | ||||
|         assert self.buf is not None | ||||
|         value = self.get_value( | ||||
|             self.buf, | ||||
|             offset + self.start_offset, | ||||
|  | @ -987,14 +1056,15 @@ class PdfParser: | |||
|         self.cached_objects[ref] = value | ||||
|         return value | ||||
| 
 | ||||
|     def linearize_page_tree(self, node=None): | ||||
|         if node is None: | ||||
|             node = self.page_tree_root | ||||
|     def linearize_page_tree( | ||||
|         self, node: PdfDict | None = None | ||||
|     ) -> list[IndirectReference]: | ||||
|         page_node = node if node is not None else self.page_tree_root | ||||
|         check_format_condition( | ||||
|             node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages" | ||||
|             page_node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages" | ||||
|         ) | ||||
|         pages = [] | ||||
|         for kid in node[b"Kids"]: | ||||
|         for kid in page_node[b"Kids"]: | ||||
|             kid_object = self.read_indirect(kid) | ||||
|             if kid_object[b"Type"] == b"Page": | ||||
|                 pages.append(kid) | ||||
|  |  | |||
|  | @ -39,7 +39,7 @@ import struct | |||
| import warnings | ||||
| import zlib | ||||
| from enum import IntEnum | ||||
| from typing import IO, TYPE_CHECKING, Any, NoReturn | ||||
| from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn | ||||
| 
 | ||||
| from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence | ||||
| from ._binary import i16be as i16 | ||||
|  | @ -144,7 +144,7 @@ def _safe_zlib_decompress(s): | |||
|     return plaintext | ||||
| 
 | ||||
| 
 | ||||
| def _crc32(data, seed=0): | ||||
| def _crc32(data: bytes, seed: int = 0) -> int: | ||||
|     return zlib.crc32(data, seed) & 0xFFFFFFFF | ||||
| 
 | ||||
| 
 | ||||
|  | @ -191,7 +191,7 @@ class ChunkStream: | |||
|         assert self.queue is not None | ||||
|         self.queue.append((cid, pos, length)) | ||||
| 
 | ||||
|     def call(self, cid, pos, length): | ||||
|     def call(self, cid: bytes, pos: int, length: int) -> bytes: | ||||
|         """Call the appropriate chunk handler""" | ||||
| 
 | ||||
|         logger.debug("STREAM %r %s %s", cid, pos, length) | ||||
|  | @ -230,6 +230,7 @@ class ChunkStream: | |||
| 
 | ||||
|         cids = [] | ||||
| 
 | ||||
|         assert self.fp is not None | ||||
|         while True: | ||||
|             try: | ||||
|                 cid, pos, length = self.read() | ||||
|  | @ -407,6 +408,7 @@ class PngStream(ChunkStream): | |||
| 
 | ||||
|     def chunk_iCCP(self, pos: int, length: int) -> bytes: | ||||
|         # ICC profile | ||||
|         assert self.fp is not None | ||||
|         s = ImageFile._safe_read(self.fp, length) | ||||
|         # according to PNG spec, the iCCP chunk contains: | ||||
|         # Profile name  1-79 bytes (character string) | ||||
|  | @ -434,6 +436,7 @@ class PngStream(ChunkStream): | |||
| 
 | ||||
|     def chunk_IHDR(self, pos: int, length: int) -> bytes: | ||||
|         # image header | ||||
|         assert self.fp is not None | ||||
|         s = ImageFile._safe_read(self.fp, length) | ||||
|         if length < 13: | ||||
|             if ImageFile.LOAD_TRUNCATED_IMAGES: | ||||
|  | @ -471,6 +474,7 @@ class PngStream(ChunkStream): | |||
| 
 | ||||
|     def chunk_PLTE(self, pos: int, length: int) -> bytes: | ||||
|         # palette | ||||
|         assert self.fp is not None | ||||
|         s = ImageFile._safe_read(self.fp, length) | ||||
|         if self.im_mode == "P": | ||||
|             self.im_palette = "RGB", s | ||||
|  | @ -478,6 +482,7 @@ class PngStream(ChunkStream): | |||
| 
 | ||||
|     def chunk_tRNS(self, pos: int, length: int) -> bytes: | ||||
|         # transparency | ||||
|         assert self.fp is not None | ||||
|         s = ImageFile._safe_read(self.fp, length) | ||||
|         if self.im_mode == "P": | ||||
|             if _simple_palette.match(s): | ||||
|  | @ -498,6 +503,7 @@ class PngStream(ChunkStream): | |||
| 
 | ||||
|     def chunk_gAMA(self, pos: int, length: int) -> bytes: | ||||
|         # gamma setting | ||||
|         assert self.fp is not None | ||||
|         s = ImageFile._safe_read(self.fp, length) | ||||
|         self.im_info["gamma"] = i32(s) / 100000.0 | ||||
|         return s | ||||
|  | @ -506,6 +512,7 @@ class PngStream(ChunkStream): | |||
|         # chromaticity, 8 unsigned ints, actual value is scaled by 100,000 | ||||
|         # WP x,y, Red x,y, Green x,y Blue x,y | ||||
| 
 | ||||
|         assert self.fp is not None | ||||
|         s = ImageFile._safe_read(self.fp, length) | ||||
|         raw_vals = struct.unpack(">%dI" % (len(s) // 4), s) | ||||
|         self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals) | ||||
|  | @ -518,6 +525,7 @@ class PngStream(ChunkStream): | |||
|         # 2 saturation | ||||
|         # 3 absolute colorimetric | ||||
| 
 | ||||
|         assert self.fp is not None | ||||
|         s = ImageFile._safe_read(self.fp, length) | ||||
|         if length < 1: | ||||
|             if ImageFile.LOAD_TRUNCATED_IMAGES: | ||||
|  | @ -529,6 +537,7 @@ class PngStream(ChunkStream): | |||
| 
 | ||||
|     def chunk_pHYs(self, pos: int, length: int) -> bytes: | ||||
|         # pixels per unit | ||||
|         assert self.fp is not None | ||||
|         s = ImageFile._safe_read(self.fp, length) | ||||
|         if length < 9: | ||||
|             if ImageFile.LOAD_TRUNCATED_IMAGES: | ||||
|  | @ -546,6 +555,7 @@ class PngStream(ChunkStream): | |||
| 
 | ||||
|     def chunk_tEXt(self, pos: int, length: int) -> bytes: | ||||
|         # text | ||||
|         assert self.fp is not None | ||||
|         s = ImageFile._safe_read(self.fp, length) | ||||
|         try: | ||||
|             k, v = s.split(b"\0", 1) | ||||
|  | @ -554,17 +564,18 @@ class PngStream(ChunkStream): | |||
|             k = s | ||||
|             v = b"" | ||||
|         if k: | ||||
|             k = k.decode("latin-1", "strict") | ||||
|             k_str = k.decode("latin-1", "strict") | ||||
|             v_str = v.decode("latin-1", "replace") | ||||
| 
 | ||||
|             self.im_info[k] = v if k == "exif" else v_str | ||||
|             self.im_text[k] = v_str | ||||
|             self.im_info[k_str] = v if k == b"exif" else v_str | ||||
|             self.im_text[k_str] = v_str | ||||
|             self.check_text_memory(len(v_str)) | ||||
| 
 | ||||
|         return s | ||||
| 
 | ||||
|     def chunk_zTXt(self, pos: int, length: int) -> bytes: | ||||
|         # compressed text | ||||
|         assert self.fp is not None | ||||
|         s = ImageFile._safe_read(self.fp, length) | ||||
|         try: | ||||
|             k, v = s.split(b"\0", 1) | ||||
|  | @ -589,16 +600,17 @@ class PngStream(ChunkStream): | |||
|             v = b"" | ||||
| 
 | ||||
|         if k: | ||||
|             k = k.decode("latin-1", "strict") | ||||
|             v = v.decode("latin-1", "replace") | ||||
|             k_str = k.decode("latin-1", "strict") | ||||
|             v_str = v.decode("latin-1", "replace") | ||||
| 
 | ||||
|             self.im_info[k] = self.im_text[k] = v | ||||
|             self.check_text_memory(len(v)) | ||||
|             self.im_info[k_str] = self.im_text[k_str] = v_str | ||||
|             self.check_text_memory(len(v_str)) | ||||
| 
 | ||||
|         return s | ||||
| 
 | ||||
|     def chunk_iTXt(self, pos: int, length: int) -> bytes: | ||||
|         # international text | ||||
|         assert self.fp is not None | ||||
|         r = s = ImageFile._safe_read(self.fp, length) | ||||
|         try: | ||||
|             k, r = r.split(b"\0", 1) | ||||
|  | @ -627,25 +639,27 @@ class PngStream(ChunkStream): | |||
|         if k == b"XML:com.adobe.xmp": | ||||
|             self.im_info["xmp"] = v | ||||
|         try: | ||||
|             k = k.decode("latin-1", "strict") | ||||
|             lang = lang.decode("utf-8", "strict") | ||||
|             tk = tk.decode("utf-8", "strict") | ||||
|             v = v.decode("utf-8", "strict") | ||||
|             k_str = k.decode("latin-1", "strict") | ||||
|             lang_str = lang.decode("utf-8", "strict") | ||||
|             tk_str = tk.decode("utf-8", "strict") | ||||
|             v_str = v.decode("utf-8", "strict") | ||||
|         except UnicodeError: | ||||
|             return s | ||||
| 
 | ||||
|         self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk) | ||||
|         self.check_text_memory(len(v)) | ||||
|         self.im_info[k_str] = self.im_text[k_str] = iTXt(v_str, lang_str, tk_str) | ||||
|         self.check_text_memory(len(v_str)) | ||||
| 
 | ||||
|         return s | ||||
| 
 | ||||
|     def chunk_eXIf(self, pos: int, length: int) -> bytes: | ||||
|         assert self.fp is not None | ||||
|         s = ImageFile._safe_read(self.fp, length) | ||||
|         self.im_info["exif"] = b"Exif\x00\x00" + s | ||||
|         return s | ||||
| 
 | ||||
|     # APNG chunks | ||||
|     def chunk_acTL(self, pos: int, length: int) -> bytes: | ||||
|         assert self.fp is not None | ||||
|         s = ImageFile._safe_read(self.fp, length) | ||||
|         if length < 8: | ||||
|             if ImageFile.LOAD_TRUNCATED_IMAGES: | ||||
|  | @ -666,6 +680,7 @@ class PngStream(ChunkStream): | |||
|         return s | ||||
| 
 | ||||
|     def chunk_fcTL(self, pos: int, length: int) -> bytes: | ||||
|         assert self.fp is not None | ||||
|         s = ImageFile._safe_read(self.fp, length) | ||||
|         if length < 26: | ||||
|             if ImageFile.LOAD_TRUNCATED_IMAGES: | ||||
|  | @ -695,6 +710,7 @@ class PngStream(ChunkStream): | |||
|         return s | ||||
| 
 | ||||
|     def chunk_fdAT(self, pos: int, length: int) -> bytes: | ||||
|         assert self.fp is not None | ||||
|         if length < 4: | ||||
|             if ImageFile.LOAD_TRUNCATED_IMAGES: | ||||
|                 s = ImageFile._safe_read(self.fp, length) | ||||
|  | @ -1075,21 +1091,21 @@ _OUTMODES = { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| def putchunk(fp, cid, *data): | ||||
| def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None: | ||||
|     """Write a PNG chunk (including CRC field)""" | ||||
| 
 | ||||
|     data = b"".join(data) | ||||
|     byte_data = b"".join(data) | ||||
| 
 | ||||
|     fp.write(o32(len(data)) + cid) | ||||
|     fp.write(data) | ||||
|     crc = _crc32(data, _crc32(cid)) | ||||
|     fp.write(o32(len(byte_data)) + cid) | ||||
|     fp.write(byte_data) | ||||
|     crc = _crc32(byte_data, _crc32(cid)) | ||||
|     fp.write(o32(crc)) | ||||
| 
 | ||||
| 
 | ||||
| class _idat: | ||||
|     # wrap output from the encoder in IDAT chunks | ||||
| 
 | ||||
|     def __init__(self, fp, chunk): | ||||
|     def __init__(self, fp, chunk) -> None: | ||||
|         self.fp = fp | ||||
|         self.chunk = chunk | ||||
| 
 | ||||
|  | @ -1100,7 +1116,7 @@ class _idat: | |||
| class _fdat: | ||||
|     # wrap encoder output in fdAT chunks | ||||
| 
 | ||||
|     def __init__(self, fp, chunk, seq_num): | ||||
|     def __init__(self, fp: IO[bytes], chunk, seq_num: int) -> None: | ||||
|         self.fp = fp | ||||
|         self.chunk = chunk | ||||
|         self.seq_num = seq_num | ||||
|  | @ -1110,7 +1126,21 @@ class _fdat: | |||
|         self.seq_num += 1 | ||||
| 
 | ||||
| 
 | ||||
| def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_images): | ||||
| class _Frame(NamedTuple): | ||||
|     im: Image.Image | ||||
|     bbox: tuple[int, int, int, int] | None | ||||
|     encoderinfo: dict[str, Any] | ||||
| 
 | ||||
| 
 | ||||
| def _write_multiple_frames( | ||||
|     im: Image.Image, | ||||
|     fp: IO[bytes], | ||||
|     chunk, | ||||
|     mode: str, | ||||
|     rawmode: str, | ||||
|     default_image: Image.Image | None, | ||||
|     append_images: list[Image.Image], | ||||
| ) -> Image.Image | None: | ||||
|     duration = im.encoderinfo.get("duration") | ||||
|     loop = im.encoderinfo.get("loop", im.info.get("loop", 0)) | ||||
|     disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE)) | ||||
|  | @ -1126,7 +1156,7 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i | |||
|         for imSequence in imSequences: | ||||
|             total += getattr(imSequence, "n_frames", 1) | ||||
| 
 | ||||
|     im_frames = [] | ||||
|     im_frames: list[_Frame] = [] | ||||
|     frame_count = 0 | ||||
|     for i, imSequence in enumerate(imSequences): | ||||
|         for im_frame in ImageSequence.Iterator(imSequence): | ||||
|  | @ -1147,24 +1177,24 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i | |||
| 
 | ||||
|             if im_frames: | ||||
|                 previous = im_frames[-1] | ||||
|                 prev_disposal = previous["encoderinfo"].get("disposal") | ||||
|                 prev_blend = previous["encoderinfo"].get("blend") | ||||
|                 prev_disposal = previous.encoderinfo.get("disposal") | ||||
|                 prev_blend = previous.encoderinfo.get("blend") | ||||
|                 if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2: | ||||
|                     prev_disposal = Disposal.OP_BACKGROUND | ||||
| 
 | ||||
|                 if prev_disposal == Disposal.OP_BACKGROUND: | ||||
|                     base_im = previous["im"].copy() | ||||
|                     base_im = previous.im.copy() | ||||
|                     dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0)) | ||||
|                     bbox = previous["bbox"] | ||||
|                     bbox = previous.bbox | ||||
|                     if bbox: | ||||
|                         dispose = dispose.crop(bbox) | ||||
|                     else: | ||||
|                         bbox = (0, 0) + im.size | ||||
|                     base_im.paste(dispose, bbox) | ||||
|                 elif prev_disposal == Disposal.OP_PREVIOUS: | ||||
|                     base_im = im_frames[-2]["im"] | ||||
|                     base_im = im_frames[-2].im | ||||
|                 else: | ||||
|                     base_im = previous["im"] | ||||
|                     base_im = previous.im | ||||
|                 delta = ImageChops.subtract_modulo( | ||||
|                     im_frame.convert("RGBA"), base_im.convert("RGBA") | ||||
|                 ) | ||||
|  | @ -1175,18 +1205,18 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i | |||
|                     and prev_blend == encoderinfo.get("blend") | ||||
|                     and "duration" in encoderinfo | ||||
|                 ): | ||||
|                     previous["encoderinfo"]["duration"] += encoderinfo["duration"] | ||||
|                     previous.encoderinfo["duration"] += encoderinfo["duration"] | ||||
|                     if progress: | ||||
|                         im._save_all_progress(imSequence, i, frame_count, total) | ||||
|                     continue | ||||
|             else: | ||||
|                 bbox = None | ||||
|             im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) | ||||
|             im_frames.append(_Frame(im_frame, bbox, encoderinfo)) | ||||
|             if progress: | ||||
|                 im._save_all_progress(imSequence, i, frame_count, total) | ||||
| 
 | ||||
|     if len(im_frames) == 1 and not default_image: | ||||
|         return im_frames[0]["im"] | ||||
|         return im_frames[0].im | ||||
| 
 | ||||
|     # animation control | ||||
|     chunk( | ||||
|  | @ -1204,14 +1234,14 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i | |||
| 
 | ||||
|     seq_num = 0 | ||||
|     for frame, frame_data in enumerate(im_frames): | ||||
|         im_frame = frame_data["im"] | ||||
|         if not frame_data["bbox"]: | ||||
|         im_frame = frame_data.im | ||||
|         if not frame_data.bbox: | ||||
|             bbox = (0, 0) + im_frame.size | ||||
|         else: | ||||
|             bbox = frame_data["bbox"] | ||||
|             bbox = frame_data.bbox | ||||
|             im_frame = im_frame.crop(bbox) | ||||
|         size = im_frame.size | ||||
|         encoderinfo = frame_data["encoderinfo"] | ||||
|         encoderinfo = frame_data.encoderinfo | ||||
|         frame_duration = int(round(encoderinfo.get("duration", 0))) | ||||
|         frame_disposal = encoderinfo.get("disposal", disposal) | ||||
|         frame_blend = encoderinfo.get("blend", blend) | ||||
|  | @ -1246,13 +1276,16 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i | |||
|                 [("zip", (0, 0) + im_frame.size, 0, rawmode)], | ||||
|             ) | ||||
|             seq_num = fdat_chunks.seq_num | ||||
|     return None | ||||
| 
 | ||||
| 
 | ||||
| def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | ||||
|     _save(im, fp, filename, save_all=True) | ||||
| 
 | ||||
| 
 | ||||
| def _save(im, fp, filename, chunk=putchunk, save_all=False): | ||||
| def _save( | ||||
|     im: Image.Image, fp, filename: str | bytes, chunk=putchunk, save_all: bool = False | ||||
| ) -> None: | ||||
|     # save an image to disk (called by the save method) | ||||
| 
 | ||||
|     if save_all: | ||||
|  | @ -1428,12 +1461,15 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): | |||
|             exif = exif[6:] | ||||
|         chunk(fp, b"eXIf", exif) | ||||
| 
 | ||||
|     single_im: Image.Image | None = im | ||||
|     if save_all: | ||||
|         im = _write_multiple_frames( | ||||
|         single_im = _write_multiple_frames( | ||||
|             im, fp, chunk, mode, rawmode, default_image, append_images | ||||
|         ) | ||||
|     if im: | ||||
|         ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) | ||||
|     if single_im: | ||||
|         ImageFile._save( | ||||
|             single_im, _idat(fp, chunk), [("zip", (0, 0) + single_im.size, 0, rawmode)] | ||||
|         ) | ||||
| 
 | ||||
|     if info: | ||||
|         for info_chunk in info.chunks: | ||||
|  | @ -1454,7 +1490,7 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): | |||
| # PNG chunk converter | ||||
| 
 | ||||
| 
 | ||||
| def getchunks(im, **params): | ||||
| def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]: | ||||
|     """Return a list of PNG chunks representing this image.""" | ||||
| 
 | ||||
|     class collector: | ||||
|  | @ -1463,19 +1499,19 @@ def getchunks(im, **params): | |||
|         def write(self, data: bytes) -> None: | ||||
|             pass | ||||
| 
 | ||||
|         def append(self, chunk: bytes) -> None: | ||||
|         def append(self, chunk: tuple[bytes, bytes, bytes]) -> None: | ||||
|             self.data.append(chunk) | ||||
| 
 | ||||
|     def append(fp, cid, *data): | ||||
|         data = b"".join(data) | ||||
|         crc = o32(_crc32(data, _crc32(cid))) | ||||
|         fp.append((cid, data, crc)) | ||||
|     def append(fp: collector, cid: bytes, *data: bytes) -> None: | ||||
|         byte_data = b"".join(data) | ||||
|         crc = o32(_crc32(byte_data, _crc32(cid))) | ||||
|         fp.append((cid, byte_data, crc)) | ||||
| 
 | ||||
|     fp = collector() | ||||
| 
 | ||||
|     try: | ||||
|         im.encoderinfo = params | ||||
|         _save(im, fp, None, append) | ||||
|         _save(im, fp, "", append) | ||||
|     finally: | ||||
|         del im.encoderinfo | ||||
| 
 | ||||
|  |  | |||
|  | @ -185,7 +185,7 @@ def _layerinfo(fp, ct_bytes): | |||
|     # read layerinfo block | ||||
|     layers = [] | ||||
| 
 | ||||
|     def read(size): | ||||
|     def read(size: int) -> bytes: | ||||
|         return ImageFile._safe_read(fp, size) | ||||
| 
 | ||||
|     ct = si16(read(2)) | ||||
|  |  | |||
|  | @ -334,12 +334,13 @@ class IFDRational(Rational): | |||
| 
 | ||||
|     __slots__ = ("_numerator", "_denominator", "_val") | ||||
| 
 | ||||
|     def __init__(self, value, denominator=1): | ||||
|     def __init__(self, value, denominator: int = 1) -> None: | ||||
|         """ | ||||
|         :param value: either an integer numerator, a | ||||
|         float/rational/other number, or an IFDRational | ||||
|         :param denominator: Optional integer denominator | ||||
|         """ | ||||
|         self._val: Fraction | float | ||||
|         if isinstance(value, IFDRational): | ||||
|             self._numerator = value.numerator | ||||
|             self._denominator = value.denominator | ||||
|  | @ -636,13 +637,13 @@ class ImageFileDirectory_v2(_IFDv2Base): | |||
|             val = (val,) | ||||
|         return val | ||||
| 
 | ||||
|     def __contains__(self, tag): | ||||
|     def __contains__(self, tag: object) -> bool: | ||||
|         return tag in self._tags_v2 or tag in self._tagdata | ||||
| 
 | ||||
|     def __setitem__(self, tag, value): | ||||
|     def __setitem__(self, tag, value) -> None: | ||||
|         self._setitem(tag, value, self.legacy_api) | ||||
| 
 | ||||
|     def _setitem(self, tag, value, legacy_api): | ||||
|     def _setitem(self, tag, value, legacy_api) -> None: | ||||
|         basetypes = (Number, bytes, str) | ||||
| 
 | ||||
|         info = TiffTags.lookup(tag, self.group) | ||||
|  | @ -758,7 +759,7 @@ class ImageFileDirectory_v2(_IFDv2Base): | |||
|         return data | ||||
| 
 | ||||
|     @_register_writer(1)  # Basic type, except for the legacy API. | ||||
|     def write_byte(self, data): | ||||
|     def write_byte(self, data) -> bytes: | ||||
|         if isinstance(data, IFDRational): | ||||
|             data = int(data) | ||||
|         if isinstance(data, int): | ||||
|  | @ -772,7 +773,7 @@ class ImageFileDirectory_v2(_IFDv2Base): | |||
|         return data.decode("latin-1", "replace") | ||||
| 
 | ||||
|     @_register_writer(2) | ||||
|     def write_string(self, value): | ||||
|     def write_string(self, value) -> bytes: | ||||
|         # remerge of https://github.com/python-pillow/Pillow/pull/1416 | ||||
|         if isinstance(value, int): | ||||
|             value = str(value) | ||||
|  | @ -790,7 +791,7 @@ class ImageFileDirectory_v2(_IFDv2Base): | |||
|         return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) | ||||
| 
 | ||||
|     @_register_writer(5) | ||||
|     def write_rational(self, *values): | ||||
|     def write_rational(self, *values) -> bytes: | ||||
|         return b"".join( | ||||
|             self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values | ||||
|         ) | ||||
|  | @ -800,7 +801,7 @@ class ImageFileDirectory_v2(_IFDv2Base): | |||
|         return data | ||||
| 
 | ||||
|     @_register_writer(7) | ||||
|     def write_undefined(self, value): | ||||
|     def write_undefined(self, value) -> bytes: | ||||
|         if isinstance(value, IFDRational): | ||||
|             value = int(value) | ||||
|         if isinstance(value, int): | ||||
|  | @ -817,13 +818,13 @@ class ImageFileDirectory_v2(_IFDv2Base): | |||
|         return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) | ||||
| 
 | ||||
|     @_register_writer(10) | ||||
|     def write_signed_rational(self, *values): | ||||
|     def write_signed_rational(self, *values) -> bytes: | ||||
|         return b"".join( | ||||
|             self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) | ||||
|             for frac in values | ||||
|         ) | ||||
| 
 | ||||
|     def _ensure_read(self, fp, size): | ||||
|     def _ensure_read(self, fp: IO[bytes], size: int) -> bytes: | ||||
|         ret = fp.read(size) | ||||
|         if len(ret) != size: | ||||
|             msg = ( | ||||
|  | @ -977,7 +978,7 @@ class ImageFileDirectory_v2(_IFDv2Base): | |||
| 
 | ||||
|         return result | ||||
| 
 | ||||
|     def save(self, fp): | ||||
|     def save(self, fp: IO[bytes]) -> int: | ||||
|         if fp.tell() == 0:  # skip TIFF header on subsequent pages | ||||
|             # tiff header -- PIL always starts the first IFD at offset 8 | ||||
|             fp.write(self._prefix + self._pack("HL", 42, 8)) | ||||
|  | @ -1017,7 +1018,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): | |||
|     ..  deprecated:: 3.0.0 | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, *args, **kwargs): | ||||
|     def __init__(self, *args, **kwargs) -> None: | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self._legacy_api = True | ||||
| 
 | ||||
|  | @ -1029,7 +1030,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): | |||
|     """Dictionary of tag types""" | ||||
| 
 | ||||
|     @classmethod | ||||
|     def from_v2(cls, original): | ||||
|     def from_v2(cls, original) -> ImageFileDirectory_v1: | ||||
|         """Returns an | ||||
|         :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` | ||||
|         instance with the same data as is contained in the original | ||||
|  | @ -1063,7 +1064,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): | |||
|         ifd._tags_v2 = dict(self._tags_v2) | ||||
|         return ifd | ||||
| 
 | ||||
|     def __contains__(self, tag): | ||||
|     def __contains__(self, tag: object) -> bool: | ||||
|         return tag in self._tags_v1 or tag in self._tagdata | ||||
| 
 | ||||
|     def __len__(self) -> int: | ||||
|  | @ -1072,7 +1073,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): | |||
|     def __iter__(self): | ||||
|         return iter(set(self._tagdata) | set(self._tags_v1)) | ||||
| 
 | ||||
|     def __setitem__(self, tag, value): | ||||
|     def __setitem__(self, tag, value) -> None: | ||||
|         for legacy_api in (False, True): | ||||
|             self._setitem(tag, value, legacy_api) | ||||
| 
 | ||||
|  | @ -1122,7 +1123,7 @@ class TiffImageFile(ImageFile.ImageFile): | |||
|         self.tag_v2 = ImageFileDirectory_v2(ifh) | ||||
| 
 | ||||
|         # legacy IFD entries will be filled in later | ||||
|         self.ifd = None | ||||
|         self.ifd: ImageFileDirectory_v1 | None = None | ||||
| 
 | ||||
|         # setup frame pointers | ||||
|         self.__first = self.__next = self.tag_v2.next | ||||
|  | @ -1232,7 +1233,7 @@ class TiffImageFile(ImageFile.ImageFile): | |||
|                 val = val[math.ceil((10 + n + size) / 2) * 2 :] | ||||
|         return blocks | ||||
| 
 | ||||
|     def load(self): | ||||
|     def load(self) -> Image.core.PixelAccess | None: | ||||
|         if self.tile and self.use_load_libtiff: | ||||
|             return self._load_libtiff() | ||||
|         return super().load() | ||||
|  | @ -1343,7 +1344,7 @@ class TiffImageFile(ImageFile.ImageFile): | |||
| 
 | ||||
|         return Image.Image.load(self) | ||||
| 
 | ||||
|     def _setup(self): | ||||
|     def _setup(self) -> None: | ||||
|         """Setup this image object based on current tags""" | ||||
| 
 | ||||
|         if 0xBC01 in self.tag_v2: | ||||
|  | @ -1537,13 +1538,13 @@ class TiffImageFile(ImageFile.ImageFile): | |||
|                     # adjust stride width accordingly | ||||
|                     stride /= bps_count | ||||
| 
 | ||||
|                 a = (tile_rawmode, int(stride), 1) | ||||
|                 args = (tile_rawmode, int(stride), 1) | ||||
|                 self.tile.append( | ||||
|                     ( | ||||
|                         self._compression, | ||||
|                         (x, y, min(x + w, xsize), min(y + h, ysize)), | ||||
|                         offset, | ||||
|                         a, | ||||
|                         args, | ||||
|                     ) | ||||
|                 ) | ||||
|                 x = x + w | ||||
|  | @ -1938,7 +1939,7 @@ class AppendingTiffWriter: | |||
|         521,  # JPEGACTables | ||||
|     } | ||||
| 
 | ||||
|     def __init__(self, fn, new=False): | ||||
|     def __init__(self, fn, new: bool = False) -> None: | ||||
|         if hasattr(fn, "read"): | ||||
|             self.f = fn | ||||
|             self.close_fp = False | ||||
|  | @ -2015,7 +2016,7 @@ class AppendingTiffWriter: | |||
|     def tell(self) -> int: | ||||
|         return self.f.tell() - self.offsetOfNewPage | ||||
| 
 | ||||
|     def seek(self, offset, whence=io.SEEK_SET): | ||||
|     def seek(self, offset: int, whence=io.SEEK_SET) -> int: | ||||
|         if whence == os.SEEK_SET: | ||||
|             offset += self.offsetOfNewPage | ||||
| 
 | ||||
|  |  | |||
|  | @ -24,8 +24,11 @@ and has been tested with a few sample files found using google. | |||
| """ | ||||
| from __future__ import annotations | ||||
| 
 | ||||
| from typing import IO | ||||
| 
 | ||||
| from . import Image, ImageFile | ||||
| from ._binary import i32le as i32 | ||||
| from ._typing import StrOrBytesPath | ||||
| 
 | ||||
| 
 | ||||
| class WalImageFile(ImageFile.ImageFile): | ||||
|  | @ -50,7 +53,7 @@ class WalImageFile(ImageFile.ImageFile): | |||
|         if next_name: | ||||
|             self.info["next_name"] = next_name | ||||
| 
 | ||||
|     def load(self): | ||||
|     def load(self) -> Image.core.PixelAccess | None: | ||||
|         if not self.im: | ||||
|             self.im = Image.core.new(self.mode, self.size) | ||||
|             self.frombytes(self.fp.read(self.size[0] * self.size[1])) | ||||
|  | @ -58,7 +61,7 @@ class WalImageFile(ImageFile.ImageFile): | |||
|         return Image.Image.load(self) | ||||
| 
 | ||||
| 
 | ||||
| def open(filename): | ||||
| def open(filename: StrOrBytesPath | IO[bytes]) -> WalImageFile: | ||||
|     """ | ||||
|     Load texture from a Quake2 WAL texture file. | ||||
| 
 | ||||
|  |  | |||
|  | @ -115,7 +115,7 @@ class WebPImageFile(ImageFile.ImageFile): | |||
|         self.__loaded = -1 | ||||
|         self.__timestamp = 0 | ||||
| 
 | ||||
|     def _get_next(self): | ||||
|     def _get_next(self) -> tuple[bytes, int, int]: | ||||
|         # Get next frame | ||||
|         ret = self._decoder.get_next() | ||||
|         self.__physical_frame += 1 | ||||
|  | @ -144,7 +144,7 @@ class WebPImageFile(ImageFile.ImageFile): | |||
|         while self.__physical_frame < frame: | ||||
|             self._get_next()  # Advance to the requested frame | ||||
| 
 | ||||
|     def load(self): | ||||
|     def load(self) -> Image.core.PixelAccess | None: | ||||
|         if _webp.HAVE_WEBPANIM: | ||||
|             if self.__loaded != self.__logical_frame: | ||||
|                 self._seek(self.__logical_frame) | ||||
|  |  | |||
|  | @ -152,7 +152,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): | |||
|     def _load(self) -> ImageFile.StubHandler | None: | ||||
|         return _handler | ||||
| 
 | ||||
|     def load(self, dpi=None): | ||||
|     def load(self, dpi: int | None = None) -> Image.core.PixelAccess | None: | ||||
|         if dpi is not None and self._inch is not None: | ||||
|             self.info["dpi"] = dpi | ||||
|             x0, y0, x1, y1 = self.info["wmf_bbox"] | ||||
|  |  | |||
							
								
								
									
										3
									
								
								src/PIL/_imagingtk.pyi
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/PIL/_imagingtk.pyi
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| from typing import Any | ||||
| 
 | ||||
| def __getattr__(name: str) -> Any: ... | ||||
|  | @ -2251,6 +2251,11 @@ _getcolors(ImagingObject *self, PyObject *args) { | |||
|             ImagingColorItem *v = &items[i]; | ||||
|             PyObject *item = Py_BuildValue( | ||||
|                 "iN", v->count, getpixel(self->image, self->access, v->x, v->y)); | ||||
|             if (item == NULL) { | ||||
|                 Py_DECREF(out); | ||||
|                 free(items); | ||||
|                 return NULL; | ||||
|             } | ||||
|             PyList_SetItem(out, i, item); | ||||
|         } | ||||
|     } | ||||
|  | @ -4448,5 +4453,9 @@ PyInit__imaging(void) { | |||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
| #ifdef Py_GIL_DISABLED | ||||
|     PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); | ||||
| #endif | ||||
| 
 | ||||
|     return m; | ||||
| } | ||||
|  |  | |||
|  | @ -1538,5 +1538,9 @@ PyInit__imagingcms(void) { | |||
| 
 | ||||
|     PyDateTime_IMPORT; | ||||
| 
 | ||||
| #ifdef Py_GIL_DISABLED | ||||
|     PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); | ||||
| #endif | ||||
| 
 | ||||
|     return m; | ||||
| } | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ | |||
| 
 | ||||
| #define PY_SSIZE_T_CLEAN | ||||
| #include "Python.h" | ||||
| #include "thirdparty/pythoncapi_compat.h" | ||||
| #include "libImaging/Imaging.h" | ||||
| 
 | ||||
| #include <ft2build.h> | ||||
|  | @ -1209,30 +1210,49 @@ font_getvarnames(FontObject *self) { | |||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     int *list_names_filled = PyMem_Malloc(num_namedstyles * sizeof(int)); | ||||
|     if (list_names_filled == NULL) { | ||||
|         Py_DECREF(list_names); | ||||
|         FT_Done_MM_Var(library, master); | ||||
|         return PyErr_NoMemory(); | ||||
|     } | ||||
| 
 | ||||
|     for (int i = 0; i < num_namedstyles; i++) { | ||||
|         list_names_filled[i] = 0; | ||||
|     } | ||||
| 
 | ||||
|     name_count = FT_Get_Sfnt_Name_Count(self->face); | ||||
|     for (i = 0; i < name_count; i++) { | ||||
|         error = FT_Get_Sfnt_Name(self->face, i, &name); | ||||
|         if (error) { | ||||
|             PyMem_Free(list_names_filled); | ||||
|             Py_DECREF(list_names); | ||||
|             FT_Done_MM_Var(library, master); | ||||
|             return geterror(error); | ||||
|         } | ||||
| 
 | ||||
|         for (j = 0; j < num_namedstyles; j++) { | ||||
|             if (PyList_GetItem(list_names, j) != NULL) { | ||||
|             if (list_names_filled[j]) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             if (master->namedstyle[j].strid == name.name_id) { | ||||
|                 list_name = Py_BuildValue("y#", name.string, name.string_len); | ||||
|                 if (list_name == NULL) { | ||||
|                     PyMem_Free(list_names_filled); | ||||
|                     Py_DECREF(list_names); | ||||
|                     FT_Done_MM_Var(library, master); | ||||
|                     return NULL; | ||||
|                 } | ||||
|                 PyList_SetItem(list_names, j, list_name); | ||||
|                 list_names_filled[j] = 1; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     PyMem_Free(list_names_filled); | ||||
|     FT_Done_MM_Var(library, master); | ||||
| 
 | ||||
|     return list_names; | ||||
| } | ||||
| 
 | ||||
|  | @ -1289,9 +1309,14 @@ font_getvaraxes(FontObject *self) { | |||
| 
 | ||||
|             if (name.name_id == axis.strid) { | ||||
|                 axis_name = Py_BuildValue("y#", name.string, name.string_len); | ||||
|                 PyDict_SetItemString( | ||||
|                     list_axis, "name", axis_name ? axis_name : Py_None); | ||||
|                 Py_XDECREF(axis_name); | ||||
|                 if (axis_name == NULL) { | ||||
|                     Py_DECREF(list_axis); | ||||
|                     Py_DECREF(list_axes); | ||||
|                     FT_Done_MM_Var(library, master); | ||||
|                     return NULL; | ||||
|                 } | ||||
|                 PyDict_SetItemString(list_axis, "name", axis_name); | ||||
|                 Py_DECREF(axis_name); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|  | @ -1345,7 +1370,12 @@ font_setvaraxes(FontObject *self, PyObject *args) { | |||
|         return PyErr_NoMemory(); | ||||
|     } | ||||
|     for (i = 0; i < num_coords; i++) { | ||||
|         item = PyList_GET_ITEM(axes, i); | ||||
|         item = PyList_GetItemRef(axes, i); | ||||
|         if (item == NULL) { | ||||
|             free(coords); | ||||
|             return NULL; | ||||
|         } | ||||
| 
 | ||||
|         if (PyFloat_Check(item)) { | ||||
|             coord = PyFloat_AS_DOUBLE(item); | ||||
|         } else if (PyLong_Check(item)) { | ||||
|  | @ -1353,10 +1383,12 @@ font_setvaraxes(FontObject *self, PyObject *args) { | |||
|         } else if (PyNumber_Check(item)) { | ||||
|             coord = PyFloat_AsDouble(item); | ||||
|         } else { | ||||
|             Py_DECREF(item); | ||||
|             free(coords); | ||||
|             PyErr_SetString(PyExc_TypeError, "list must contain numbers"); | ||||
|             return NULL; | ||||
|         } | ||||
|         Py_DECREF(item); | ||||
|         coords[i] = coord * 65536; | ||||
|     } | ||||
| 
 | ||||
|  | @ -1576,5 +1608,9 @@ PyInit__imagingft(void) { | |||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
| #ifdef Py_GIL_DISABLED | ||||
|     PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); | ||||
| #endif | ||||
| 
 | ||||
|     return m; | ||||
| } | ||||
|  |  | |||
|  | @ -290,5 +290,9 @@ PyInit__imagingmath(void) { | |||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
| #ifdef Py_GIL_DISABLED | ||||
|     PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); | ||||
| #endif | ||||
| 
 | ||||
|     return m; | ||||
| } | ||||
|  |  | |||
|  | @ -269,5 +269,9 @@ PyInit__imagingmorph(void) { | |||
| 
 | ||||
|     m = PyModule_Create(&module_def); | ||||
| 
 | ||||
| #ifdef Py_GIL_DISABLED | ||||
|     PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); | ||||
| #endif | ||||
| 
 | ||||
|     return m; | ||||
| } | ||||
|  |  | |||
|  | @ -62,5 +62,10 @@ PyInit__imagingtk(void) { | |||
|         Py_DECREF(m); | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
| #ifdef Py_GIL_DISABLED | ||||
|     PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); | ||||
| #endif | ||||
| 
 | ||||
|     return m; | ||||
| } | ||||
|  |  | |||
|  | @ -1005,5 +1005,9 @@ PyInit__webp(void) { | |||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
| #ifdef Py_GIL_DISABLED | ||||
|     PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); | ||||
| #endif | ||||
| 
 | ||||
|     return m; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										26
									
								
								src/encode.c
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								src/encode.c
									
									
									
									
									
								
							|  | @ -25,6 +25,7 @@ | |||
| #define PY_SSIZE_T_CLEAN | ||||
| #include "Python.h" | ||||
| 
 | ||||
| #include "thirdparty/pythoncapi_compat.h" | ||||
| #include "libImaging/Imaging.h" | ||||
| #include "libImaging/Gif.h" | ||||
| 
 | ||||
|  | @ -671,11 +672,17 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { | |||
|         tags_size = PyList_Size(tags); | ||||
|         TRACE(("tags size: %d\n", (int)tags_size)); | ||||
|         for (pos = 0; pos < tags_size; pos++) { | ||||
|             item = PyList_GetItem(tags, pos); | ||||
|             item = PyList_GetItemRef(tags, pos); | ||||
|             if (item == NULL) { | ||||
|                 return NULL; | ||||
|             } | ||||
| 
 | ||||
|             if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) { | ||||
|                 Py_DECREF(item); | ||||
|                 PyErr_SetString(PyExc_ValueError, "Invalid tags list"); | ||||
|                 return NULL; | ||||
|             } | ||||
|             Py_DECREF(item); | ||||
|         } | ||||
|         pos = 0; | ||||
|     } | ||||
|  | @ -703,11 +710,17 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { | |||
| 
 | ||||
|     num_core_tags = sizeof(core_tags) / sizeof(int); | ||||
|     for (pos = 0; pos < tags_size; pos++) { | ||||
|         item = PyList_GetItem(tags, pos); | ||||
|         item = PyList_GetItemRef(tags, pos); | ||||
|         if (item == NULL) { | ||||
|             return NULL; | ||||
|         } | ||||
| 
 | ||||
|         // We already checked that tags is a 2-tuple list.
 | ||||
|         key = PyTuple_GetItem(item, 0); | ||||
|         key = PyTuple_GET_ITEM(item, 0); | ||||
|         key_int = (int)PyLong_AsLong(key); | ||||
|         value = PyTuple_GetItem(item, 1); | ||||
|         value = PyTuple_GET_ITEM(item, 1); | ||||
|         Py_DECREF(item); | ||||
| 
 | ||||
|         status = 0; | ||||
|         is_core_tag = 0; | ||||
|         is_var_length = 0; | ||||
|  | @ -721,7 +734,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { | |||
|         } | ||||
| 
 | ||||
|         if (!is_core_tag) { | ||||
|             PyObject *tag_type = PyDict_GetItem(types, key); | ||||
|             PyObject *tag_type; | ||||
|             if (PyDict_GetItemRef(types, key, &tag_type) < 0) { | ||||
|                 return NULL;  // Exception has been already set
 | ||||
|             } | ||||
|             if (tag_type) { | ||||
|                 int type_int = PyLong_AsLong(tag_type); | ||||
|                 if (type_int >= TIFF_BYTE && type_int <= TIFF_DOUBLE) { | ||||
|  |  | |||
							
								
								
									
										13
									
								
								src/path.c
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/path.c
									
									
									
									
									
								
							|  | @ -26,6 +26,7 @@ | |||
|  */ | ||||
| 
 | ||||
| #include "Python.h" | ||||
| #include "thirdparty/pythoncapi_compat.h" | ||||
| #include "libImaging/Imaging.h" | ||||
| 
 | ||||
| #include <math.h> | ||||
|  | @ -179,14 +180,21 @@ PyPath_Flatten(PyObject *data, double **pxy) { | |||
|         }                                                               \ | ||||
|         free(xy);                                                       \ | ||||
|         return -1;                                                      \ | ||||
|     }                                                                   \ | ||||
|     if (decref) {                                                       \ | ||||
|         Py_DECREF(op);                                                  \ | ||||
|     } | ||||
| 
 | ||||
|     /* Copy table to path array */ | ||||
|     if (PyList_Check(data)) { | ||||
|         for (i = 0; i < n; i++) { | ||||
|             double x, y; | ||||
|             PyObject *op = PyList_GET_ITEM(data, i); | ||||
|             assign_item_to_array(op, 0); | ||||
|             PyObject *op = PyList_GetItemRef(data, i); | ||||
|             if (op == NULL) { | ||||
|                 free(xy); | ||||
|                 return -1; | ||||
|             } | ||||
|             assign_item_to_array(op, 1); | ||||
|         } | ||||
|     } else if (PyTuple_Check(data)) { | ||||
|         for (i = 0; i < n; i++) { | ||||
|  | @ -209,7 +217,6 @@ PyPath_Flatten(PyObject *data, double **pxy) { | |||
|                 } | ||||
|             } | ||||
|             assign_item_to_array(op, 1); | ||||
|             Py_DECREF(op); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										1360
									
								
								src/thirdparty/pythoncapi_compat.h
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1360
									
								
								src/thirdparty/pythoncapi_compat.h
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	Block a user