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