Merge branch 'main' into multiband
|  | @ -51,7 +51,7 @@ build_script: | |||
| 
 | ||||
| test_script: | ||||
| - cd c:\pillow | ||||
| - '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma' | ||||
| - '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma' | ||||
| - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% | ||||
| - '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' | ||||
| - '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' | ||||
|  |  | |||
|  | @ -30,6 +30,7 @@ python3 -m pip install --upgrade pip | |||
| python3 -m pip install --upgrade wheel | ||||
| python3 -m pip install coverage | ||||
| python3 -m pip install defusedxml | ||||
| python3 -m pip install ipython | ||||
| python3 -m pip install olefile | ||||
| python3 -m pip install -U pytest | ||||
| python3 -m pip install -U pytest-cov | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| cibuildwheel==2.19.2 | ||||
| cibuildwheel==2.20.0 | ||||
|  |  | |||
|  | @ -1 +1,11 @@ | |||
| mypy==1.10.1 | ||||
| mypy==1.11.2 | ||||
| IceSpringPySideStubs-PyQt6 | ||||
| IceSpringPySideStubs-PySide6 | ||||
| ipython | ||||
| numpy | ||||
| packaging | ||||
| pytest | ||||
| sphinx | ||||
| types-defusedxml | ||||
| types-olefile | ||||
| types-setuptools | ||||
|  |  | |||
							
								
								
									
										4
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -2,6 +2,9 @@ | |||
| 
 | ||||
| set -e | ||||
| 
 | ||||
| if [[ "$ImageOS" == "macos13" ]]; then | ||||
|     brew uninstall gradle maven | ||||
| fi | ||||
| brew install \ | ||||
|     freetype \ | ||||
|     ghostscript \ | ||||
|  | @ -20,6 +23,7 @@ export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" | |||
| 
 | ||||
| python3 -m pip install coverage | ||||
| python3 -m pip install defusedxml | ||||
| python3 -m pip install ipython | ||||
| python3 -m pip install olefile | ||||
| python3 -m pip install -U pytest | ||||
| python3 -m pip install -U pytest-cov | ||||
|  |  | |||
							
								
								
									
										1
									
								
								.github/workflows/test-cygwin.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -74,6 +74,7 @@ jobs: | |||
|             perl | ||||
|             python3${{ matrix.python-minor-version }}-cython | ||||
|             python3${{ matrix.python-minor-version }}-devel | ||||
|             python3${{ matrix.python-minor-version }}-ipython | ||||
|             python3${{ matrix.python-minor-version }}-numpy | ||||
|             python3${{ matrix.python-minor-version }}-sip | ||||
|             python3${{ matrix.python-minor-version }}-tkinter | ||||
|  |  | |||
							
								
								
									
										2
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -87,7 +87,7 @@ jobs: | |||
|         echo "C:\Program Files\NASM" >> $env:GITHUB_PATH | ||||
| 
 | ||||
|         choco install ghostscript --version=10.3.1 --no-progress | ||||
|         echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH | ||||
|         echo "C:\Program Files\gs\gs10.03.1\bin" >> $env:GITHUB_PATH | ||||
| 
 | ||||
|         # Install extra test images | ||||
|         xcopy /S /Y Tests\test-images\* Tests\images | ||||
|  |  | |||
							
								
								
									
										6
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -97,6 +97,7 @@ jobs: | |||
|           path: ./wheelhouse/*.whl | ||||
| 
 | ||||
|   build-2-native-wheels: | ||||
|     if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' | ||||
|     name: ${{ matrix.name }} | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|  | @ -150,6 +151,7 @@ jobs: | |||
|           path: ./wheelhouse/*.whl | ||||
| 
 | ||||
|   windows: | ||||
|     if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' | ||||
|     name: Windows ${{ matrix.cibw_arch }} | ||||
|     runs-on: windows-latest | ||||
|     strategy: | ||||
|  | @ -256,7 +258,7 @@ jobs: | |||
|         path: dist/*.tar.gz | ||||
| 
 | ||||
|   scientific-python-nightly-wheels-publish: | ||||
|     if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | ||||
|     if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') | ||||
|     needs: [build-2-native-wheels, windows] | ||||
|     runs-on: ubuntu-latest | ||||
|     name: Upload wheels to scientific-python-nightly-wheels | ||||
|  | @ -273,7 +275,7 @@ jobs: | |||
|           anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} | ||||
| 
 | ||||
|   pypi-publish: | ||||
|     if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') | ||||
|     if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') | ||||
|     needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] | ||||
|     runs-on: ubuntu-latest | ||||
|     name: Upload release to PyPI | ||||
|  |  | |||
|  | @ -1,12 +1,12 @@ | |||
| repos: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     rev: v0.5.0 | ||||
|     rev: v0.6.0 | ||||
|     hooks: | ||||
|       - id: ruff | ||||
|         args: [--exit-non-zero-on-fix] | ||||
| 
 | ||||
|   - repo: https://github.com/psf/black-pre-commit-mirror | ||||
|     rev: 24.4.2 | ||||
|     rev: 24.8.0 | ||||
|     hooks: | ||||
|       - id: black | ||||
| 
 | ||||
|  | @ -50,7 +50,7 @@ repos: | |||
|         exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ | ||||
| 
 | ||||
|   - repo: https://github.com/python-jsonschema/check-jsonschema | ||||
|     rev: 0.28.6 | ||||
|     rev: 0.29.1 | ||||
|     hooks: | ||||
|       - id: check-github-workflows | ||||
|       - id: check-readthedocs | ||||
|  | @ -62,12 +62,12 @@ repos: | |||
|       - id: sphinx-lint | ||||
| 
 | ||||
|   - repo: https://github.com/tox-dev/pyproject-fmt | ||||
|     rev: 2.1.3 | ||||
|     rev: 2.2.1 | ||||
|     hooks: | ||||
|       - id: pyproject-fmt | ||||
| 
 | ||||
|   - repo: https://github.com/abravalheri/validate-pyproject | ||||
|     rev: v0.18 | ||||
|     rev: v0.19 | ||||
|     hooks: | ||||
|       - id: validate-pyproject | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										27
									
								
								CHANGES.rst
									
									
									
									
									
								
							
							
						
						|  | @ -5,6 +5,33 @@ Changelog (Pillow) | |||
| 11.0.0 (unreleased) | ||||
| ------------------- | ||||
| 
 | ||||
| - Updated error message when saving WebP with invalid width or height #8322 | ||||
|   [radarhere, hugovk] | ||||
| 
 | ||||
| - Remove warning if NumPy failed to raise an error during conversion #8326 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - If left and right sides meet in ImageDraw.rounded_rectangle(), do not draw rectangle to fill gap #8304 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Remove WebP support without anim, mux/demux, and with buggy alpha #8213 | ||||
|   [homm, radarhere] | ||||
| 
 | ||||
| - Add missing TIFF CMYK;16B reader #8298 | ||||
|   [homm] | ||||
| 
 | ||||
| - Remove all WITH_* flags from _imaging.c and other flags #8211 | ||||
|   [homm] | ||||
| 
 | ||||
| - Improve ImageDraw2 shape methods #8265 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Lock around usages of imaging memory arenas #8238 | ||||
|   [lysnikolaou] | ||||
| 
 | ||||
| - Deprecate JpegImageFile huffman_ac and huffman_dc #8274 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Deprecate ImageMath lambda_eval and unsafe_eval options argument #8242 | ||||
|   [radarhere] | ||||
| 
 | ||||
|  |  | |||
| After Width: | Height: | Size: 411 B | 
|  | @ -105,91 +105,65 @@ class TestColorLut3DCoreAPI: | |||
|         with pytest.raises(TypeError): | ||||
|             im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) | ||||
| 
 | ||||
|     def test_correct_args(self) -> None: | ||||
|         im = Image.new("RGB", (10, 10), 0) | ||||
| 
 | ||||
|         im.im.color_lut_3d( | ||||
|             "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) | ||||
|         ) | ||||
| 
 | ||||
|         im.im.color_lut_3d( | ||||
|             "CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) | ||||
|         ) | ||||
| 
 | ||||
|         im.im.color_lut_3d( | ||||
|             "RGB", | ||||
|             Image.Resampling.BILINEAR, | ||||
|             *self.generate_identity_table(3, (2, 3, 3)), | ||||
|         ) | ||||
| 
 | ||||
|         im.im.color_lut_3d( | ||||
|             "RGB", | ||||
|             Image.Resampling.BILINEAR, | ||||
|             *self.generate_identity_table(3, (65, 3, 3)), | ||||
|         ) | ||||
| 
 | ||||
|         im.im.color_lut_3d( | ||||
|             "RGB", | ||||
|             Image.Resampling.BILINEAR, | ||||
|             *self.generate_identity_table(3, (3, 65, 3)), | ||||
|         ) | ||||
| 
 | ||||
|         im.im.color_lut_3d( | ||||
|             "RGB", | ||||
|             Image.Resampling.BILINEAR, | ||||
|             *self.generate_identity_table(3, (3, 3, 65)), | ||||
|         ) | ||||
| 
 | ||||
|     def test_wrong_mode(self) -> None: | ||||
|         with pytest.raises(ValueError, match="wrong mode"): | ||||
|             im = Image.new("L", (10, 10), 0) | ||||
|             im.im.color_lut_3d( | ||||
|                 "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) | ||||
|             ) | ||||
| 
 | ||||
|         with pytest.raises(ValueError, match="wrong mode"): | ||||
|             im = Image.new("RGB", (10, 10), 0) | ||||
|             im.im.color_lut_3d( | ||||
|                 "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) | ||||
|             ) | ||||
| 
 | ||||
|         with pytest.raises(ValueError, match="wrong mode"): | ||||
|             im = Image.new("L", (10, 10), 0) | ||||
|             im.im.color_lut_3d( | ||||
|                 "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) | ||||
|             ) | ||||
| 
 | ||||
|         with pytest.raises(ValueError, match="wrong mode"): | ||||
|             im = Image.new("RGB", (10, 10), 0) | ||||
|             im.im.color_lut_3d( | ||||
|                 "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) | ||||
|             ) | ||||
| 
 | ||||
|         with pytest.raises(ValueError, match="wrong mode"): | ||||
|             im = Image.new("RGB", (10, 10), 0) | ||||
|             im.im.color_lut_3d( | ||||
|                 "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) | ||||
|             ) | ||||
| 
 | ||||
|     def test_correct_mode(self) -> None: | ||||
|         im = Image.new("RGBA", (10, 10), 0) | ||||
|         im.im.color_lut_3d( | ||||
|             "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) | ||||
|         ) | ||||
| 
 | ||||
|         im = Image.new("RGBA", (10, 10), 0) | ||||
|         im.im.color_lut_3d( | ||||
|             "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) | ||||
|         ) | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         "lut_mode, table_channels, table_size", | ||||
|         [ | ||||
|             ("RGB", 3, 3), | ||||
|             ("CMYK", 4, 3), | ||||
|             ("RGB", 3, (2, 3, 3)), | ||||
|             ("RGB", 3, (65, 3, 3)), | ||||
|             ("RGB", 3, (3, 65, 3)), | ||||
|             ("RGB", 3, (2, 3, 65)), | ||||
|         ], | ||||
|     ) | ||||
|     def test_correct_args( | ||||
|         self, lut_mode: str, table_channels: int, table_size: int | tuple[int, int, int] | ||||
|     ) -> None: | ||||
|         im = Image.new("RGB", (10, 10), 0) | ||||
|         im.im.color_lut_3d( | ||||
|             "HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) | ||||
|             lut_mode, | ||||
|             Image.Resampling.BILINEAR, | ||||
|             *self.generate_identity_table(table_channels, table_size), | ||||
|         ) | ||||
| 
 | ||||
|         im = Image.new("RGB", (10, 10), 0) | ||||
|     @pytest.mark.parametrize( | ||||
|         "image_mode, lut_mode, table_channels, table_size", | ||||
|         [ | ||||
|             ("L", "RGB", 3, 3), | ||||
|             ("RGB", "L", 3, 3), | ||||
|             ("L", "L", 3, 3), | ||||
|             ("RGB", "RGBA", 3, 3), | ||||
|             ("RGB", "RGB", 4, 3), | ||||
|         ], | ||||
|     ) | ||||
|     def test_wrong_mode( | ||||
|         self, image_mode: str, lut_mode: str, table_channels: int, table_size: int | ||||
|     ) -> None: | ||||
|         with pytest.raises(ValueError, match="wrong mode"): | ||||
|             im = Image.new(image_mode, (10, 10), 0) | ||||
|             im.im.color_lut_3d( | ||||
|                 lut_mode, | ||||
|                 Image.Resampling.BILINEAR, | ||||
|                 *self.generate_identity_table(table_channels, table_size), | ||||
|             ) | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         "image_mode, lut_mode, table_channels, table_size", | ||||
|         [ | ||||
|             ("RGBA", "RGBA", 3, 3), | ||||
|             ("RGBA", "RGBA", 4, 3), | ||||
|             ("RGB", "HSV", 3, 3), | ||||
|             ("RGB", "RGBA", 4, 3), | ||||
|         ], | ||||
|     ) | ||||
|     def test_correct_mode( | ||||
|         self, image_mode: str, lut_mode: str, table_channels: int, table_size: int | ||||
|     ) -> None: | ||||
|         im = Image.new(image_mode, (10, 10), 0) | ||||
|         im.im.color_lut_3d( | ||||
|             "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) | ||||
|             lut_mode, | ||||
|             Image.Resampling.BILINEAR, | ||||
|             *self.generate_identity_table(table_channels, table_size), | ||||
|         ) | ||||
| 
 | ||||
|     def test_identities(self) -> None: | ||||
|  |  | |||
|  | @ -10,11 +10,6 @@ from PIL import features | |||
| 
 | ||||
| from .helper import skip_unless_feature | ||||
| 
 | ||||
| try: | ||||
|     from PIL import _webp | ||||
| except ImportError: | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| def test_check() -> None: | ||||
|     # Check the correctness of the convenience function | ||||
|  | @ -23,7 +18,11 @@ def test_check() -> None: | |||
|     for codec in features.codecs: | ||||
|         assert features.check_codec(codec) == features.check(codec) | ||||
|     for feature in features.features: | ||||
|         assert features.check_feature(feature) == features.check(feature) | ||||
|         if "webp" in feature: | ||||
|             with pytest.warns(DeprecationWarning): | ||||
|                 assert features.check_feature(feature) == features.check(feature) | ||||
|         else: | ||||
|             assert features.check_feature(feature) == features.check(feature) | ||||
| 
 | ||||
| 
 | ||||
| def test_version() -> None: | ||||
|  | @ -48,23 +47,26 @@ def test_version() -> None: | |||
|     for codec in features.codecs: | ||||
|         test(codec, features.version_codec) | ||||
|     for feature in features.features: | ||||
|         test(feature, features.version_feature) | ||||
|         if "webp" in feature: | ||||
|             with pytest.warns(DeprecationWarning): | ||||
|                 test(feature, features.version_feature) | ||||
|         else: | ||||
|             test(feature, features.version_feature) | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("webp") | ||||
| def test_webp_transparency() -> None: | ||||
|     assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() | ||||
|     assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY | ||||
|     with pytest.warns(DeprecationWarning): | ||||
|         assert features.check("transp_webp") == features.check_module("webp") | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("webp") | ||||
| def test_webp_mux() -> None: | ||||
|     assert features.check("webp_mux") == _webp.HAVE_WEBPMUX | ||||
|     with pytest.warns(DeprecationWarning): | ||||
|         assert features.check("webp_mux") == features.check_module("webp") | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("webp") | ||||
| def test_webp_anim() -> None: | ||||
|     assert features.check("webp_anim") == _webp.HAVE_WEBPANIM | ||||
|     with pytest.warns(DeprecationWarning): | ||||
|         assert features.check("webp_anim") == features.check_module("webp") | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("libjpeg_turbo") | ||||
|  |  | |||
|  | @ -152,7 +152,7 @@ def test_sanity_ati2_bc5u(image_path: str) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     ("image_path", "expected_path"), | ||||
|     "image_path, expected_path", | ||||
|     ( | ||||
|         # hexeditted to be typeless | ||||
|         (TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM), | ||||
|  | @ -248,7 +248,7 @@ def test_dx10_r8g8b8a8_unorm_srgb() -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     ("mode", "size", "test_file"), | ||||
|     "mode, size, test_file", | ||||
|     [ | ||||
|         ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), | ||||
|         ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), | ||||
|  | @ -373,7 +373,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     ("mode", "test_file"), | ||||
|     "mode, test_file", | ||||
|     [ | ||||
|         ("L", "Tests/images/linear_gradient.png"), | ||||
|         ("LA", "Tests/images/uncompressed_la.png"), | ||||
|  |  | |||
|  | @ -80,9 +80,7 @@ simple_eps_file_with_long_binary_data = ( | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") | ||||
| @pytest.mark.parametrize( | ||||
|     ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) | ||||
| ) | ||||
| @pytest.mark.parametrize("filename, size", ((FILE1, (460, 352)), (FILE2, (360, 252)))) | ||||
| @pytest.mark.parametrize("scale", (1, 2)) | ||||
| def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: | ||||
|     expected_size = tuple(s * scale for s in size) | ||||
|  |  | |||
|  | @ -978,7 +978,7 @@ def test_webp_background(tmp_path: Path) -> None: | |||
|     out = str(tmp_path / "temp.gif") | ||||
| 
 | ||||
|     # Test opaque WebP background | ||||
|     if features.check("webp") and features.check("webp_anim"): | ||||
|     if features.check("webp"): | ||||
|         with Image.open("Tests/images/hopper.webp") as im: | ||||
|             assert im.info["background"] == (255, 255, 255, 255) | ||||
|             im.save(out) | ||||
|  |  | |||
|  | @ -57,6 +57,7 @@ def test_getiptcinfo_fotostation() -> None: | |||
|         iptc = IptcImagePlugin.getiptcinfo(im) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert iptc is not None | ||||
|     for tag in iptc.keys(): | ||||
|         if tag[0] == 240: | ||||
|             return | ||||
|  | @ -76,6 +77,16 @@ def test_getiptcinfo_zero_padding() -> None: | |||
|     assert len(iptc) == 3 | ||||
| 
 | ||||
| 
 | ||||
| def test_getiptcinfo_tiff() -> None: | ||||
|     # Arrange | ||||
|     with Image.open("Tests/images/hopper.Lab.tif") as im: | ||||
|         # Act | ||||
|         iptc = IptcImagePlugin.getiptcinfo(im) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert iptc == {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"} | ||||
| 
 | ||||
| 
 | ||||
| def test_getiptcinfo_tiff_none() -> None: | ||||
|     # Arrange | ||||
|     with Image.open("Tests/images/hopper.tif") as im: | ||||
|  |  | |||
|  | @ -154,7 +154,7 @@ class TestFileJpeg: | |||
|             assert k > 0.9 | ||||
| 
 | ||||
|     def test_rgb(self) -> None: | ||||
|         def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]: | ||||
|         def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]: | ||||
|             return tuple(v[0] for v in im.layer) | ||||
| 
 | ||||
|         im = hopper() | ||||
|  | @ -829,7 +829,7 @@ class TestFileJpeg: | |||
|         with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: | ||||
|             # Act / Assert | ||||
|             # "When the image resolution is unknown, 72 [dpi] is designated." | ||||
|             # https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html | ||||
|             # https://exiv2.org/tags.html | ||||
|             assert im.info.get("dpi") == (72, 72) | ||||
| 
 | ||||
|     def test_invalid_exif(self) -> None: | ||||
|  | @ -1019,13 +1019,16 @@ class TestFileJpeg: | |||
| 
 | ||||
|         # SOI, EOI | ||||
|         for marker in b"\xff\xd8", b"\xff\xd9": | ||||
|             assert marker in data[1] and marker in data[2] | ||||
|             assert marker in data[1] | ||||
|             assert marker in data[2] | ||||
|         # DHT, DQT | ||||
|         for marker in b"\xff\xc4", b"\xff\xdb": | ||||
|             assert marker in data[1] and marker not in data[2] | ||||
|             assert marker in data[1] | ||||
|             assert marker not in data[2] | ||||
|         # SOF0, SOS, APP0 (JFIF header) | ||||
|         for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0": | ||||
|             assert marker not in data[1] and marker in data[2] | ||||
|             assert marker not in data[1] | ||||
|             assert marker in data[2] | ||||
| 
 | ||||
|         with Image.open(BytesIO(data[0])) as interchange_im: | ||||
|             with Image.open(BytesIO(data[1] + data[2])) as combined_im: | ||||
|  | @ -1045,6 +1048,13 @@ class TestFileJpeg: | |||
| 
 | ||||
|         assert im._repr_jpeg_() is None | ||||
| 
 | ||||
|     def test_deprecation(self) -> None: | ||||
|         with Image.open(TEST_FILE) as im: | ||||
|             with pytest.warns(DeprecationWarning): | ||||
|                 assert im.huffman_ac == {} | ||||
|             with pytest.warns(DeprecationWarning): | ||||
|                 assert im.huffman_dc == {} | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(not is_win32(), reason="Windows only") | ||||
| @skip_unless_feature("jpg") | ||||
|  |  | |||
|  | @ -233,7 +233,7 @@ def test_layers() -> None: | |||
|         ("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"), | ||||
|         ("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"), | ||||
|         ("foo.jp2", {"no_jp2": False}, 4, b"jP"), | ||||
|         ("foo.jp2", {"no_jp2": False}, 4, b"jP"), | ||||
|         (None, {"no_jp2": False}, 4, b"jP"), | ||||
|     ), | ||||
| ) | ||||
| def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: | ||||
|  |  | |||
|  | @ -240,9 +240,10 @@ class TestFileLibTiff(LibTiffTestCase): | |||
| 
 | ||||
|             new_ifd = TiffImagePlugin.ImageFileDirectory_v2() | ||||
|             for tag, info in core_items.items(): | ||||
|                 assert info.type is not None | ||||
|                 if info.length == 1: | ||||
|                     new_ifd[tag] = values[info.type] | ||||
|                 if info.length == 0: | ||||
|                 elif not info.length: | ||||
|                     new_ifd[tag] = tuple(values[info.type] for _ in range(3)) | ||||
|                 else: | ||||
|                     new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) | ||||
|  |  | |||
|  | @ -85,7 +85,9 @@ def test_exif(test_file: str) -> None: | |||
|         im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) | ||||
| 
 | ||||
|     for im in (im_original, im_reloaded): | ||||
|         assert isinstance(im, MpoImagePlugin.MpoImageFile) | ||||
|         info = im._getexif() | ||||
|         assert info is not None | ||||
|         assert info[272] == "Nintendo 3DS" | ||||
|         assert info[296] == 2 | ||||
|         assert info[34665] == 188 | ||||
|  |  | |||
|  | @ -424,8 +424,10 @@ class TestFilePng: | |||
|         im = roundtrip(im, pnginfo=info) | ||||
|         assert im.info == {"spam": "Eggs", "eggs": "Spam"} | ||||
|         assert im.text == {"spam": "Eggs", "eggs": "Spam"} | ||||
|         assert isinstance(im.text["spam"], PngImagePlugin.iTXt) | ||||
|         assert im.text["spam"].lang == "en" | ||||
|         assert im.text["spam"].tkey == "Spam" | ||||
|         assert isinstance(im.text["eggs"], PngImagePlugin.iTXt) | ||||
|         assert im.text["eggs"].lang == "en" | ||||
|         assert im.text["eggs"].tkey == "Eggs" | ||||
| 
 | ||||
|  | @ -776,7 +778,7 @@ class TestFilePng: | |||
| 
 | ||||
|         mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() | ||||
| 
 | ||||
|         sys.stdout = mystdout  # type: ignore[assignment] | ||||
|         sys.stdout = mystdout | ||||
| 
 | ||||
|         with Image.open(TEST_PNG_FILE) as im: | ||||
|             im.save(sys.stdout, "PNG") | ||||
|  |  | |||
|  | @ -373,7 +373,7 @@ def test_save_stdout(buffer: bool) -> None: | |||
| 
 | ||||
|     mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() | ||||
| 
 | ||||
|     sys.stdout = mystdout  # type: ignore[assignment] | ||||
|     sys.stdout = mystdout | ||||
| 
 | ||||
|     with Image.open(TEST_FILE) as im: | ||||
|         im.save(sys.stdout, "PPM") | ||||
|  |  | |||
|  | @ -684,6 +684,13 @@ class TestFileTiff: | |||
|             with Image.open(outfile) as reloaded: | ||||
|                 assert_image_equal_tofile(reloaded, infile) | ||||
| 
 | ||||
|     def test_invalid_tiled_dimensions(self) -> None: | ||||
|         with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp: | ||||
|             data = fp.read() | ||||
|         b = BytesIO(data[:144] + b"\x02" + data[145:]) | ||||
|         with pytest.raises(ValueError): | ||||
|             Image.open(b) | ||||
| 
 | ||||
|     @pytest.mark.parametrize("mode", ("P", "PA")) | ||||
|     def test_palette(self, mode: str, tmp_path: Path) -> None: | ||||
|         outfile = str(tmp_path / "temp.tif") | ||||
|  |  | |||
|  | @ -48,8 +48,6 @@ class TestFileWebp: | |||
|         self.rgb_mode = "RGB" | ||||
| 
 | ||||
|     def test_version(self) -> None: | ||||
|         _webp.WebPDecoderVersion() | ||||
|         _webp.WebPDecoderBuggyAlpha() | ||||
|         version = features.version_module("webp") | ||||
|         assert version is not None | ||||
|         assert re.search(r"\d+\.\d+\.\d+$", version) | ||||
|  | @ -117,7 +115,6 @@ class TestFileWebp: | |||
|         hopper().save(buffer_method, format="WEBP", method=6) | ||||
|         assert buffer_no_args.getbuffer() != buffer_method.getbuffer() | ||||
| 
 | ||||
|     @skip_unless_feature("webp_anim") | ||||
|     def test_save_all(self, tmp_path: Path) -> None: | ||||
|         temp_file = str(tmp_path / "temp.webp") | ||||
|         im = Image.new("RGB", (1, 1)) | ||||
|  | @ -132,10 +129,9 @@ class TestFileWebp: | |||
| 
 | ||||
|     def test_icc_profile(self, tmp_path: Path) -> None: | ||||
|         self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) | ||||
|         if _webp.HAVE_WEBPANIM: | ||||
|             self._roundtrip( | ||||
|                 tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} | ||||
|             ) | ||||
|         self._roundtrip( | ||||
|             tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} | ||||
|         ) | ||||
| 
 | ||||
|     def test_write_unsupported_mode_L(self, tmp_path: Path) -> None: | ||||
|         """ | ||||
|  | @ -161,27 +157,32 @@ class TestFileWebp: | |||
|             im.save(temp_file, method=0) | ||||
|         assert str(e.value) == "encoding error 6" | ||||
| 
 | ||||
|     @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") | ||||
|     def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None: | ||||
|         temp_file = str(tmp_path / "temp.webp") | ||||
|         im = Image.new("L", (16384, 16384)) | ||||
|         with pytest.raises(ValueError) as e: | ||||
|             im.save(temp_file) | ||||
|         assert ( | ||||
|             str(e.value) | ||||
|             == "encoding error 5: Image size exceeds WebP limit of 16383 pixels" | ||||
|         ) | ||||
| 
 | ||||
|     def test_WebPEncode_with_invalid_args(self) -> None: | ||||
|         """ | ||||
|         Calling encoder functions with no arguments should result in an error. | ||||
|         """ | ||||
| 
 | ||||
|         if _webp.HAVE_WEBPANIM: | ||||
|             with pytest.raises(TypeError): | ||||
|                 _webp.WebPAnimEncoder() | ||||
|         with pytest.raises(TypeError): | ||||
|             _webp.WebPAnimEncoder() | ||||
|         with pytest.raises(TypeError): | ||||
|             _webp.WebPEncode() | ||||
| 
 | ||||
|     def test_WebPDecode_with_invalid_args(self) -> None: | ||||
|     def test_WebPAnimDecoder_with_invalid_args(self) -> None: | ||||
|         """ | ||||
|         Calling decoder functions with no arguments should result in an error. | ||||
|         """ | ||||
| 
 | ||||
|         if _webp.HAVE_WEBPANIM: | ||||
|             with pytest.raises(TypeError): | ||||
|                 _webp.WebPAnimDecoder() | ||||
|         with pytest.raises(TypeError): | ||||
|             _webp.WebPDecode() | ||||
|             _webp.WebPAnimDecoder() | ||||
| 
 | ||||
|     def test_no_resource_warning(self, tmp_path: Path) -> None: | ||||
|         file_path = "Tests/images/hopper.webp" | ||||
|  | @ -200,7 +201,6 @@ class TestFileWebp: | |||
|         "background", | ||||
|         (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), | ||||
|     ) | ||||
|     @skip_unless_feature("webp_anim") | ||||
|     def test_invalid_background( | ||||
|         self, background: int | tuple[int, ...], tmp_path: Path | ||||
|     ) -> None: | ||||
|  | @ -209,7 +209,6 @@ class TestFileWebp: | |||
|         with pytest.raises(OSError): | ||||
|             im.save(temp_file, save_all=True, append_images=[im], background=background) | ||||
| 
 | ||||
|     @skip_unless_feature("webp_anim") | ||||
|     def test_background_from_gif(self, tmp_path: Path) -> None: | ||||
|         # Save L mode GIF with background | ||||
|         with Image.open("Tests/images/no_palette_with_background.gif") as im: | ||||
|  | @ -234,7 +233,6 @@ class TestFileWebp: | |||
|         difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) | ||||
|         assert difference < 5 | ||||
| 
 | ||||
|     @skip_unless_feature("webp_anim") | ||||
|     def test_duration(self, tmp_path: Path) -> None: | ||||
|         with Image.open("Tests/images/dispose_bgnd.gif") as im: | ||||
|             assert im.info["duration"] == 1000 | ||||
|  | @ -250,6 +248,7 @@ class TestFileWebp: | |||
|         temp_file = str(tmp_path / "temp.webp") | ||||
|         im = Image.new("RGBA", (1, 1)).convert("P") | ||||
|         assert im.mode == "P" | ||||
|         assert im.palette is not None | ||||
|         assert im.palette.mode == "RGBA" | ||||
|         im.save(temp_file) | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,12 +13,7 @@ from .helper import ( | |||
|     hopper, | ||||
| ) | ||||
| 
 | ||||
| _webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") | ||||
| 
 | ||||
| 
 | ||||
| def setup_module() -> None: | ||||
|     if _webp.WebPDecoderBuggyAlpha(): | ||||
|         pytest.skip("Buggy early version of WebP installed, not testing transparency") | ||||
| pytest.importorskip("PIL._webp", reason="WebP support not installed") | ||||
| 
 | ||||
| 
 | ||||
| def test_read_rgba() -> None: | ||||
|  | @ -81,9 +76,6 @@ def test_write_rgba(tmp_path: Path) -> None: | |||
|     pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) | ||||
|     pil_image.save(temp_file) | ||||
| 
 | ||||
|     if _webp.WebPDecoderBuggyAlpha(): | ||||
|         return | ||||
| 
 | ||||
|     with Image.open(temp_file) as image: | ||||
|         image.load() | ||||
| 
 | ||||
|  | @ -93,12 +85,7 @@ def test_write_rgba(tmp_path: Path) -> None: | |||
|         image.load() | ||||
|         image.getdata() | ||||
| 
 | ||||
|         # Early versions of WebP are known to produce higher deviations: | ||||
|         # deal with it | ||||
|         if _webp.WebPDecoderVersion() <= 0x201: | ||||
|             assert_image_similar(image, pil_image, 3.0) | ||||
|         else: | ||||
|             assert_image_similar(image, pil_image, 1.0) | ||||
|         assert_image_similar(image, pil_image, 1.0) | ||||
| 
 | ||||
| 
 | ||||
| def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: | ||||
|  |  | |||
|  | @ -15,10 +15,7 @@ from .helper import ( | |||
|     skip_unless_feature, | ||||
| ) | ||||
| 
 | ||||
| pytestmark = [ | ||||
|     skip_unless_feature("webp"), | ||||
|     skip_unless_feature("webp_anim"), | ||||
| ] | ||||
| pytestmark = skip_unless_feature("webp") | ||||
| 
 | ||||
| 
 | ||||
| def test_n_frames() -> None: | ||||
|  |  | |||
|  | @ -8,14 +8,11 @@ from PIL import Image | |||
| 
 | ||||
| from .helper import assert_image_equal, hopper | ||||
| 
 | ||||
| _webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") | ||||
| pytest.importorskip("PIL._webp", reason="WebP support not installed") | ||||
| RGB_MODE = "RGB" | ||||
| 
 | ||||
| 
 | ||||
| def test_write_lossless_rgb(tmp_path: Path) -> None: | ||||
|     if _webp.WebPDecoderVersion() < 0x0200: | ||||
|         pytest.skip("lossless not included") | ||||
| 
 | ||||
|     temp_file = str(tmp_path / "temp.webp") | ||||
| 
 | ||||
|     hopper(RGB_MODE).save(temp_file, lossless=True) | ||||
|  |  | |||
|  | @ -10,10 +10,7 @@ from PIL import Image | |||
| 
 | ||||
| from .helper import mark_if_feature_version, skip_unless_feature | ||||
| 
 | ||||
| pytestmark = [ | ||||
|     skip_unless_feature("webp"), | ||||
|     skip_unless_feature("webp_mux"), | ||||
| ] | ||||
| pytestmark = skip_unless_feature("webp") | ||||
| 
 | ||||
| ElementTree: ModuleType | None | ||||
| try: | ||||
|  | @ -119,7 +116,15 @@ def test_read_no_exif() -> None: | |||
| def test_getxmp() -> None: | ||||
|     with Image.open("Tests/images/flower.webp") as im: | ||||
|         assert "xmp" not in im.info | ||||
|         assert im.getxmp() == {} | ||||
|         if ElementTree is None: | ||||
|             with pytest.warns( | ||||
|                 UserWarning, | ||||
|                 match="XMP data cannot be read without defusedxml dependency", | ||||
|             ): | ||||
|                 xmp = im.getxmp() | ||||
|         else: | ||||
|             xmp = im.getxmp() | ||||
|         assert xmp == {} | ||||
| 
 | ||||
|     with Image.open("Tests/images/flower2.webp") as im: | ||||
|         if ElementTree is None: | ||||
|  | @ -136,7 +141,6 @@ def test_getxmp() -> None: | |||
|             ) | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("webp_anim") | ||||
| def test_write_animated_metadata(tmp_path: Path) -> None: | ||||
|     iccp_data = b"<iccp_data>" | ||||
|     exif_data = b"<exif_data>" | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ from __future__ import annotations | |||
| 
 | ||||
| import os | ||||
| from pathlib import Path | ||||
| from typing import AnyStr | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
|  | @ -92,7 +93,7 @@ def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: | |||
| 
 | ||||
| 
 | ||||
| def _test_high_characters( | ||||
|     request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes | ||||
|     request: pytest.FixtureRequest, tmp_path: Path, message: AnyStr | ||||
| ) -> None: | ||||
|     tempname = save_font(request, tmp_path) | ||||
|     font = ImageFont.load(tempname) | ||||
|  |  | |||
|  | @ -42,6 +42,12 @@ try: | |||
| except ImportError: | ||||
|     ElementTree = None | ||||
| 
 | ||||
| PrettyPrinter: type | None | ||||
| try: | ||||
|     from IPython.lib.pretty import PrettyPrinter | ||||
| except ImportError: | ||||
|     PrettyPrinter = None | ||||
| 
 | ||||
| 
 | ||||
| # Deprecation helper | ||||
| def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: | ||||
|  | @ -91,16 +97,15 @@ class TestImage: | |||
|         # with pytest.raises(MemoryError): | ||||
|         #   Image.new("L", (1000000, 1000000)) | ||||
| 
 | ||||
|     @pytest.mark.skipif(PrettyPrinter is None, reason="IPython is not installed") | ||||
|     def test_repr_pretty(self) -> None: | ||||
|         class Pretty: | ||||
|             def text(self, text: str) -> None: | ||||
|                 self.pretty_output = text | ||||
| 
 | ||||
|         im = Image.new("L", (100, 100)) | ||||
| 
 | ||||
|         p = Pretty() | ||||
|         im._repr_pretty_(p, None) | ||||
|         assert p.pretty_output == "<PIL.Image.Image image mode=L size=100x100>" | ||||
|         output = io.StringIO() | ||||
|         assert PrettyPrinter is not None | ||||
|         p = PrettyPrinter(output) | ||||
|         im._repr_pretty_(p, False) | ||||
|         assert output.getvalue() == "<PIL.Image.Image image mode=L size=100x100>" | ||||
| 
 | ||||
|     def test_open_formats(self) -> None: | ||||
|         PNGFILE = "Tests/images/hopper.png" | ||||
|  | @ -700,6 +705,7 @@ class TestImage: | |||
|             assert new_image.size == image.size | ||||
|             assert new_image.info == base_image.info | ||||
|             if palette_result is not None: | ||||
|                 assert new_image.palette is not None | ||||
|                 assert new_image.palette.tobytes() == palette_result.tobytes() | ||||
|             else: | ||||
|                 assert new_image.palette is None | ||||
|  | @ -817,7 +823,6 @@ class TestImage: | |||
|             assert reloaded_exif[305] == "Pillow test" | ||||
| 
 | ||||
|     @skip_unless_feature("webp") | ||||
|     @skip_unless_feature("webp_anim") | ||||
|     def test_exif_webp(self, tmp_path: Path) -> None: | ||||
|         with Image.open("Tests/images/hopper.webp") as im: | ||||
|             exif = im.getexif() | ||||
|  | @ -939,7 +944,15 @@ class TestImage: | |||
| 
 | ||||
|     def test_empty_xmp(self) -> None: | ||||
|         with Image.open("Tests/images/hopper.gif") as im: | ||||
|             assert im.getxmp() == {} | ||||
|             if ElementTree is None: | ||||
|                 with pytest.warns( | ||||
|                     UserWarning, | ||||
|                     match="XMP data cannot be read without defusedxml dependency", | ||||
|                 ): | ||||
|                     xmp = im.getxmp() | ||||
|             else: | ||||
|                 xmp = im.getxmp() | ||||
|             assert xmp == {} | ||||
| 
 | ||||
|     def test_getxmp_padded(self) -> None: | ||||
|         im = Image.new("RGB", (1, 1)) | ||||
|  | @ -990,12 +1003,14 @@ class TestImage: | |||
|         # P mode with RGBA palette | ||||
|         im = Image.new("RGBA", (1, 1)).convert("P") | ||||
|         assert im.mode == "P" | ||||
|         assert im.palette is not None | ||||
|         assert im.palette.mode == "RGBA" | ||||
|         assert im.has_transparency_data | ||||
| 
 | ||||
|     def test_apply_transparency(self) -> None: | ||||
|         im = Image.new("P", (1, 1)) | ||||
|         im.putpalette((0, 0, 0, 1, 1, 1)) | ||||
|         assert im.palette is not None | ||||
|         assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} | ||||
| 
 | ||||
|         # Test that no transformation is applied without transparency | ||||
|  | @ -1013,13 +1028,16 @@ class TestImage: | |||
|         im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA") | ||||
|         im.info["transparency"] = 0 | ||||
|         im.apply_transparency() | ||||
|         assert im.palette is not None | ||||
|         assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 128): 1} | ||||
| 
 | ||||
|         # Test that transparency bytes are applied | ||||
|         with Image.open("Tests/images/pil123p.png") as im: | ||||
|             assert isinstance(im.info["transparency"], bytes) | ||||
|             assert im.palette is not None | ||||
|             assert im.palette.colors[(27, 35, 6)] == 24 | ||||
|             im.apply_transparency() | ||||
|             assert im.palette is not None | ||||
|             assert im.palette.colors[(27, 35, 6, 214)] == 24 | ||||
| 
 | ||||
|     def test_constants(self) -> None: | ||||
|  |  | |||
|  | @ -230,7 +230,7 @@ class TestImagePutPixelError: | |||
|                 im.putpixel((0, 0), v)  # type: ignore[arg-type] | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         ("mode", "band_numbers", "match"), | ||||
|         "mode, band_numbers, match", | ||||
|         ( | ||||
|             ("L", (0, 2), "color must be int or single-element tuple"), | ||||
|             ("LA", (0, 3), "color must be int, or tuple of one or two elements"), | ||||
|  |  | |||
|  | @ -47,7 +47,7 @@ def test_toarray() -> None: | |||
|             with pytest.raises(OSError): | ||||
|                 numpy.array(im_truncated) | ||||
|         else: | ||||
|             with pytest.warns(UserWarning): | ||||
|             with pytest.warns(DeprecationWarning): | ||||
|                 numpy.array(im_truncated) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -113,4 +113,5 @@ def test_fromarray_palette() -> None: | |||
|     out = Image.fromarray(a, "P") | ||||
| 
 | ||||
|     # Assert that the Python and C palettes match | ||||
|     assert out.palette is not None | ||||
|     assert len(out.palette.colors) == len(out.im.getpalette()) / 3 | ||||
|  |  | |||
|  | @ -218,6 +218,7 @@ def test_trns_RGB(tmp_path: Path) -> None: | |||
| def test_l_macro_rounding(convert_mode: str) -> None: | ||||
|     for mode in ("P", "PA"): | ||||
|         im = Image.new(mode, (1, 1)) | ||||
|         assert im.palette is not None | ||||
|         im.palette.getcolor((0, 1, 2)) | ||||
| 
 | ||||
|         converted_im = im.convert(convert_mode) | ||||
|  |  | |||
|  | @ -86,6 +86,7 @@ def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: | |||
|     im = Image.new("P", (1, 1)) | ||||
|     im.putpalette(palette, mode) | ||||
|     assert im.getpalette() == [1, 2, 3] | ||||
|     assert im.palette is not None | ||||
|     assert im.palette.colors == {(1, 2, 3, 4): 0} | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -69,6 +69,7 @@ def test_quantize_no_dither() -> None: | |||
| 
 | ||||
|     converted = image.quantize(dither=Image.Dither.NONE, palette=palette) | ||||
|     assert converted.mode == "P" | ||||
|     assert converted.palette is not None | ||||
|     assert converted.palette.palette == palette.palette.palette | ||||
| 
 | ||||
| 
 | ||||
|  | @ -81,6 +82,7 @@ def test_quantize_no_dither2() -> None: | |||
|     palette.putpalette(data) | ||||
|     quantized = im.quantize(dither=Image.Dither.NONE, palette=palette) | ||||
| 
 | ||||
|     assert quantized.palette is not None | ||||
|     assert tuple(quantized.palette.palette) == data | ||||
| 
 | ||||
|     px = quantized.load() | ||||
|  | @ -117,6 +119,7 @@ def test_colors() -> None: | |||
|     im = hopper() | ||||
|     colors = 2 | ||||
|     converted = im.quantize(colors) | ||||
|     assert converted.palette is not None | ||||
|     assert len(converted.palette.palette) == colors * len("RGB") | ||||
| 
 | ||||
| 
 | ||||
|  | @ -147,6 +150,7 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None: | |||
|     converted = im.quantize(method=method) | ||||
|     converted_px = converted.load() | ||||
|     assert converted_px is not None | ||||
|     assert converted.palette is not None | ||||
|     assert converted_px[0, 0] == converted.palette.colors[color] | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -398,7 +398,8 @@ def test_logical() -> None: | |||
|             for y in (a, b): | ||||
|                 imy = Image.new("1", (1, 1), y) | ||||
|                 value = op(imx, imy).getpixel((0, 0)) | ||||
|                 assert not isinstance(value, tuple) and value is not None | ||||
|                 assert not isinstance(value, tuple) | ||||
|                 assert value is not None | ||||
|                 out.append(value) | ||||
|         return out | ||||
| 
 | ||||
|  |  | |||
|  | @ -857,6 +857,27 @@ def test_rounded_rectangle_corners( | |||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def test_rounded_rectangle_joined_x_different_corners() -> None: | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw.Draw(im, "RGBA") | ||||
| 
 | ||||
|     # Act | ||||
|     draw.rounded_rectangle( | ||||
|         (20, 10, 80, 90), | ||||
|         30, | ||||
|         fill="red", | ||||
|         outline="green", | ||||
|         width=5, | ||||
|         corners=(True, False, False, False), | ||||
|     ) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert_image_equal_tofile( | ||||
|         im, "Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png" | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "xy, radius, type", | ||||
|     [ | ||||
|  |  | |||
|  | @ -65,6 +65,36 @@ def test_mode() -> None: | |||
|         ImageDraw2.Draw("L") | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("bbox", BBOX) | ||||
| @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) | ||||
| def test_arc(bbox: Coords, start: float, end: float) -> None: | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw2.Draw(im) | ||||
|     pen = ImageDraw2.Pen("white", width=1) | ||||
| 
 | ||||
|     # Act | ||||
|     draw.arc(bbox, pen, start, end) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("bbox", BBOX) | ||||
| def test_chord(bbox: Coords) -> None: | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw2.Draw(im) | ||||
|     pen = ImageDraw2.Pen("yellow") | ||||
|     brush = ImageDraw2.Brush("red") | ||||
| 
 | ||||
|     # Act | ||||
|     draw.chord(bbox, pen, 0, 180, brush) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_RGB.png", 1) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("bbox", BBOX) | ||||
| def test_ellipse(bbox: Coords) -> None: | ||||
|     # Arrange | ||||
|  | @ -123,6 +153,22 @@ def test_line_pen_as_brush(points: Coords) -> None: | |||
|     assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("bbox", BBOX) | ||||
| @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) | ||||
| def test_pieslice(bbox: Coords, start: float, end: float) -> None: | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw2.Draw(im) | ||||
|     pen = ImageDraw2.Pen("blue") | ||||
|     brush = ImageDraw2.Brush("white") | ||||
| 
 | ||||
|     # Act | ||||
|     draw.pieslice(bbox, pen, start, end, brush) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("points", POINTS) | ||||
| def test_polygon(points: Coords) -> None: | ||||
|     # Arrange | ||||
|  |  | |||
|  | @ -94,7 +94,6 @@ class TestImageFile: | |||
|             assert (48, 48) == p.image.size | ||||
| 
 | ||||
|     @skip_unless_feature("webp") | ||||
|     @skip_unless_feature("webp_anim") | ||||
|     def test_incremental_webp(self) -> None: | ||||
|         with ImageFile.Parser() as p: | ||||
|             with open("Tests/images/hopper.webp", "rb") as f: | ||||
|  | @ -318,7 +317,13 @@ class TestPyEncoder(CodecsTest): | |||
| 
 | ||||
|         fp = BytesIO() | ||||
|         ImageFile._save( | ||||
|             im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] | ||||
|             im, | ||||
|             fp, | ||||
|             [ | ||||
|                 ImageFile._Tile( | ||||
|                     "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB" | ||||
|                 ) | ||||
|             ], | ||||
|         ) | ||||
| 
 | ||||
|         assert MockPyEncoder.last | ||||
|  | @ -334,7 +339,7 @@ class TestPyEncoder(CodecsTest): | |||
|         im.tile = [("MOCK", None, 32, None)] | ||||
| 
 | ||||
|         fp = BytesIO() | ||||
|         ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) | ||||
|         ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")]) | ||||
| 
 | ||||
|         assert MockPyEncoder.last | ||||
|         assert MockPyEncoder.last.state.xoff == 0 | ||||
|  | @ -351,7 +356,9 @@ class TestPyEncoder(CodecsTest): | |||
|         MockPyEncoder.last = None | ||||
|         with pytest.raises(ValueError): | ||||
|             ImageFile._save( | ||||
|                 im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] | ||||
|                 im, | ||||
|                 fp, | ||||
|                 [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")], | ||||
|             ) | ||||
|         last: MockPyEncoder | None = MockPyEncoder.last | ||||
|         assert last | ||||
|  | @ -359,7 +366,9 @@ class TestPyEncoder(CodecsTest): | |||
| 
 | ||||
|         with pytest.raises(ValueError): | ||||
|             ImageFile._save( | ||||
|                 im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] | ||||
|                 im, | ||||
|                 fp, | ||||
|                 [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")], | ||||
|             ) | ||||
| 
 | ||||
|     def test_oversize(self) -> None: | ||||
|  | @ -372,14 +381,22 @@ class TestPyEncoder(CodecsTest): | |||
|             ImageFile._save( | ||||
|                 im, | ||||
|                 fp, | ||||
|                 [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], | ||||
|                 [ | ||||
|                     ImageFile._Tile( | ||||
|                         "MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB" | ||||
|                     ) | ||||
|                 ], | ||||
|             ) | ||||
| 
 | ||||
|         with pytest.raises(ValueError): | ||||
|             ImageFile._save( | ||||
|                 im, | ||||
|                 fp, | ||||
|                 [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], | ||||
|                 [ | ||||
|                     ImageFile._Tile( | ||||
|                         "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB" | ||||
|                     ) | ||||
|                 ], | ||||
|             ) | ||||
| 
 | ||||
|     def test_encode(self) -> None: | ||||
|  | @ -395,9 +412,8 @@ class TestPyEncoder(CodecsTest): | |||
|         with pytest.raises(NotImplementedError): | ||||
|             encoder.encode_to_pyfd() | ||||
| 
 | ||||
|         fh = BytesIO() | ||||
|         with pytest.raises(NotImplementedError): | ||||
|             encoder.encode_to_file(fh, 0) | ||||
|             encoder.encode_to_file(0, 0) | ||||
| 
 | ||||
|     def test_zero_height(self) -> None: | ||||
|         with pytest.raises(UnidentifiedImageError): | ||||
|  |  | |||
|  | @ -717,14 +717,14 @@ def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: | |||
| 
 | ||||
|     font = ImageFont.truetype("Tests/fonts/AdobeVFPrototype.ttf", 36) | ||||
|     _check_text(font, "Tests/images/variation_adobe.png", 11) | ||||
|     for name in ["Bold", b"Bold"]: | ||||
|     for name in ("Bold", b"Bold"): | ||||
|         font.set_variation_by_name(name) | ||||
|         assert font.getname()[1] == "Bold" | ||||
|     _check_text(font, "Tests/images/variation_adobe_name.png", 16) | ||||
| 
 | ||||
|     font = ImageFont.truetype("Tests/fonts/TINY5x3GX.ttf", 36) | ||||
|     _check_text(font, "Tests/images/variation_tiny.png", 40) | ||||
|     for name in ["200", b"200"]: | ||||
|     for name in ("200", b"200"): | ||||
|         font.set_variation_by_name(name) | ||||
|         assert font.getname()[1] == "200" | ||||
|     _check_text(font, "Tests/images/variation_tiny_name.png", 40) | ||||
|  |  | |||
|  | @ -46,7 +46,8 @@ def img_to_string(im: Image.Image) -> str: | |||
|         line = "" | ||||
|         for c in range(im.width): | ||||
|             value = im.getpixel((c, r)) | ||||
|             assert not isinstance(value, tuple) and value is not None | ||||
|             assert not isinstance(value, tuple) | ||||
|             assert value is not None | ||||
|             line += chars[value > 0] | ||||
|         result.append(line) | ||||
|     return "\n".join(result) | ||||
|  |  | |||
|  | @ -390,7 +390,7 @@ def test_colorize_3color_offset() -> None: | |||
| 
 | ||||
| def test_exif_transpose() -> None: | ||||
|     exts = [".jpg"] | ||||
|     if features.check("webp") and features.check("webp_anim"): | ||||
|     if features.check("webp"): | ||||
|         exts.append(".webp") | ||||
|     for ext in exts: | ||||
|         with Image.open("Tests/images/hopper" + ext) as base_im: | ||||
|  |  | |||
|  | @ -46,7 +46,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     ("test_file", "test_mode"), | ||||
|     "test_file, test_mode", | ||||
|     [ | ||||
|         ("Tests/images/hopper.jpg", None), | ||||
|         ("Tests/images/hopper.jpg", "L"), | ||||
|  |  | |||
|  | @ -5,8 +5,6 @@ import sys | |||
| from io import BytesIO | ||||
| from pathlib import Path | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, PSDraw | ||||
| 
 | ||||
| 
 | ||||
|  | @ -49,17 +47,16 @@ def test_draw_postscript(tmp_path: Path) -> None: | |||
|     assert os.path.getsize(tempfile) > 0 | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("buffer", (True, False)) | ||||
| def test_stdout(buffer: bool) -> None: | ||||
| def test_stdout() -> None: | ||||
|     # Temporarily redirect stdout | ||||
|     old_stdout = sys.stdout | ||||
| 
 | ||||
|     class MyStdOut: | ||||
|         buffer = BytesIO() | ||||
| 
 | ||||
|     mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() | ||||
|     mystdout = MyStdOut() | ||||
| 
 | ||||
|     sys.stdout = mystdout  # type: ignore[assignment] | ||||
|     sys.stdout = mystdout | ||||
| 
 | ||||
|     ps = PSDraw.PSDraw() | ||||
|     _create_document(ps) | ||||
|  | @ -67,6 +64,4 @@ def test_stdout(buffer: bool) -> None: | |||
|     # Reset stdout | ||||
|     sys.stdout = old_stdout | ||||
| 
 | ||||
|     if isinstance(mystdout, MyStdOut): | ||||
|         mystdout = mystdout.buffer | ||||
|     assert mystdout.getvalue() != b"" | ||||
|     assert mystdout.buffer.getvalue() != b"" | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| from pathlib import Path | ||||
| from typing import TYPE_CHECKING, Union | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
|  | @ -8,6 +9,20 @@ from PIL import Image, ImageQt | |||
| 
 | ||||
| from .helper import assert_image_equal_tofile, assert_image_similar, hopper | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     import PyQt6 | ||||
|     import PySide6 | ||||
| 
 | ||||
|     QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication] | ||||
|     QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout] | ||||
|     QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] | ||||
|     QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel] | ||||
|     QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter] | ||||
|     QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] | ||||
|     QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint] | ||||
|     QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion] | ||||
|     QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget] | ||||
| 
 | ||||
| if ImageQt.qt_is_installed: | ||||
|     from PIL.ImageQt import QPixmap | ||||
| 
 | ||||
|  | @ -20,7 +35,7 @@ if ImageQt.qt_is_installed: | |||
|         from PySide6.QtGui import QImage, QPainter, QRegion | ||||
|         from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget | ||||
| 
 | ||||
|     class Example(QWidget): | ||||
|     class Example(QWidget):  # type: ignore[misc] | ||||
|         def __init__(self) -> None: | ||||
|             super().__init__() | ||||
| 
 | ||||
|  | @ -28,11 +43,12 @@ if ImageQt.qt_is_installed: | |||
| 
 | ||||
|             qimage = ImageQt.ImageQt(img) | ||||
| 
 | ||||
|             pixmap1 = ImageQt.QPixmap.fromImage(qimage) | ||||
|             pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage) | ||||
| 
 | ||||
|             QHBoxLayout(self)  # hbox | ||||
|             # hbox | ||||
|             QHBoxLayout(self)  # type: ignore[operator] | ||||
| 
 | ||||
|             lbl = QLabel(self) | ||||
|             lbl = QLabel(self)  # type: ignore[operator] | ||||
|             # Segfault in the problem | ||||
|             lbl.setPixmap(pixmap1.copy()) | ||||
| 
 | ||||
|  | @ -46,7 +62,7 @@ def roundtrip(expected: Image.Image) -> None: | |||
| @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") | ||||
| def test_sanity(tmp_path: Path) -> None: | ||||
|     # Segfault test | ||||
|     app: QApplication | None = QApplication([]) | ||||
|     app: QApplication | None = QApplication([])  # type: ignore[operator] | ||||
|     ex = Example() | ||||
|     assert app  # Silence warning | ||||
|     assert ex  # Silence warning | ||||
|  | @ -56,7 +72,7 @@ def test_sanity(tmp_path: Path) -> None: | |||
|         im = hopper(mode) | ||||
|         data = ImageQt.toqpixmap(im) | ||||
| 
 | ||||
|         assert isinstance(data, QPixmap) | ||||
|         assert data.__class__.__name__ == "QPixmap" | ||||
|         assert not data.isNull() | ||||
| 
 | ||||
|         # Test saving the file | ||||
|  | @ -64,14 +80,14 @@ def test_sanity(tmp_path: Path) -> None: | |||
|         data.save(tempfile) | ||||
| 
 | ||||
|         # Render the image | ||||
|         qimage = ImageQt.ImageQt(im) | ||||
|         data = QPixmap.fromImage(qimage) | ||||
|         qt_format = QImage.Format if ImageQt.qt_version == "6" else QImage | ||||
|         qimage = QImage(128, 128, qt_format.Format_ARGB32) | ||||
|         painter = QPainter(qimage) | ||||
|         image_label = QLabel() | ||||
|         imageqt = ImageQt.ImageQt(im) | ||||
|         data = getattr(QPixmap, "fromImage")(imageqt) | ||||
|         qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage | ||||
|         qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32"))  # type: ignore[operator] | ||||
|         painter = QPainter(qimage)  # type: ignore[operator] | ||||
|         image_label = QLabel()  # type: ignore[operator] | ||||
|         image_label.setPixmap(data) | ||||
|         image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) | ||||
|         image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128))  # type: ignore[operator] | ||||
|         painter.end() | ||||
|         rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png") | ||||
|         qimage.save(rendered_tempfile) | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None: | |||
|     src = hopper(mode) | ||||
|     data = ImageQt.toqimage(src) | ||||
| 
 | ||||
|     assert isinstance(data, QImage) | ||||
|     assert isinstance(data, QImage)  # type: ignore[arg-type, misc] | ||||
|     assert not data.isNull() | ||||
| 
 | ||||
|     # reload directly from the qimage | ||||
|  |  | |||
|  | @ -54,8 +54,8 @@ def test_nonetype() -> None: | |||
|     assert xres.denominator is not None | ||||
|     assert yres._val is not None | ||||
| 
 | ||||
|     assert xres and 1 | ||||
|     assert xres and yres | ||||
|     assert xres | ||||
|     assert yres | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|  |  | |||
|  | @ -30,28 +30,6 @@ def test_is_not_path(tmp_path: Path) -> None: | |||
|     assert not it_is_not | ||||
| 
 | ||||
| 
 | ||||
| def test_is_directory() -> None: | ||||
|     # Arrange | ||||
|     directory = "Tests" | ||||
| 
 | ||||
|     # Act | ||||
|     it_is = _util.is_directory(directory) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert it_is | ||||
| 
 | ||||
| 
 | ||||
| def test_is_not_directory() -> None: | ||||
|     # Arrange | ||||
|     text = "abc" | ||||
| 
 | ||||
|     # Act | ||||
|     it_is_not = _util.is_directory(text) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert not it_is_not | ||||
| 
 | ||||
| 
 | ||||
| def test_deferred_error() -> None: | ||||
|     # Arrange | ||||
| 
 | ||||
|  |  | |||
|  | @ -118,6 +118,24 @@ The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and | |||
| :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword | ||||
| arguments can be used instead. | ||||
| 
 | ||||
| JpegImageFile.huffman_ac and JpegImageFile.huffman_dc | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| .. deprecated:: 11.0.0 | ||||
| 
 | ||||
| The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They | ||||
| have been deprecated, and will be removed in Pillow 12 (2025-10-15). | ||||
| 
 | ||||
| Specific WebP Feature Checks | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| .. deprecated:: 11.0.0 | ||||
| 
 | ||||
| ``features.check("transp_webp")``, ``features.check("webp_mux")`` and | ||||
| ``features.check("webp_anim")`` are now deprecated. They will always return | ||||
| ``True`` if the WebP module is installed, until they are removed in Pillow | ||||
| 12.0.0 (2025-10-15). | ||||
| 
 | ||||
| Removed features | ||||
| ---------------- | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ from __future__ import annotations | |||
| 
 | ||||
| import struct | ||||
| from io import BytesIO | ||||
| from typing import IO | ||||
| 
 | ||||
| from PIL import Image, ImageFile | ||||
| 
 | ||||
|  | @ -94,26 +95,26 @@ DXT3_FOURCC = 0x33545844 | |||
| DXT5_FOURCC = 0x35545844 | ||||
| 
 | ||||
| 
 | ||||
| def _decode565(bits): | ||||
| def _decode565(bits: int) -> tuple[int, int, int]: | ||||
|     a = ((bits >> 11) & 0x1F) << 3 | ||||
|     b = ((bits >> 5) & 0x3F) << 2 | ||||
|     c = (bits & 0x1F) << 3 | ||||
|     return a, b, c | ||||
| 
 | ||||
| 
 | ||||
| def _c2a(a, b): | ||||
| def _c2a(a: int, b: int) -> int: | ||||
|     return (2 * a + b) // 3 | ||||
| 
 | ||||
| 
 | ||||
| def _c2b(a, b): | ||||
| def _c2b(a: int, b: int) -> int: | ||||
|     return (a + b) // 2 | ||||
| 
 | ||||
| 
 | ||||
| def _c3(a, b): | ||||
| def _c3(a: int, b: int) -> int: | ||||
|     return (2 * b + a) // 3 | ||||
| 
 | ||||
| 
 | ||||
| def _dxt1(data, width, height): | ||||
| def _dxt1(data: IO[bytes], width: int, height: int) -> bytes: | ||||
|     # TODO implement this function as pixel format in decode.c | ||||
|     ret = bytearray(4 * width * height) | ||||
| 
 | ||||
|  | @ -151,7 +152,7 @@ def _dxt1(data, width, height): | |||
|     return bytes(ret) | ||||
| 
 | ||||
| 
 | ||||
| def _dxtc_alpha(a0, a1, ac0, ac1, ai): | ||||
| def _dxtc_alpha(a0: int, a1: int, ac0: int, ac1: int, ai: int) -> int: | ||||
|     if ai <= 12: | ||||
|         ac = (ac0 >> ai) & 7 | ||||
|     elif ai == 15: | ||||
|  | @ -175,7 +176,7 @@ def _dxtc_alpha(a0, a1, ac0, ac1, ai): | |||
|     return alpha | ||||
| 
 | ||||
| 
 | ||||
| def _dxt5(data, width, height): | ||||
| def _dxt5(data: IO[bytes], width: int, height: int) -> bytes: | ||||
|     # TODO implement this function as pixel format in decode.c | ||||
|     ret = bytearray(4 * width * height) | ||||
| 
 | ||||
|  | @ -211,7 +212,7 @@ class DdsImageFile(ImageFile.ImageFile): | |||
|     format = "DDS" | ||||
|     format_description = "DirectDraw Surface" | ||||
| 
 | ||||
|     def _open(self): | ||||
|     def _open(self) -> None: | ||||
|         if not _accept(self.fp.read(4)): | ||||
|             msg = "not a DDS file" | ||||
|             raise SyntaxError(msg) | ||||
|  | @ -242,19 +243,20 @@ class DdsImageFile(ImageFile.ImageFile): | |||
|         elif fourcc == b"DXT5": | ||||
|             self.decoder = "DXT5" | ||||
|         else: | ||||
|             msg = f"Unimplemented pixel format {fourcc}" | ||||
|             msg = f"Unimplemented pixel format {repr(fourcc)}" | ||||
|             raise NotImplementedError(msg) | ||||
| 
 | ||||
|         self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] | ||||
| 
 | ||||
|     def load_seek(self, pos): | ||||
|     def load_seek(self, pos: int) -> None: | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| class DXT1Decoder(ImageFile.PyDecoder): | ||||
|     _pulls_fd = True | ||||
| 
 | ||||
|     def decode(self, buffer): | ||||
|     def decode(self, buffer: bytes) -> tuple[int, int]: | ||||
|         assert self.fd is not None | ||||
|         try: | ||||
|             self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) | ||||
|         except struct.error as e: | ||||
|  | @ -266,7 +268,8 @@ class DXT1Decoder(ImageFile.PyDecoder): | |||
| class DXT5Decoder(ImageFile.PyDecoder): | ||||
|     _pulls_fd = True | ||||
| 
 | ||||
|     def decode(self, buffer): | ||||
|     def decode(self, buffer: bytes) -> tuple[int, int]: | ||||
|         assert self.fd is not None | ||||
|         try: | ||||
|             self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) | ||||
|         except struct.error as e: | ||||
|  | @ -279,7 +282,7 @@ Image.register_decoder("DXT1", DXT1Decoder) | |||
| Image.register_decoder("DXT5", DXT5Decoder) | ||||
| 
 | ||||
| 
 | ||||
| def _accept(prefix): | ||||
| def _accept(prefix: bytes) -> bool: | ||||
|     return prefix[:4] == b"DDS " | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/animated_hopper.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 57 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/contrasted_hopper.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/cropped_hopper.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/enhanced_hopper.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/flip_left_right_hopper.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/flip_top_bottom_hopper.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/hopper_ps.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 8.1 KiB | 
|  | @ -1220,8 +1220,7 @@ using the general tags available through tiffinfo. | |||
| WebP | ||||
| ^^^^ | ||||
| 
 | ||||
| Pillow reads and writes WebP files. The specifics of Pillow's capabilities with | ||||
| this format are currently undocumented. | ||||
| Pillow reads and writes WebP files. Requires libwebp v0.5.0 or later. | ||||
| 
 | ||||
| .. _webp-saving: | ||||
| 
 | ||||
|  | @ -1249,29 +1248,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: | |||
| **exact** | ||||
|     If true, preserve the transparent RGB values. Otherwise, discard | ||||
|     invisible RGB values for better compression. Defaults to false. | ||||
|     Requires libwebp 0.5.0 or later. | ||||
| 
 | ||||
| **icc_profile** | ||||
|     The ICC Profile to include in the saved file. Only supported if | ||||
|     the system WebP library was built with webpmux support. | ||||
|     The ICC Profile to include in the saved file. | ||||
| 
 | ||||
| **exif** | ||||
|     The exif data to include in the saved file. Only supported if | ||||
|     the system WebP library was built with webpmux support. | ||||
|     The exif data to include in the saved file. | ||||
| 
 | ||||
| **xmp** | ||||
|     The XMP data to include in the saved file. Only supported if | ||||
|     the system WebP library was built with webpmux support. | ||||
|     The XMP data to include in the saved file. | ||||
| 
 | ||||
| Saving sequences | ||||
| ~~~~~~~~~~~~~~~~ | ||||
| 
 | ||||
| .. note:: | ||||
| 
 | ||||
|     Support for animated WebP files will only be enabled if the system WebP | ||||
|     library is v0.5.0 or later. You can check webp animation support at | ||||
|     runtime by calling ``features.check("webp_anim")``. | ||||
| 
 | ||||
| When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default | ||||
| only the first frame of a multiframe image will be saved. If the ``save_all`` | ||||
| argument is present and true, then all frames will be saved, and the following | ||||
|  | @ -1528,19 +1517,21 @@ To add other read or write support, use | |||
| :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF | ||||
| handler. :: | ||||
| 
 | ||||
|     from PIL import Image | ||||
|     from typing import IO | ||||
| 
 | ||||
|     from PIL import Image, ImageFile | ||||
|     from PIL import WmfImagePlugin | ||||
| 
 | ||||
| 
 | ||||
|     class WmfHandler: | ||||
|         def open(self, im): | ||||
|     class WmfHandler(ImageFile.StubHandler): | ||||
|         def open(self, im: ImageFile.StubImageFile) -> None: | ||||
|             ... | ||||
| 
 | ||||
|         def load(self, im): | ||||
|         def load(self, im: ImageFile.StubImageFile) -> Image.Image: | ||||
|             ... | ||||
|             return image | ||||
| 
 | ||||
|         def save(self, im, fp, filename): | ||||
|         def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: | ||||
|             ... | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/masked_hopper.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/merged_hopper.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 7.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/pasted_hopper.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/rebanded_hopper.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/rolled_hopper.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/rotated_hopper_180.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/rotated_hopper_270.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/rotated_hopper_90.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/show_hopper.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/thumbnail_hopper.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/handbook/transformed_hopper.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.0 KiB | 
|  | @ -37,6 +37,9 @@ example, let’s display the image we just loaded:: | |||
| 
 | ||||
|     >>> im.show() | ||||
| 
 | ||||
| .. image:: show_hopper.webp | ||||
|     :align: center | ||||
| 
 | ||||
| .. note:: | ||||
| 
 | ||||
|     The standard version of :py:meth:`~PIL.Image.Image.show` is not very | ||||
|  | @ -79,6 +82,9 @@ Convert files to JPEG | |||
|             except OSError: | ||||
|                 print("cannot convert", infile) | ||||
| 
 | ||||
| .. image:: ../../Tests/images/hopper.jpg | ||||
|     :align: center | ||||
| 
 | ||||
| A second argument can be supplied to the :py:meth:`~PIL.Image.Image.save` | ||||
| method which explicitly specifies a file format. If you use a non-standard | ||||
| extension, you must always specify the format this way: | ||||
|  | @ -103,6 +109,9 @@ Create JPEG thumbnails | |||
|             except OSError: | ||||
|                 print("cannot create thumbnail for", infile) | ||||
| 
 | ||||
| .. image:: thumbnail_hopper.jpg | ||||
|     :align: center | ||||
| 
 | ||||
| It is important to note that the library doesn’t decode or load the raster data | ||||
| unless it really has to. When you open a file, the file header is read to | ||||
| determine the file format and extract things like mode, size, and other | ||||
|  | @ -140,16 +149,19 @@ Copying a subrectangle from an image | |||
| 
 | ||||
| :: | ||||
| 
 | ||||
|     box = (100, 100, 400, 400) | ||||
|     box = (0, 0, 64, 64) | ||||
|     region = im.crop(box) | ||||
| 
 | ||||
| The region is defined by a 4-tuple, where coordinates are (left, upper, right, | ||||
| lower). The Python Imaging Library uses a coordinate system with (0, 0) in the | ||||
| upper left corner. Also note that coordinates refer to positions between the | ||||
| pixels, so the region in the above example is exactly 300x300 pixels. | ||||
| pixels, so the region in the above example is exactly 64x64 pixels. | ||||
| 
 | ||||
| The region could now be processed in a certain manner and pasted back. | ||||
| 
 | ||||
| .. image:: cropped_hopper.webp | ||||
|     :align: center | ||||
| 
 | ||||
| Processing a subrectangle, and pasting it back | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
|  | @ -164,6 +176,9 @@ modes of the original image and the region do not need to match. If they don’t | |||
| the region is automatically converted before being pasted (see the section on | ||||
| :ref:`color-transforms` below for details). | ||||
| 
 | ||||
| .. image:: pasted_hopper.webp | ||||
|     :align: center | ||||
| 
 | ||||
| Here’s an additional example: | ||||
| 
 | ||||
| Rolling an image | ||||
|  | @ -171,7 +186,7 @@ Rolling an image | |||
| 
 | ||||
| :: | ||||
| 
 | ||||
|     def roll(im, delta): | ||||
|     def roll(im: Image.Image, delta: int) -> Image.Image: | ||||
|         """Roll an image sideways.""" | ||||
|         xsize, ysize = im.size | ||||
| 
 | ||||
|  | @ -186,6 +201,9 @@ Rolling an image | |||
| 
 | ||||
|         return im | ||||
| 
 | ||||
| .. image:: rolled_hopper.webp | ||||
|     :align: center | ||||
| 
 | ||||
| Or if you would like to merge two images into a wider image: | ||||
| 
 | ||||
| Merging images | ||||
|  | @ -193,7 +211,7 @@ Merging images | |||
| 
 | ||||
| :: | ||||
| 
 | ||||
|     def merge(im1, im2): | ||||
|     def merge(im1: Image.Image, im2: Image.Image) -> Image.Image: | ||||
|         w = im1.size[0] + im2.size[0] | ||||
|         h = max(im1.size[1], im2.size[1]) | ||||
|         im = Image.new("RGBA", (w, h)) | ||||
|  | @ -203,6 +221,9 @@ Merging images | |||
| 
 | ||||
|         return im | ||||
| 
 | ||||
| .. image:: merged_hopper.webp | ||||
|     :align: center | ||||
| 
 | ||||
| For more advanced tricks, the paste method can also take a transparency mask as | ||||
| an optional argument. In this mask, the value 255 indicates that the pasted | ||||
| image is opaque in that position (that is, the pasted image should be used as | ||||
|  | @ -229,6 +250,9 @@ Note that for a single-band image, :py:meth:`~PIL.Image.Image.split` returns | |||
| the image itself. To work with individual color bands, you may want to convert | ||||
| the image to “RGB” first. | ||||
| 
 | ||||
| .. image:: rebanded_hopper.webp | ||||
|     :align: center | ||||
| 
 | ||||
| Geometrical transforms | ||||
| ---------------------- | ||||
| 
 | ||||
|  | @ -245,6 +269,9 @@ Simple geometry transforms | |||
|     out = im.resize((128, 128)) | ||||
|     out = im.rotate(45) # degrees counter-clockwise | ||||
| 
 | ||||
| .. image:: rotated_hopper_90.webp | ||||
|     :align: center | ||||
| 
 | ||||
| To rotate the image in 90 degree steps, you can either use the | ||||
| :py:meth:`~PIL.Image.Image.rotate` method or the | ||||
| :py:meth:`~PIL.Image.Image.transpose` method. The latter can also be used to | ||||
|  | @ -256,11 +283,38 @@ Transposing an image | |||
| :: | ||||
| 
 | ||||
|     out = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) | ||||
| 
 | ||||
| .. image:: flip_left_right_hopper.webp | ||||
|     :align: center | ||||
| 
 | ||||
| :: | ||||
| 
 | ||||
|     out = im.transpose(Image.Transpose.FLIP_TOP_BOTTOM) | ||||
| 
 | ||||
| .. image:: flip_top_bottom_hopper.webp | ||||
|     :align: center | ||||
| 
 | ||||
| :: | ||||
| 
 | ||||
|     out = im.transpose(Image.Transpose.ROTATE_90) | ||||
| 
 | ||||
| .. image:: rotated_hopper_90.webp | ||||
|     :align: center | ||||
| 
 | ||||
| :: | ||||
| 
 | ||||
|     out = im.transpose(Image.Transpose.ROTATE_180) | ||||
| 
 | ||||
| .. image:: rotated_hopper_180.webp | ||||
|     :align: center | ||||
| 
 | ||||
| :: | ||||
| 
 | ||||
|     out = im.transpose(Image.Transpose.ROTATE_270) | ||||
| 
 | ||||
| .. image:: rotated_hopper_270.webp | ||||
|     :align: center | ||||
| 
 | ||||
| ``transpose(ROTATE)`` operations can also be performed identically with | ||||
| :py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is | ||||
| true, to provide for the same changes to the image's size. | ||||
|  | @ -278,7 +332,7 @@ choose to resize relative to a given size. | |||
| 
 | ||||
|     from PIL import Image, ImageOps | ||||
|     size = (100, 150) | ||||
|     with Image.open("Tests/images/hopper.webp") as im: | ||||
|     with Image.open("hopper.webp") as im: | ||||
|         ImageOps.contain(im, size).save("imageops_contain.webp") | ||||
|         ImageOps.cover(im, size).save("imageops_cover.webp") | ||||
|         ImageOps.fit(im, size).save("imageops_fit.webp") | ||||
|  | @ -342,6 +396,9 @@ Applying filters | |||
|     from PIL import ImageFilter | ||||
|     out = im.filter(ImageFilter.DETAIL) | ||||
| 
 | ||||
| .. image:: enhanced_hopper.webp | ||||
|     :align: center | ||||
| 
 | ||||
| Point Operations | ||||
| ^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
|  | @ -355,8 +412,11 @@ Applying point transforms | |||
| 
 | ||||
| :: | ||||
| 
 | ||||
|     # multiply each pixel by 1.2 | ||||
|     out = im.point(lambda i: i * 1.2) | ||||
|     # multiply each pixel by 20 | ||||
|     out = im.point(lambda i: i * 20) | ||||
| 
 | ||||
| .. image:: transformed_hopper.webp | ||||
|     :align: center | ||||
| 
 | ||||
| Using the above technique, you can quickly apply any simple expression to an | ||||
| image. You can also combine the :py:meth:`~PIL.Image.Image.point` and | ||||
|  | @ -388,6 +448,9 @@ Note the syntax used to create the mask:: | |||
| 
 | ||||
|     imout = im.point(lambda i: expression and 255) | ||||
| 
 | ||||
| .. image:: masked_hopper.webp | ||||
|     :align: center | ||||
| 
 | ||||
| Python only evaluates the portion of a logical expression as is necessary to | ||||
| determine the outcome, and returns the last value examined as the result of the | ||||
| expression. So if the expression above is false (0), Python does not look at | ||||
|  | @ -412,6 +475,10 @@ Enhancing images | |||
|     enh = ImageEnhance.Contrast(im) | ||||
|     enh.enhance(1.3).show("30% more contrast") | ||||
| 
 | ||||
| 
 | ||||
| .. image:: contrasted_hopper.jpg | ||||
|     :align: center | ||||
| 
 | ||||
| Image sequences | ||||
| --------------- | ||||
| 
 | ||||
|  | @ -444,10 +511,43 @@ Reading sequences | |||
| As seen in this example, you’ll get an :py:exc:`EOFError` exception when the | ||||
| sequence ends. | ||||
| 
 | ||||
| Writing sequences | ||||
| ^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| You can create animated GIFs with Pillow, e.g. | ||||
| 
 | ||||
| :: | ||||
| 
 | ||||
|     from PIL import Image | ||||
| 
 | ||||
|     # List of image filenames | ||||
|     image_filenames = [ | ||||
|         "hopper.jpg", | ||||
|         "rotated_hopper_270.jpg", | ||||
|         "rotated_hopper_180.jpg", | ||||
|         "rotated_hopper_90.jpg", | ||||
|     ] | ||||
| 
 | ||||
|     # Open images and create a list | ||||
|     images = [Image.open(filename) for filename in image_filenames] | ||||
| 
 | ||||
|     # Save the images as an animated GIF | ||||
|     images[0].save( | ||||
|         "animated_hopper.gif", | ||||
|         save_all=True, | ||||
|         append_images=images[1:], | ||||
|         duration=500,  # duration of each frame in milliseconds | ||||
|         loop=0,  # loop forever | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| .. image:: animated_hopper.gif | ||||
|     :align: center | ||||
| 
 | ||||
| The following class lets you use the for-statement to loop over the sequence: | ||||
| 
 | ||||
| Using the ImageSequence Iterator class | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| Using the :py:class:`~PIL.ImageSequence.Iterator` class | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| :: | ||||
| 
 | ||||
|  | @ -467,25 +567,61 @@ Drawing PostScript | |||
| 
 | ||||
| :: | ||||
| 
 | ||||
|     from PIL import Image | ||||
|     from PIL import PSDraw | ||||
|     from PIL import Image, PSDraw | ||||
|     import os | ||||
| 
 | ||||
|     with Image.open("hopper.ppm") as im: | ||||
|         title = "hopper" | ||||
|         box = (1 * 72, 2 * 72, 7 * 72, 10 * 72)  # in points | ||||
|     # Define the PostScript file | ||||
|     ps_file = open("hopper.ps", "wb") | ||||
| 
 | ||||
|         ps = PSDraw.PSDraw()  # default is sys.stdout or sys.stdout.buffer | ||||
|         ps.begin_document(title) | ||||
|     # Create a PSDraw object | ||||
|     ps = PSDraw.PSDraw(ps_file) | ||||
| 
 | ||||
|         # draw the image (75 dpi) | ||||
|         ps.image(box, im, 75) | ||||
|         ps.rectangle(box) | ||||
|     # Start the document | ||||
|     ps.begin_document() | ||||
| 
 | ||||
|         # draw title | ||||
|         ps.setfont("HelveticaNarrow-Bold", 36) | ||||
|         ps.text((3 * 72, 4 * 72), title) | ||||
|     # Set the text to be drawn | ||||
|     text = "Hopper" | ||||
| 
 | ||||
|         ps.end_document() | ||||
|     # Define the PostScript font | ||||
|     font_name = "Helvetica-Narrow-Bold" | ||||
|     font_size = 36 | ||||
| 
 | ||||
|     # Calculate text size (approximation as PSDraw doesn't provide direct method) | ||||
|     # Assuming average character width as 0.6 of the font size | ||||
|     text_width = len(text) * font_size * 0.6 | ||||
|     text_height = font_size | ||||
| 
 | ||||
|     # Set the position (top-center) | ||||
|     page_width, page_height = 595, 842  # A4 size in points | ||||
|     text_x = (page_width - text_width) // 2 | ||||
|     text_y = page_height - text_height - 50  # Distance from the top of the page | ||||
| 
 | ||||
|     # Load the image | ||||
|     image_path = "hopper.ppm"  # Update this with your image path | ||||
|     with Image.open(image_path) as im: | ||||
|         # Resize the image if it's too large | ||||
|         im.thumbnail((page_width - 100, page_height // 2)) | ||||
| 
 | ||||
|         # Define the box where the image will be placed | ||||
|         img_x = (page_width - im.width) // 2 | ||||
|         img_y = text_y + text_height - 200  # 200 points below the text | ||||
| 
 | ||||
|         # Draw the image (75 dpi) | ||||
|         ps.image((img_x, img_y, img_x + im.width, img_y + im.height), im, 75) | ||||
| 
 | ||||
|     # Draw the text | ||||
|     ps.setfont(font_name, font_size) | ||||
|     ps.text((text_x, text_y), text) | ||||
| 
 | ||||
|     # End the document | ||||
|     ps.end_document() | ||||
|     ps_file.close() | ||||
| 
 | ||||
| .. image:: hopper_ps.webp | ||||
| 
 | ||||
| .. note:: | ||||
| 
 | ||||
|     PostScript converted to PDF for display purposes | ||||
| 
 | ||||
| More on reading images | ||||
| ---------------------- | ||||
|  | @ -553,7 +689,7 @@ Reading from a tar archive | |||
| 
 | ||||
|     from PIL import Image, TarIO | ||||
| 
 | ||||
|     fp = TarIO.TarIO("Tests/images/hopper.tar", "hopper.jpg") | ||||
|     fp = TarIO.TarIO("hopper.tar", "hopper.jpg") | ||||
|     im = Image.open(fp) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -568,8 +704,7 @@ in the current directory can be saved as JPEGs at reduced quality. | |||
|     import glob | ||||
|     from PIL import Image | ||||
| 
 | ||||
| 
 | ||||
|     def compress_image(source_path, dest_path): | ||||
|     def compress_image(source_path: str, dest_path: str) -> None: | ||||
|         with Image.open(source_path) as img: | ||||
|             if img.mode != "RGB": | ||||
|                 img = img.convert("RGB") | ||||
|  |  | |||
|  | @ -53,7 +53,7 @@ true color. | |||
|     from PIL import Image, ImageFile | ||||
| 
 | ||||
| 
 | ||||
|     def _accept(prefix): | ||||
|     def _accept(prefix: bytes) -> bool: | ||||
|         return prefix[:4] == b"SPAM" | ||||
| 
 | ||||
| 
 | ||||
|  | @ -62,7 +62,7 @@ true color. | |||
|         format = "SPAM" | ||||
|         format_description = "Spam raster image" | ||||
| 
 | ||||
|         def _open(self): | ||||
|         def _open(self) -> None: | ||||
| 
 | ||||
|             header = self.fp.read(128).split() | ||||
| 
 | ||||
|  | @ -82,7 +82,7 @@ true color. | |||
|                 raise SyntaxError(msg) | ||||
| 
 | ||||
|             # data descriptor | ||||
|             self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] | ||||
|             self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] | ||||
| 
 | ||||
| 
 | ||||
|     Image.register_open(SpamImageFile.format, SpamImageFile, _accept) | ||||
|  |  | |||
|  | @ -55,10 +55,6 @@ Many of Pillow's features require external libraries: | |||
| 
 | ||||
| * **libwebp** provides the WebP format. | ||||
| 
 | ||||
|   * Pillow has been tested with version **0.1.3**, which does not read | ||||
|     transparent WebP files. Versions **0.3.0** and above support | ||||
|     transparency. | ||||
| 
 | ||||
| * **openjpeg** provides JPEG 2000 functionality. | ||||
| 
 | ||||
|   * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, | ||||
|  | @ -275,18 +271,18 @@ Build Options | |||
| 
 | ||||
| * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, | ||||
|   ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, | ||||
|   ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, | ||||
|   ``-C lcms=disable``, ``-C webp=disable``, | ||||
|   ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. | ||||
|   Disable building the corresponding feature even if the development | ||||
|   libraries are present on the building machine. | ||||
| 
 | ||||
| * Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, | ||||
|   ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, | ||||
|   ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, | ||||
|   ``-C lcms=enable``, ``-C webp=enable``, | ||||
|   ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. | ||||
|   Require that the corresponding feature is built. The build will raise | ||||
|   an exception if the libraries are not found. Webpmux (WebP metadata) | ||||
|   relies on WebP support. Tcl and Tk also must be used together. | ||||
|   an exception if the libraries are not found. Tcl and Tk must be used | ||||
|   together. | ||||
| 
 | ||||
| * Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. | ||||
|   These flags are used to compile a modified version of libraqm and | ||||
|  |  | |||
|  | @ -362,6 +362,7 @@ Classes | |||
|     :undoc-members: | ||||
|     :show-inheritance: | ||||
| .. autoclass:: PIL.Image.ImagePointHandler | ||||
| .. autoclass:: PIL.Image.ImagePointTransform | ||||
| .. autoclass:: PIL.Image.ImageTransformHandler | ||||
| 
 | ||||
| Protocols | ||||
|  |  | |||
|  | @ -37,6 +37,11 @@ Example: Parse an image | |||
| Classes | ||||
| ------- | ||||
| 
 | ||||
| .. autoclass:: PIL.ImageFile._Tile() | ||||
|     :member-order: bysource | ||||
|     :members: | ||||
|     :show-inheritance: | ||||
| 
 | ||||
| .. autoclass:: PIL.ImageFile.Parser() | ||||
|     :members: | ||||
| 
 | ||||
|  |  | |||
|  | @ -91,3 +91,11 @@ Constants | |||
|     Set to 1,000,000, to protect against potential DOS attacks. Pillow will | ||||
|     raise a :py:exc:`ValueError` if the number of characters is over this limit. The | ||||
|     check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. | ||||
| 
 | ||||
| Dictionaries | ||||
| ------------ | ||||
| 
 | ||||
| .. autoclass:: Axis | ||||
|     :members: | ||||
|     :undoc-members: | ||||
|     :show-inheritance: | ||||
|  |  | |||
|  | @ -54,12 +54,12 @@ Feature version numbers are available only where stated. | |||
| Support for the following features can be checked: | ||||
| 
 | ||||
| * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. | ||||
| * ``transp_webp``: Support for transparency in WebP images. | ||||
| * ``webp_mux``: (compile time) Support for EXIF data in WebP images. | ||||
| * ``webp_anim``: (compile time) Support for animated WebP images. | ||||
| * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. | ||||
| * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. | ||||
| * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. | ||||
| * ``transp_webp``: Deprecated. Always ``True`` if WebP module is installed. | ||||
| * ``webp_mux``: Deprecated. Always ``True`` if WebP module is installed. | ||||
| * ``webp_anim``: Deprecated. Always ``True`` if WebP module is installed. | ||||
| 
 | ||||
| .. autofunction:: PIL.features.check_feature | ||||
| .. autofunction:: PIL.features.version_feature | ||||
|  |  | |||
|  | @ -78,3 +78,7 @@ on some Python versions. | |||
| 
 | ||||
| An internal interface module previously known as :mod:`~PIL._imaging`, | ||||
| implemented in :file:`_imaging.c`. | ||||
| 
 | ||||
| .. py:class:: ImagingCore | ||||
| 
 | ||||
|     A representation of the image data. | ||||
|  |  | |||
|  | @ -185,6 +185,14 @@ Plugin reference | |||
|     :undoc-members: | ||||
|     :show-inheritance: | ||||
| 
 | ||||
| :mod:`~PIL.MpoImagePlugin` Module | ||||
| ---------------------------------- | ||||
| 
 | ||||
| .. automodule:: PIL.MpoImagePlugin | ||||
|     :members: | ||||
|     :undoc-members: | ||||
|     :show-inheritance: | ||||
| 
 | ||||
| :mod:`~PIL.MspImagePlugin` Module | ||||
| --------------------------------- | ||||
| 
 | ||||
|  |  | |||
|  | @ -50,6 +50,22 @@ The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and | |||
| :py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more | ||||
| keyword arguments can be used instead. | ||||
| 
 | ||||
| JpegImageFile.huffman_ac and JpegImageFile.huffman_dc | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| .. deprecated:: 11.0.0 | ||||
| 
 | ||||
| The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They | ||||
| have been deprecated, and will be removed in Pillow 12 (2025-10-15). | ||||
| 
 | ||||
| Specific WebP Feature Checks | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| ``features.check("transp_webp")``, ``features.check("webp_mux")`` and | ||||
| ``features.check("webp_anim")`` are now deprecated. They will always return | ||||
| ``True`` if the WebP module is installed, until they are removed in Pillow | ||||
| 12.0.0 (2025-10-15). | ||||
| 
 | ||||
| API Changes | ||||
| =========== | ||||
| 
 | ||||
|  | @ -77,3 +93,10 @@ others prepare for 3.13, and to ensure Pillow could be used immediately at the r | |||
| of 3.13.0 final (2024-10-01, :pep:`719`). | ||||
| 
 | ||||
| Pillow 11.0.0 now officially supports Python 3.13. | ||||
| 
 | ||||
| C-level Flags | ||||
| ^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Some compiling flags like ``WITH_THREADING``, ``WITH_IMAGECHOPS``, and other | ||||
| ``WITH_*`` were removed. These flags were not available through the build system, | ||||
| but they could be edited in the C source. | ||||
|  |  | |||
|  | @ -109,6 +109,7 @@ lint.select = [ | |||
|   "ISC",    # flake8-implicit-str-concat | ||||
|   "LOG",    # flake8-logging | ||||
|   "PGH",    # pygrep-hooks | ||||
|   "PT",     # flake8-pytest-style | ||||
|   "PYI",    # flake8-pyi | ||||
|   "RUF100", # unused noqa (yesqa) | ||||
|   "UP",     # pyupgrade | ||||
|  | @ -120,6 +121,12 @@ lint.ignore = [ | |||
|   "E221",   # Multiple spaces before operator | ||||
|   "E226",   # Missing whitespace around arithmetic operator | ||||
|   "E241",   # Multiple spaces after ',' | ||||
|   "PT001",  # pytest-fixture-incorrect-parentheses-style | ||||
|   "PT007",  # pytest-parametrize-values-wrong-type | ||||
|   "PT011",  # pytest-raises-too-broad | ||||
|   "PT012",  # pytest-raises-with-multiple-statements | ||||
|   "PT016",  # pytest-fail-without-message | ||||
|   "PT017",  # pytest-assert-in-except | ||||
|   "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 | ||||
|   "PYI034", # flake8-pyi: typing.Self added in Python 3.11 | ||||
| ] | ||||
|  | @ -129,6 +136,7 @@ lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [ | |||
| lint.per-file-ignores."Tests/oss-fuzz/fuzz_pillow.py" = [ | ||||
|   "I002", | ||||
| ] | ||||
| lint.flake8-pytest-style.parametrize-names-type = "csv" | ||||
| lint.isort.known-first-party = [ | ||||
|   "PIL", | ||||
| ] | ||||
|  | @ -158,7 +166,4 @@ 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$', | ||||
| ] | ||||
|  |  | |||
|  | @ -52,7 +52,7 @@ def testimage() -> None: | |||
|     or you call the "load" method: | ||||
| 
 | ||||
|     >>> im = Image.open("Tests/images/hopper.ppm") | ||||
|     >>> print(im.im) # internal image attribute | ||||
|     >>> print(im._im) # internal image attribute | ||||
|     None | ||||
|     >>> a = im.load() | ||||
|     >>> type(im.im) # doctest: +ELLIPSIS | ||||
|  |  | |||
							
								
								
									
										45
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						|  | @ -295,7 +295,6 @@ class pil_build_ext(build_ext): | |||
|             "raqm", | ||||
|             "lcms", | ||||
|             "webp", | ||||
|             "webpmux", | ||||
|             "jpeg2000", | ||||
|             "imagequant", | ||||
|             "xcb", | ||||
|  | @ -794,28 +793,18 @@ class pil_build_ext(build_ext): | |||
| 
 | ||||
|         if feature.want("webp"): | ||||
|             _dbg("Looking for webp") | ||||
|             if _find_include_file(self, "webp/encode.h") and _find_include_file( | ||||
|                 self, "webp/decode.h" | ||||
|             if all( | ||||
|                 _find_include_file(self, "webp/" + include) | ||||
|                 for include in ("encode.h", "decode.h", "mux.h", "demux.h") | ||||
|             ): | ||||
|                 # In Google's precompiled zip it is call "libwebp": | ||||
|                 if _find_library_file(self, "webp"): | ||||
|                     feature.webp = "webp" | ||||
|                 elif _find_library_file(self, "libwebp"): | ||||
|                     feature.webp = "libwebp" | ||||
| 
 | ||||
|         if feature.want("webpmux"): | ||||
|             _dbg("Looking for webpmux") | ||||
|             if _find_include_file(self, "webp/mux.h") and _find_include_file( | ||||
|                 self, "webp/demux.h" | ||||
|             ): | ||||
|                 if _find_library_file(self, "webpmux") and _find_library_file( | ||||
|                     self, "webpdemux" | ||||
|                 ): | ||||
|                     feature.webpmux = "webpmux" | ||||
|                 if _find_library_file(self, "libwebpmux") and _find_library_file( | ||||
|                     self, "libwebpdemux" | ||||
|                 ): | ||||
|                     feature.webpmux = "libwebpmux" | ||||
|                 # In Google's precompiled zip it is called "libwebp" | ||||
|                 for prefix in ("", "lib"): | ||||
|                     if all( | ||||
|                         _find_library_file(self, prefix + library) | ||||
|                         for library in ("webp", "webpmux", "webpdemux") | ||||
|                     ): | ||||
|                         feature.webp = prefix + "webp" | ||||
|                         break | ||||
| 
 | ||||
|         if feature.want("xcb"): | ||||
|             _dbg("Looking for xcb") | ||||
|  | @ -904,15 +893,8 @@ class pil_build_ext(build_ext): | |||
|             self._remove_extension("PIL._imagingcms") | ||||
| 
 | ||||
|         if feature.webp: | ||||
|             libs = [feature.webp] | ||||
|             defs = [] | ||||
| 
 | ||||
|             if feature.webpmux: | ||||
|                 defs.append(("HAVE_WEBPMUX", None)) | ||||
|                 libs.append(feature.webpmux) | ||||
|                 libs.append(feature.webpmux.replace("pmux", "pdemux")) | ||||
| 
 | ||||
|             self._update_extension("PIL._webp", libs, defs) | ||||
|             libs = [feature.webp, feature.webp + "mux", feature.webp + "demux"] | ||||
|             self._update_extension("PIL._webp", libs) | ||||
|         else: | ||||
|             self._remove_extension("PIL._webp") | ||||
| 
 | ||||
|  | @ -953,7 +935,6 @@ class pil_build_ext(build_ext): | |||
|             (feature.raqm, "RAQM (Text shaping)", raqm_extra_info), | ||||
|             (feature.lcms, "LITTLECMS2"), | ||||
|             (feature.webp, "WEBP"), | ||||
|             (feature.webpmux, "WEBPMUX"), | ||||
|             (feature.xcb, "XCB (X protocol)"), | ||||
|         ] | ||||
| 
 | ||||
|  |  | |||
|  | @ -467,6 +467,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | |||
|     magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2" | ||||
|     fp.write(magic) | ||||
| 
 | ||||
|     assert im.palette is not None | ||||
|     fp.write(struct.pack("<i", 1))  # Uncompressed or DirectX compression | ||||
|     fp.write(struct.pack("<b", Encoding.UNCOMPRESSED)) | ||||
|     fp.write(struct.pack("<b", 1 if im.palette.mode == "RGBA" else 0)) | ||||
|  | @ -477,7 +478,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | |||
|         fp.write(struct.pack("<i", 5)) | ||||
|         fp.write(struct.pack("<i", 0)) | ||||
| 
 | ||||
|     ImageFile._save(im, fp, [("BLP", (0, 0) + im.size, 0, im.mode)]) | ||||
|     ImageFile._save(im, fp, [ImageFile._Tile("BLP", (0, 0) + im.size, 0, im.mode)]) | ||||
| 
 | ||||
| 
 | ||||
| Image.register_open(BlpImageFile.format, BlpImageFile, _accept) | ||||
|  |  | |||
|  | @ -170,6 +170,8 @@ class BmpImageFile(ImageFile.ImageFile): | |||
| 
 | ||||
|         # ------------------ Special case : header is reported 40, which | ||||
|         # ---------------------- is shorter than real size for bpp >= 16 | ||||
|         assert isinstance(file_info["width"], int) | ||||
|         assert isinstance(file_info["height"], int) | ||||
|         self._size = file_info["width"], file_info["height"] | ||||
| 
 | ||||
|         # ------- If color count was not found in the header, compute from bits | ||||
|  | @ -482,7 +484,9 @@ def _save( | |||
|     if palette: | ||||
|         fp.write(palette) | ||||
| 
 | ||||
|     ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]) | ||||
|     ImageFile._save( | ||||
|         im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))] | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| # | ||||
|  |  | |||
|  | @ -65,16 +65,24 @@ def has_ghostscript() -> bool: | |||
|     return gs_binary is not False | ||||
| 
 | ||||
| 
 | ||||
| def Ghostscript(tile, size, fp, scale=1, transparency: bool = False) -> Image.Image: | ||||
| def Ghostscript( | ||||
|     tile: list[ImageFile._Tile], | ||||
|     size: tuple[int, int], | ||||
|     fp: IO[bytes], | ||||
|     scale: int = 1, | ||||
|     transparency: bool = False, | ||||
| ) -> Image.core.ImagingCore: | ||||
|     """Render an image using Ghostscript""" | ||||
|     global gs_binary | ||||
|     if not has_ghostscript(): | ||||
|         msg = "Unable to locate Ghostscript on paths" | ||||
|         raise OSError(msg) | ||||
|     assert isinstance(gs_binary, str) | ||||
| 
 | ||||
|     # Unpack decoder tile | ||||
|     decoder, tile, offset, data = tile[0] | ||||
|     length, bbox = data | ||||
|     args = tile[0].args | ||||
|     assert isinstance(args, tuple) | ||||
|     length, bbox = args | ||||
| 
 | ||||
|     # Hack to support hi-res rendering | ||||
|     scale = int(scale) or 1 | ||||
|  | @ -182,7 +190,6 @@ class EpsImageFile(ImageFile.ImageFile): | |||
|         self.fp.seek(offset) | ||||
| 
 | ||||
|         self._mode = "RGB" | ||||
|         self._size = None | ||||
| 
 | ||||
|         byte_arr = bytearray(255) | ||||
|         bytes_mv = memoryview(byte_arr) | ||||
|  | @ -220,14 +227,18 @@ class EpsImageFile(ImageFile.ImageFile): | |||
|             if k == "BoundingBox": | ||||
|                 if v == "(atend)": | ||||
|                     reading_trailer_comments = True | ||||
|                 elif not self._size or (trailer_reached and reading_trailer_comments): | ||||
|                 elif not self.tile or (trailer_reached and reading_trailer_comments): | ||||
|                     try: | ||||
|                         # Note: The DSC spec says that BoundingBox | ||||
|                         # fields should be integers, but some drivers | ||||
|                         # put floating point values there anyway. | ||||
|                         box = [int(float(i)) for i in v.split()] | ||||
|                         self._size = box[2] - box[0], box[3] - box[1] | ||||
|                         self.tile = [("eps", (0, 0) + self.size, offset, (length, box))] | ||||
|                         self.tile = [ | ||||
|                             ImageFile._Tile( | ||||
|                                 "eps", (0, 0) + self.size, offset, (length, box) | ||||
|                             ) | ||||
|                         ] | ||||
|                     except Exception: | ||||
|                         pass | ||||
|             return True | ||||
|  | @ -334,7 +345,7 @@ class EpsImageFile(ImageFile.ImageFile): | |||
|                 trailer_reached = True | ||||
|             bytes_read = 0 | ||||
| 
 | ||||
|         if not self._size: | ||||
|         if not self.tile: | ||||
|             msg = "cannot determine EPS bounding box" | ||||
|             raise OSError(msg) | ||||
| 
 | ||||
|  | @ -422,7 +433,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) - | |||
|     if hasattr(fp, "flush"): | ||||
|         fp.flush() | ||||
| 
 | ||||
|     ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)]) | ||||
|     ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0, None)]) | ||||
| 
 | ||||
|     fp.write(b"\n%%%%EndBinary\n") | ||||
|     fp.write(b"grestore end\n") | ||||
|  |  | |||
|  | @ -81,6 +81,8 @@ class FpxImageFile(ImageFile.ImageFile): | |||
| 
 | ||||
|         # size (highest resolution) | ||||
| 
 | ||||
|         assert isinstance(prop[0x1000002], int) | ||||
|         assert isinstance(prop[0x1000003], int) | ||||
|         self._size = prop[0x1000002], prop[0x1000003] | ||||
| 
 | ||||
|         size = max(self.size) | ||||
|  |  | |||
|  | @ -89,7 +89,7 @@ class GbrImageFile(ImageFile.ImageFile): | |||
|         self._data_size = width * height * color_depth | ||||
| 
 | ||||
|     def load(self) -> Image.core.PixelAccess | None: | ||||
|         if not self.im: | ||||
|         if self._im is None: | ||||
|             self.im = Image.core.new(self.mode, self.size) | ||||
|             self.frombytes(self.fp.read(self._data_size)) | ||||
|         return Image.Image.load(self) | ||||
|  |  | |||
|  | @ -155,7 +155,7 @@ class GifImageFile(ImageFile.ImageFile): | |||
|         if not self._seek_check(frame): | ||||
|             return | ||||
|         if frame < self.__frame: | ||||
|             self.im = None | ||||
|             self._im = None | ||||
|             self._seek(0) | ||||
| 
 | ||||
|         last_frame = self.__frame | ||||
|  | @ -320,11 +320,14 @@ class GifImageFile(ImageFile.ImageFile): | |||
|             else: | ||||
|                 self._mode = "L" | ||||
| 
 | ||||
|             if not palette and self.global_palette: | ||||
|             if palette: | ||||
|                 self.palette = palette | ||||
|             elif self.global_palette: | ||||
|                 from copy import copy | ||||
| 
 | ||||
|                 palette = copy(self.global_palette) | ||||
|             self.palette = palette | ||||
|                 self.palette = copy(self.global_palette) | ||||
|             else: | ||||
|                 self.palette = None | ||||
|         else: | ||||
|             if self.mode == "P": | ||||
|                 if ( | ||||
|  | @ -376,7 +379,7 @@ class GifImageFile(ImageFile.ImageFile): | |||
|                     self.dispose = Image.core.fill(dispose_mode, dispose_size, color) | ||||
|                 else: | ||||
|                     # replace with previous contents | ||||
|                     if self.im is not None: | ||||
|                     if self._im is not None: | ||||
|                         # only dispose the extent in this frame | ||||
|                         self.dispose = self._crop(self.im, self.dispose_extent) | ||||
|                     elif frame_transparency is not None: | ||||
|  | @ -434,7 +437,7 @@ class GifImageFile(ImageFile.ImageFile): | |||
|                 self.im = Image.core.fill("P", self.size, self._frame_transparency or 0) | ||||
|                 self.im.putpalette("RGB", *self._frame_palette.getdata()) | ||||
|             else: | ||||
|                 self.im = None | ||||
|                 self._im = None | ||||
|         self._mode = temp_mode | ||||
|         self._frame_palette = None | ||||
| 
 | ||||
|  | @ -495,6 +498,7 @@ def _normalize_mode(im: Image.Image) -> Image.Image: | |||
|         return im | ||||
|     if Image.getmodebase(im.mode) == "RGB": | ||||
|         im = im.convert("P", palette=Image.Palette.ADAPTIVE) | ||||
|         assert im.palette is not None | ||||
|         if im.palette.mode == "RGBA": | ||||
|             for rgba in im.palette.colors: | ||||
|                 if rgba[3] == 0: | ||||
|  | @ -536,11 +540,11 @@ def _normalize_palette( | |||
|         if not source_palette: | ||||
|             source_palette = bytearray(i // 3 for i in range(768)) | ||||
|         im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) | ||||
|     assert source_palette is not None | ||||
| 
 | ||||
|     used_palette_colors: list[int] | None | ||||
|     if palette: | ||||
|         used_palette_colors = [] | ||||
|         assert source_palette is not None | ||||
|         used_palette_colors: list[int | None] = [] | ||||
|         assert im.palette is not None | ||||
|         for i in range(0, len(source_palette), 3): | ||||
|             source_color = tuple(source_palette[i : i + 3]) | ||||
|             index = im.palette.colors.get(source_color) | ||||
|  | @ -553,20 +557,25 @@ def _normalize_palette( | |||
|                     if j not in used_palette_colors: | ||||
|                         used_palette_colors[i] = j | ||||
|                         break | ||||
|         im = im.remap_palette(used_palette_colors) | ||||
|         dest_map: list[int] = [] | ||||
|         for index in used_palette_colors: | ||||
|             assert index is not None | ||||
|             dest_map.append(index) | ||||
|         im = im.remap_palette(dest_map) | ||||
|     else: | ||||
|         used_palette_colors = _get_optimize(im, info) | ||||
|         if used_palette_colors is not None: | ||||
|             im = im.remap_palette(used_palette_colors, source_palette) | ||||
|         optimized_palette_colors = _get_optimize(im, info) | ||||
|         if optimized_palette_colors is not None: | ||||
|             im = im.remap_palette(optimized_palette_colors, source_palette) | ||||
|             if "transparency" in info: | ||||
|                 try: | ||||
|                     info["transparency"] = used_palette_colors.index( | ||||
|                     info["transparency"] = optimized_palette_colors.index( | ||||
|                         info["transparency"] | ||||
|                     ) | ||||
|                 except ValueError: | ||||
|                     del info["transparency"] | ||||
|             return im | ||||
| 
 | ||||
|     assert im.palette is not None | ||||
|     im.palette.palette = source_palette | ||||
|     return im | ||||
| 
 | ||||
|  | @ -578,7 +587,8 @@ def _write_single_frame( | |||
| ) -> None: | ||||
|     im_out = _normalize_mode(im) | ||||
|     for k, v in im_out.info.items(): | ||||
|         im.encoderinfo.setdefault(k, v) | ||||
|         if isinstance(k, str): | ||||
|             im.encoderinfo.setdefault(k, v) | ||||
|     im_out = _normalize_palette(im_out, palette, im.encoderinfo) | ||||
| 
 | ||||
|     for s in _get_global_header(im_out, im.encoderinfo): | ||||
|  | @ -591,7 +601,9 @@ def _write_single_frame( | |||
|     _write_local_header(fp, im, (0, 0), flags) | ||||
| 
 | ||||
|     im_out.encoderconfig = (8, get_interlace(im)) | ||||
|     ImageFile._save(im_out, fp, [("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]) | ||||
|     ImageFile._save( | ||||
|         im_out, fp, [ImageFile._Tile("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])] | ||||
|     ) | ||||
| 
 | ||||
|     fp.write(b"\0")  # end of image data | ||||
| 
 | ||||
|  | @ -630,7 +642,8 @@ def _write_multiple_frames( | |||
|                 for k, v in im_frame.info.items(): | ||||
|                     if k == "transparency": | ||||
|                         continue | ||||
|                     im.encoderinfo.setdefault(k, v) | ||||
|                     if isinstance(k, str): | ||||
|                         im.encoderinfo.setdefault(k, v) | ||||
| 
 | ||||
|             encoderinfo = im.encoderinfo.copy() | ||||
|             if "transparency" in im_frame.info: | ||||
|  | @ -660,10 +673,12 @@ def _write_multiple_frames( | |||
|                         ) | ||||
|                         background = _get_background(im_frame, color) | ||||
|                         background_im = Image.new("P", im_frame.size, background) | ||||
|                         assert im_frames[0].im.palette is not None | ||||
|                         background_im.putpalette(im_frames[0].im.palette) | ||||
|                     bbox = _getbbox(background_im, im_frame)[1] | ||||
|                 elif encoderinfo.get("optimize") and im_frame.mode != "1": | ||||
|                     if "transparency" not in encoderinfo: | ||||
|                         assert im_frame.palette is not None | ||||
|                         try: | ||||
|                             encoderinfo["transparency"] = ( | ||||
|                                 im_frame.palette._new_color_index(im_frame) | ||||
|  | @ -901,6 +916,7 @@ def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None: | |||
|             if optimise or max(used_palette_colors) >= len(used_palette_colors): | ||||
|                 return used_palette_colors | ||||
| 
 | ||||
|             assert im.palette is not None | ||||
|             num_palette_colors = len(im.palette.palette) // Image.getmodebands( | ||||
|                 im.palette.mode | ||||
|             ) | ||||
|  | @ -950,7 +966,7 @@ def _get_palette_bytes(im: Image.Image) -> bytes: | |||
|     :param im: Image object | ||||
|     :returns: Bytes, len<=768 suitable for inclusion in gif header | ||||
|     """ | ||||
|     return im.palette.palette if im.palette else b"" | ||||
|     return bytes(im.palette.palette) if im.palette else b"" | ||||
| 
 | ||||
| 
 | ||||
| def _get_background( | ||||
|  | @ -963,6 +979,7 @@ def _get_background( | |||
|             # WebPImagePlugin stores an RGBA value in info["background"] | ||||
|             # So it must be converted to the same format as GifImagePlugin's | ||||
|             # info["background"] - a global color table index | ||||
|             assert im.palette is not None | ||||
|             try: | ||||
|                 background = im.palette.getcolor(info_background, im) | ||||
|             except ValueError as e: | ||||
|  | @ -1054,7 +1071,9 @@ def _write_frame_data( | |||
|         _write_local_header(fp, im_frame, offset, 0) | ||||
| 
 | ||||
|         ImageFile._save( | ||||
|             im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])] | ||||
|             im_frame, | ||||
|             fp, | ||||
|             [ImageFile._Tile("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])], | ||||
|         ) | ||||
| 
 | ||||
|         fp.write(b"\0")  # end of image data | ||||
|  |  | |||
|  | @ -308,7 +308,7 @@ class IcnsImageFile(ImageFile.ImageFile): | |||
|             ) | ||||
| 
 | ||||
|         px = Image.Image.load(self) | ||||
|         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 | ||||
|             return px | ||||
|         self.load_prepare() | ||||
|  |  | |||
|  | @ -97,7 +97,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | |||
|             if bits != 32: | ||||
|                 and_mask = Image.new("1", size) | ||||
|                 ImageFile._save( | ||||
|                     and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))] | ||||
|                     and_mask, | ||||
|                     image_io, | ||||
|                     [ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))], | ||||
|                 ) | ||||
|         else: | ||||
|             frame.save(image_io, "png") | ||||
|  | @ -317,18 +319,18 @@ class IcoImageFile(ImageFile.ImageFile): | |||
|         self.load() | ||||
| 
 | ||||
|     @property | ||||
|     def size(self): | ||||
|     def size(self) -> tuple[int, int]: | ||||
|         return self._size | ||||
| 
 | ||||
|     @size.setter | ||||
|     def size(self, value): | ||||
|     def size(self, value: tuple[int, int]) -> None: | ||||
|         if value not in self.info["sizes"]: | ||||
|             msg = "This is not one of the allowed sizes of this image" | ||||
|             raise ValueError(msg) | ||||
|         self._size = value | ||||
| 
 | ||||
|     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 | ||||
|             return Image.Image.load(self) | ||||
|         im = self.ico.getimage(self.size) | ||||
|  |  | |||
|  | @ -360,7 +360,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | |||
|             palette += im_palette[colors * i : colors * (i + 1)] | ||||
|             palette += b"\x00" * (256 - colors) | ||||
|         fp.write(palette)  # 768 bytes | ||||
|     ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]) | ||||
|     ImageFile._save( | ||||
|         im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))] | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| # | ||||
|  |  | |||
							
								
								
									
										327
									
								
								src/PIL/Image.py
									
									
									
									
									
								
							
							
						
						|  | @ -38,7 +38,7 @@ import struct | |||
| import sys | ||||
| import tempfile | ||||
| import warnings | ||||
| from collections.abc import Callable, MutableMapping, Sequence | ||||
| from collections.abc import Callable, Iterator, MutableMapping, Sequence | ||||
| from enum import IntEnum | ||||
| from types import ModuleType | ||||
| from typing import ( | ||||
|  | @ -218,7 +218,12 @@ if hasattr(core, "DEFAULT_STRATEGY"): | |||
| # Registries | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     from . import ImageFile, ImagePalette | ||||
|     import mmap | ||||
|     from xml.etree.ElementTree import Element | ||||
| 
 | ||||
|     from IPython.lib.pretty import PrettyPrinter | ||||
| 
 | ||||
|     from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin | ||||
|     from ._typing import NumpyArray, StrOrBytesPath, TypeGuard | ||||
| ID: list[str] = [] | ||||
| OPEN: dict[ | ||||
|  | @ -241,9 +246,9 @@ ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {} | |||
| _ENDIAN = "<" if sys.byteorder == "little" else ">" | ||||
| 
 | ||||
| 
 | ||||
| def _conv_type_shape(im): | ||||
| def _conv_type_shape(im: Image) -> tuple[tuple[int, ...], str]: | ||||
|     m = ImageMode.getmode(im.mode) | ||||
|     shape = (im.height, im.width) | ||||
|     shape: tuple[int, ...] = (im.height, im.width) | ||||
|     extra = len(m.bands) | ||||
|     if extra != 1: | ||||
|         shape += (extra,) | ||||
|  | @ -465,43 +470,53 @@ def _getencoder( | |||
| # Simple expression analyzer | ||||
| 
 | ||||
| 
 | ||||
| class _E: | ||||
|     def __init__(self, scale, offset) -> None: | ||||
| class ImagePointTransform: | ||||
|     """ | ||||
|     Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than | ||||
|     8 bits, this represents an affine transformation, where the value is multiplied by | ||||
|     ``scale`` and ``offset`` is added. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, scale: float, offset: float) -> None: | ||||
|         self.scale = scale | ||||
|         self.offset = offset | ||||
| 
 | ||||
|     def __neg__(self): | ||||
|         return _E(-self.scale, -self.offset) | ||||
|     def __neg__(self) -> ImagePointTransform: | ||||
|         return ImagePointTransform(-self.scale, -self.offset) | ||||
| 
 | ||||
|     def __add__(self, other): | ||||
|         if isinstance(other, _E): | ||||
|             return _E(self.scale + other.scale, self.offset + other.offset) | ||||
|         return _E(self.scale, self.offset + other) | ||||
|     def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform: | ||||
|         if isinstance(other, ImagePointTransform): | ||||
|             return ImagePointTransform( | ||||
|                 self.scale + other.scale, self.offset + other.offset | ||||
|             ) | ||||
|         return ImagePointTransform(self.scale, self.offset + other) | ||||
| 
 | ||||
|     __radd__ = __add__ | ||||
| 
 | ||||
|     def __sub__(self, other): | ||||
|     def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform: | ||||
|         return self + -other | ||||
| 
 | ||||
|     def __rsub__(self, other): | ||||
|     def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform: | ||||
|         return other + -self | ||||
| 
 | ||||
|     def __mul__(self, other): | ||||
|         if isinstance(other, _E): | ||||
|     def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform: | ||||
|         if isinstance(other, ImagePointTransform): | ||||
|             return NotImplemented | ||||
|         return _E(self.scale * other, self.offset * other) | ||||
|         return ImagePointTransform(self.scale * other, self.offset * other) | ||||
| 
 | ||||
|     __rmul__ = __mul__ | ||||
| 
 | ||||
|     def __truediv__(self, other): | ||||
|         if isinstance(other, _E): | ||||
|     def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform: | ||||
|         if isinstance(other, ImagePointTransform): | ||||
|             return NotImplemented | ||||
|         return _E(self.scale / other, self.offset / other) | ||||
|         return ImagePointTransform(self.scale / other, self.offset / other) | ||||
| 
 | ||||
| 
 | ||||
| def _getscaleoffset(expr): | ||||
|     a = expr(_E(1, 0)) | ||||
|     return (a.scale, a.offset) if isinstance(a, _E) else (0, a) | ||||
| def _getscaleoffset( | ||||
|     expr: Callable[[ImagePointTransform], ImagePointTransform | float] | ||||
| ) -> tuple[float, float]: | ||||
|     a = expr(ImagePointTransform(1, 0)) | ||||
|     return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a) | ||||
| 
 | ||||
| 
 | ||||
| # -------------------------------------------------------------------- | ||||
|  | @ -530,16 +545,27 @@ class Image: | |||
|     format_description: str | None = None | ||||
|     _close_exclusive_fp_after_loading = True | ||||
| 
 | ||||
|     def __init__(self): | ||||
|     def __init__(self) -> None: | ||||
|         # FIXME: take "new" parameters / other image? | ||||
|         # FIXME: turn mode and size into delegating properties? | ||||
|         self.im = None | ||||
|         self._im: core.ImagingCore | DeferredError | None = None | ||||
|         self._mode = "" | ||||
|         self._size = (0, 0) | ||||
|         self.palette = None | ||||
|         self.info = {} | ||||
|         self.palette: ImagePalette.ImagePalette | None = None | ||||
|         self.info: dict[str | tuple[int, int], Any] = {} | ||||
|         self.readonly = 0 | ||||
|         self._exif = None | ||||
|         self._exif: Exif | None = None | ||||
| 
 | ||||
|     @property | ||||
|     def im(self) -> core.ImagingCore: | ||||
|         if isinstance(self._im, DeferredError): | ||||
|             raise self._im.ex | ||||
|         assert self._im is not None | ||||
|         return self._im | ||||
| 
 | ||||
|     @im.setter | ||||
|     def im(self, im: core.ImagingCore) -> None: | ||||
|         self._im = im | ||||
| 
 | ||||
|     @property | ||||
|     def width(self) -> int: | ||||
|  | @ -610,12 +636,12 @@ class Image: | |||
|                 logger.debug("Error closing: %s", msg) | ||||
| 
 | ||||
|         if getattr(self, "map", None): | ||||
|             self.map = None | ||||
|             self.map: mmap.mmap | None = None | ||||
| 
 | ||||
|         # Instead of simply setting to None, we're setting up a | ||||
|         # deferred error that will better explain that the core image | ||||
|         # object is gone. | ||||
|         self.im = DeferredError(ValueError("Operation on closed image")) | ||||
|         self._im = DeferredError(ValueError("Operation on closed image")) | ||||
| 
 | ||||
|     def _copy(self) -> None: | ||||
|         self.load() | ||||
|  | @ -674,7 +700,7 @@ class Image: | |||
|             id(self), | ||||
|         ) | ||||
| 
 | ||||
|     def _repr_pretty_(self, p, cycle) -> None: | ||||
|     def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: | ||||
|         """IPython plain text display support""" | ||||
| 
 | ||||
|         # Same as __repr__ but without unpredictable id(self), | ||||
|  | @ -718,35 +744,23 @@ class Image: | |||
|         return self._repr_image("JPEG") | ||||
| 
 | ||||
|     @property | ||||
|     def __array_interface__(self): | ||||
|     def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]: | ||||
|         # numpy array interface support | ||||
|         new = {"version": 3} | ||||
|         try: | ||||
|             if self.mode == "1": | ||||
|                 # Binary images need to be extended from bits to bytes | ||||
|                 # See: https://github.com/python-pillow/Pillow/issues/350 | ||||
|                 new["data"] = self.tobytes("raw", "L") | ||||
|             else: | ||||
|                 new["data"] = self.tobytes() | ||||
|         except Exception as e: | ||||
|             if not isinstance(e, (MemoryError, RecursionError)): | ||||
|                 try: | ||||
|                     import numpy | ||||
|                     from packaging.version import parse as parse_version | ||||
|                 except ImportError: | ||||
|                     pass | ||||
|                 else: | ||||
|                     if parse_version(numpy.__version__) < parse_version("1.23"): | ||||
|                         warnings.warn(str(e)) | ||||
|             raise | ||||
|         new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3} | ||||
|         if self.mode == "1": | ||||
|             # Binary images need to be extended from bits to bytes | ||||
|             # See: https://github.com/python-pillow/Pillow/issues/350 | ||||
|             new["data"] = self.tobytes("raw", "L") | ||||
|         else: | ||||
|             new["data"] = self.tobytes() | ||||
|         new["shape"], new["typestr"] = _conv_type_shape(self) | ||||
|         return new | ||||
| 
 | ||||
|     def __getstate__(self): | ||||
|     def __getstate__(self) -> list[Any]: | ||||
|         im_data = self.tobytes()  # load image first | ||||
|         return [self.info, self.mode, self.size, self.getpalette(), im_data] | ||||
| 
 | ||||
|     def __setstate__(self, state) -> None: | ||||
|     def __setstate__(self, state: list[Any]) -> None: | ||||
|         Image.__init__(self) | ||||
|         info, mode, size, palette, data = state | ||||
|         self.info = info | ||||
|  | @ -885,7 +899,7 @@ class Image: | |||
|         :returns: An image access object. | ||||
|         :rtype: :py:class:`.PixelAccess` | ||||
|         """ | ||||
|         if self.im is not None and self.palette and self.palette.dirty: | ||||
|         if self._im is not None and self.palette and self.palette.dirty: | ||||
|             # realize palette | ||||
|             mode, arr = self.palette.getdata() | ||||
|             self.im.putpalette(self.palette.mode, mode, arr) | ||||
|  | @ -902,7 +916,7 @@ class Image: | |||
|                     self.palette.mode, self.palette.mode | ||||
|                 ) | ||||
| 
 | ||||
|         if self.im is not None: | ||||
|         if self._im is not None: | ||||
|             return self.im.pixel_access(self.readonly) | ||||
|         return None | ||||
| 
 | ||||
|  | @ -1043,9 +1057,11 @@ class Image: | |||
|                     # use existing conversions | ||||
|                     trns_im = new(self.mode, (1, 1)) | ||||
|                     if self.mode == "P": | ||||
|                         assert self.palette is not None | ||||
|                         trns_im.putpalette(self.palette) | ||||
|                         if isinstance(t, tuple): | ||||
|                             err = "Couldn't allocate a palette color for transparency" | ||||
|                             assert trns_im.palette is not None | ||||
|                             try: | ||||
|                                 t = trns_im.palette.getcolor(t, self) | ||||
|                             except ValueError as e: | ||||
|  | @ -1147,7 +1163,9 @@ class Image: | |||
|         if trns is not None: | ||||
|             if new_im.mode == "P" and new_im.palette: | ||||
|                 try: | ||||
|                     new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) | ||||
|                     new_im.info["transparency"] = new_im.palette.getcolor( | ||||
|                         cast(tuple[int, ...], trns), new_im  # trns was converted to RGB | ||||
|                     ) | ||||
|                 except ValueError as e: | ||||
|                     del new_im.info["transparency"] | ||||
|                     if str(e) != "cannot allocate more than 256 colors": | ||||
|  | @ -1225,6 +1243,7 @@ class Image: | |||
|                 raise ValueError(msg) | ||||
|             im = self.im.convert("P", dither, palette.im) | ||||
|             new_im = self._new(im) | ||||
|             assert palette.palette is not None | ||||
|             new_im.palette = palette.palette.copy() | ||||
|             return new_im | ||||
| 
 | ||||
|  | @ -1334,9 +1353,6 @@ class Image: | |||
|         self.load() | ||||
|         return self._new(self.im.expand(xmargin, ymargin)) | ||||
| 
 | ||||
|     if TYPE_CHECKING: | ||||
|         from . import ImageFilter | ||||
| 
 | ||||
|     def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: | ||||
|         """ | ||||
|         Filters this image using the given filter.  For a list of | ||||
|  | @ -1418,7 +1434,7 @@ class Image: | |||
|             return out | ||||
|         return self.im.getcolors(maxcolors) | ||||
| 
 | ||||
|     def getdata(self, band: int | None = None): | ||||
|     def getdata(self, band: int | None = None) -> core.ImagingCore: | ||||
|         """ | ||||
|         Returns the contents of this image as a sequence object | ||||
|         containing pixel values.  The sequence object is flattened, so | ||||
|  | @ -1467,8 +1483,8 @@ class Image: | |||
|         def get_name(tag: str) -> str: | ||||
|             return re.sub("^{[^}]+}", "", tag) | ||||
| 
 | ||||
|         def get_value(element): | ||||
|             value = {get_name(k): v for k, v in element.attrib.items()} | ||||
|         def get_value(element: Element) -> str | dict[str, Any] | None: | ||||
|             value: dict[str, Any] = {get_name(k): v for k, v in element.attrib.items()} | ||||
|             children = list(element) | ||||
|             if children: | ||||
|                 for child in children: | ||||
|  | @ -1549,6 +1565,7 @@ class Image: | |||
|                     ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) | ||||
|         ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) | ||||
|         if ifd1 and ifd1.get(513): | ||||
|             assert exif._info is not None | ||||
|             ifds.append((ifd1, exif._info.next)) | ||||
| 
 | ||||
|         offset = None | ||||
|  | @ -1558,12 +1575,13 @@ class Image: | |||
|                 offset = current_offset | ||||
| 
 | ||||
|             fp = self.fp | ||||
|             thumbnail_offset = ifd.get(513) | ||||
|             if thumbnail_offset is not None: | ||||
|                 thumbnail_offset += getattr(self, "_exif_offset", 0) | ||||
|                 self.fp.seek(thumbnail_offset) | ||||
|                 data = self.fp.read(ifd.get(514)) | ||||
|                 fp = io.BytesIO(data) | ||||
|             if ifd is not None: | ||||
|                 thumbnail_offset = ifd.get(513) | ||||
|                 if thumbnail_offset is not None: | ||||
|                     thumbnail_offset += getattr(self, "_exif_offset", 0) | ||||
|                     self.fp.seek(thumbnail_offset) | ||||
|                     data = self.fp.read(ifd.get(514)) | ||||
|                     fp = io.BytesIO(data) | ||||
| 
 | ||||
|             with open(fp) as im: | ||||
|                 from . import TiffImagePlugin | ||||
|  | @ -1624,11 +1642,15 @@ class Image: | |||
| 
 | ||||
|         :returns: A boolean. | ||||
|         """ | ||||
|         return ( | ||||
|         if ( | ||||
|             self.mode in ("LA", "La", "PA", "RGBA", "RGBa") | ||||
|             or (self.mode == "P" and self.palette.mode.endswith("A")) | ||||
|             or "transparency" in self.info | ||||
|         ) | ||||
|         ): | ||||
|             return True | ||||
|         if self.mode == "P": | ||||
|             assert self.palette is not None | ||||
|             return self.palette.mode.endswith("A") | ||||
|         return False | ||||
| 
 | ||||
|     def apply_transparency(self) -> None: | ||||
|         """ | ||||
|  | @ -1681,7 +1703,9 @@ class Image: | |||
|         x, y = self.im.getprojection() | ||||
|         return list(x), list(y) | ||||
| 
 | ||||
|     def histogram(self, mask: Image | None = None, extrema=None) -> list[int]: | ||||
|     def histogram( | ||||
|         self, mask: Image | None = None, extrema: tuple[float, float] | None = None | ||||
|     ) -> list[int]: | ||||
|         """ | ||||
|         Returns a histogram for the image. The histogram is returned as a | ||||
|         list of pixel counts, one for each pixel value in the source | ||||
|  | @ -1707,12 +1731,14 @@ class Image: | |||
|             mask.load() | ||||
|             return self.im.histogram((0, 0), mask.im) | ||||
|         if self.mode in ("I", "F"): | ||||
|             if extrema is None: | ||||
|                 extrema = self.getextrema() | ||||
|             return self.im.histogram(extrema) | ||||
|             return self.im.histogram( | ||||
|                 extrema if extrema is not None else self.getextrema() | ||||
|             ) | ||||
|         return self.im.histogram() | ||||
| 
 | ||||
|     def entropy(self, mask=None, extrema=None): | ||||
|     def entropy( | ||||
|         self, mask: Image | None = None, extrema: tuple[float, float] | None = None | ||||
|     ) -> float: | ||||
|         """ | ||||
|         Calculates and returns the entropy for the image. | ||||
| 
 | ||||
|  | @ -1733,9 +1759,9 @@ class Image: | |||
|             mask.load() | ||||
|             return self.im.entropy((0, 0), mask.im) | ||||
|         if self.mode in ("I", "F"): | ||||
|             if extrema is None: | ||||
|                 extrema = self.getextrema() | ||||
|             return self.im.entropy(extrema) | ||||
|             return self.im.entropy( | ||||
|                 extrema if extrema is not None else self.getextrema() | ||||
|             ) | ||||
|         return self.im.entropy() | ||||
| 
 | ||||
|     def paste( | ||||
|  | @ -1807,26 +1833,30 @@ class Image: | |||
|                 raise ValueError(msg) | ||||
|             box += (box[0] + size[0], box[1] + size[1]) | ||||
| 
 | ||||
|         source: core.ImagingCore | str | float | tuple[float, ...] | ||||
|         if isinstance(im, str): | ||||
|             from . import ImageColor | ||||
| 
 | ||||
|             im = ImageColor.getcolor(im, self.mode) | ||||
| 
 | ||||
|             source = ImageColor.getcolor(im, self.mode) | ||||
|         elif isImageType(im): | ||||
|             im.load() | ||||
|             if self.mode != im.mode: | ||||
|                 if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): | ||||
|                     # should use an adapter for this! | ||||
|                     im = im.convert(self.mode) | ||||
|             im = im.im | ||||
|             source = im.im | ||||
|         elif isinstance(im, tuple): | ||||
|             source = im | ||||
|         else: | ||||
|             source = cast(float, im) | ||||
| 
 | ||||
|         self._ensure_mutable() | ||||
| 
 | ||||
|         if mask: | ||||
|             mask.load() | ||||
|             self.im.paste(im, box, mask.im) | ||||
|             self.im.paste(source, box, mask.im) | ||||
|         else: | ||||
|             self.im.paste(im, box) | ||||
|             self.im.paste(source, box) | ||||
| 
 | ||||
|     def alpha_composite( | ||||
|         self, im: Image, dest: Sequence[int] = (0, 0), source: Sequence[int] = (0, 0) | ||||
|  | @ -1886,7 +1916,13 @@ class Image: | |||
| 
 | ||||
|     def point( | ||||
|         self, | ||||
|         lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler, | ||||
|         lut: ( | ||||
|             Sequence[float] | ||||
|             | NumpyArray | ||||
|             | Callable[[int], float] | ||||
|             | Callable[[ImagePointTransform], ImagePointTransform | float] | ||||
|             | ImagePointHandler | ||||
|         ), | ||||
|         mode: str | None = None, | ||||
|     ) -> Image: | ||||
|         """ | ||||
|  | @ -1903,7 +1939,7 @@ class Image: | |||
|            object:: | ||||
| 
 | ||||
|                class Example(Image.ImagePointHandler): | ||||
|                  def point(self, data): | ||||
|                  def point(self, im: Image) -> Image: | ||||
|                    # Return result | ||||
|         :param mode: Output mode (default is same as input). This can only be used if | ||||
|            the source image has mode "L" or "P", and the output has mode "1" or the | ||||
|  | @ -1922,10 +1958,10 @@ class Image: | |||
|                 # check if the function can be used with point_transform | ||||
|                 # UNDONE wiredfool -- I think this prevents us from ever doing | ||||
|                 # a gamma function point transform on > 8bit images. | ||||
|                 scale, offset = _getscaleoffset(lut) | ||||
|                 scale, offset = _getscaleoffset(lut)  # type: ignore[arg-type] | ||||
|                 return self._new(self.im.point_transform(scale, offset)) | ||||
|             # for other modes, convert the function to a table | ||||
|             flatLut = [lut(i) for i in range(256)] * self.im.bands | ||||
|             flatLut = [lut(i) for i in range(256)] * self.im.bands  # type: ignore[arg-type] | ||||
|         else: | ||||
|             flatLut = lut | ||||
| 
 | ||||
|  | @ -1996,7 +2032,7 @@ class Image: | |||
| 
 | ||||
|     def putdata( | ||||
|         self, | ||||
|         data: Sequence[float] | Sequence[Sequence[int]] | NumpyArray, | ||||
|         data: Sequence[float] | Sequence[Sequence[int]] | core.ImagingCore | NumpyArray, | ||||
|         scale: float = 1.0, | ||||
|         offset: float = 0.0, | ||||
|     ) -> None: | ||||
|  | @ -2046,7 +2082,11 @@ class Image: | |||
|             msg = "illegal image mode" | ||||
|             raise ValueError(msg) | ||||
|         if isinstance(data, ImagePalette.ImagePalette): | ||||
|             palette = ImagePalette.raw(data.rawmode, data.palette) | ||||
|             if data.rawmode is not None: | ||||
|                 palette = ImagePalette.raw(data.rawmode, data.palette) | ||||
|             else: | ||||
|                 palette = ImagePalette.ImagePalette(palette=data.palette) | ||||
|                 palette.dirty = 1 | ||||
|         else: | ||||
|             if not isinstance(data, bytes): | ||||
|                 data = bytes(data) | ||||
|  | @ -2093,7 +2133,8 @@ class Image: | |||
|             if self.mode == "PA": | ||||
|                 alpha = value[3] if len(value) == 4 else 255 | ||||
|                 value = value[:3] | ||||
|             palette_index = self.palette.getcolor(value, self) | ||||
|             assert self.palette is not None | ||||
|             palette_index = self.palette.getcolor(tuple(value), self) | ||||
|             value = (palette_index, alpha) if self.mode == "PA" else palette_index | ||||
|         return self.im.putpixel(xy, value) | ||||
| 
 | ||||
|  | @ -2184,7 +2225,12 @@ class Image: | |||
| 
 | ||||
|         return m_im | ||||
| 
 | ||||
|     def _get_safe_box(self, size, resample, box): | ||||
|     def _get_safe_box( | ||||
|         self, | ||||
|         size: tuple[int, int], | ||||
|         resample: Resampling, | ||||
|         box: tuple[float, float, float, float], | ||||
|     ) -> tuple[int, int, int, int]: | ||||
|         """Expands the box so it includes adjacent pixels | ||||
|         that may be used by resampling with the given resampling filter. | ||||
|         """ | ||||
|  | @ -2294,7 +2340,7 @@ class Image: | |||
|             factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 | ||||
|             factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 | ||||
|             if factor_x > 1 or factor_y > 1: | ||||
|                 reduce_box = self._get_safe_box(size, resample, box) | ||||
|                 reduce_box = self._get_safe_box(size, cast(Resampling, resample), box) | ||||
|                 factor = (factor_x, factor_y) | ||||
|                 self = ( | ||||
|                     self.reduce(factor, box=reduce_box) | ||||
|  | @ -2430,7 +2476,7 @@ class Image: | |||
|             0.0, | ||||
|         ] | ||||
| 
 | ||||
|         def transform(x, y, matrix): | ||||
|         def transform(x: float, y: float, matrix: list[float]) -> tuple[float, float]: | ||||
|             (a, b, c, d, e, f) = matrix | ||||
|             return a * x + b * y + c, d * x + e * y + f | ||||
| 
 | ||||
|  | @ -2445,9 +2491,9 @@ class Image: | |||
|             xx = [] | ||||
|             yy = [] | ||||
|             for x, y in ((0, 0), (w, 0), (w, h), (0, h)): | ||||
|                 x, y = transform(x, y, matrix) | ||||
|                 xx.append(x) | ||||
|                 yy.append(y) | ||||
|                 transformed_x, transformed_y = transform(x, y, matrix) | ||||
|                 xx.append(transformed_x) | ||||
|                 yy.append(transformed_y) | ||||
|             nw = math.ceil(max(xx)) - math.floor(min(xx)) | ||||
|             nh = math.ceil(max(yy)) - math.floor(min(yy)) | ||||
| 
 | ||||
|  | @ -2705,7 +2751,7 @@ class Image: | |||
|         provided_size = tuple(map(math.floor, size)) | ||||
| 
 | ||||
|         def preserve_aspect_ratio() -> tuple[int, int] | None: | ||||
|             def round_aspect(number, key): | ||||
|             def round_aspect(number: float, key: Callable[[int], float]) -> int: | ||||
|                 return max(min(math.floor(number), math.ceil(number), key=key), 1) | ||||
| 
 | ||||
|             x, y = provided_size | ||||
|  | @ -2849,8 +2895,14 @@ class Image: | |||
|         return im | ||||
| 
 | ||||
|     def __transformer( | ||||
|         self, box, image, method, data, resample=Resampling.NEAREST, fill=1 | ||||
|     ): | ||||
|         self, | ||||
|         box: tuple[int, int, int, int], | ||||
|         image: Image, | ||||
|         method: Transform, | ||||
|         data: Sequence[float], | ||||
|         resample: int = Resampling.NEAREST, | ||||
|         fill: bool = True, | ||||
|     ) -> None: | ||||
|         w = box[2] - box[0] | ||||
|         h = box[3] - box[1] | ||||
| 
 | ||||
|  | @ -2899,11 +2951,12 @@ class Image: | |||
|             Resampling.BICUBIC, | ||||
|         ): | ||||
|             if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): | ||||
|                 msg = { | ||||
|                 unusable: dict[int, str] = { | ||||
|                     Resampling.BOX: "Image.Resampling.BOX", | ||||
|                     Resampling.HAMMING: "Image.Resampling.HAMMING", | ||||
|                     Resampling.LANCZOS: "Image.Resampling.LANCZOS", | ||||
|                 }[resample] + f" ({resample}) cannot be used." | ||||
|                 } | ||||
|                 msg = unusable[resample] + f" ({resample}) cannot be used." | ||||
|             else: | ||||
|                 msg = f"Unknown resampling filter ({resample})." | ||||
| 
 | ||||
|  | @ -2950,7 +3003,7 @@ class Image: | |||
|         self.load() | ||||
|         return self._new(self.im.effect_spread(distance)) | ||||
| 
 | ||||
|     def toqimage(self): | ||||
|     def toqimage(self) -> ImageQt.ImageQt: | ||||
|         """Returns a QImage copy of this image""" | ||||
|         from . import ImageQt | ||||
| 
 | ||||
|  | @ -2959,7 +3012,7 @@ class Image: | |||
|             raise ImportError(msg) | ||||
|         return ImageQt.toqimage(self) | ||||
| 
 | ||||
|     def toqpixmap(self): | ||||
|     def toqpixmap(self) -> ImageQt.QPixmap: | ||||
|         """Returns a QPixmap copy of this image""" | ||||
|         from . import ImageQt | ||||
| 
 | ||||
|  | @ -3286,7 +3339,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: | |||
|     return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) | ||||
| 
 | ||||
| 
 | ||||
| def fromqimage(im) -> ImageFile.ImageFile: | ||||
| def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile: | ||||
|     """Creates an image instance from a QImage image""" | ||||
|     from . import ImageQt | ||||
| 
 | ||||
|  | @ -3296,7 +3349,7 @@ def fromqimage(im) -> ImageFile.ImageFile: | |||
|     return ImageQt.fromqimage(im) | ||||
| 
 | ||||
| 
 | ||||
| def fromqpixmap(im) -> ImageFile.ImageFile: | ||||
| def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile: | ||||
|     """Creates an image instance from a QPixmap image""" | ||||
|     from . import ImageQt | ||||
| 
 | ||||
|  | @ -3843,18 +3896,18 @@ class Exif(_ExifBase): | |||
|       print(gps_ifd[ExifTags.GPS.GPSDateStamp])  # 1999:99:99 99:99:99 | ||||
|     """ | ||||
| 
 | ||||
|     endian = None | ||||
|     endian: str | None = None | ||||
|     bigtiff = False | ||||
|     _loaded = False | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         self._data = {} | ||||
|         self._hidden_data = {} | ||||
|         self._ifds = {} | ||||
|         self._info = None | ||||
|         self._loaded_exif = None | ||||
|     def __init__(self) -> None: | ||||
|         self._data: dict[int, Any] = {} | ||||
|         self._hidden_data: dict[int, Any] = {} | ||||
|         self._ifds: dict[int, dict[int, Any]] = {} | ||||
|         self._info: TiffImagePlugin.ImageFileDirectory_v2 | None = None | ||||
|         self._loaded_exif: bytes | None = None | ||||
| 
 | ||||
|     def _fixup(self, value): | ||||
|     def _fixup(self, value: Any) -> Any: | ||||
|         try: | ||||
|             if len(value) == 1 and isinstance(value, tuple): | ||||
|                 return value[0] | ||||
|  | @ -3862,24 +3915,26 @@ class Exif(_ExifBase): | |||
|             pass | ||||
|         return value | ||||
| 
 | ||||
|     def _fixup_dict(self, src_dict): | ||||
|     def _fixup_dict(self, src_dict: dict[int, Any]) -> dict[int, Any]: | ||||
|         # Helper function | ||||
|         # returns a dict with any single item tuples/lists as individual values | ||||
|         return {k: self._fixup(v) for k, v in src_dict.items()} | ||||
| 
 | ||||
|     def _get_ifd_dict(self, offset: int, group=None): | ||||
|     def _get_ifd_dict( | ||||
|         self, offset: int, group: int | None = None | ||||
|     ) -> dict[int, Any] | None: | ||||
|         try: | ||||
|             # an offset pointer to the location of the nested embedded IFD. | ||||
|             # It should be a long, but may be corrupted. | ||||
|             self.fp.seek(offset) | ||||
|         except (KeyError, TypeError): | ||||
|             pass | ||||
|             return None | ||||
|         else: | ||||
|             from . import TiffImagePlugin | ||||
| 
 | ||||
|             info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) | ||||
|             info.load(self.fp) | ||||
|             return self._fixup_dict(info) | ||||
|             return self._fixup_dict(dict(info)) | ||||
| 
 | ||||
|     def _get_head(self) -> bytes: | ||||
|         version = b"\x2B" if self.bigtiff else b"\x2A" | ||||
|  | @ -3892,7 +3947,7 @@ class Exif(_ExifBase): | |||
|             head += b"\x00\x00\x00\x00" | ||||
|         return head | ||||
| 
 | ||||
|     def load(self, data): | ||||
|     def load(self, data: bytes) -> None: | ||||
|         # Extract EXIF information.  This is highly experimental, | ||||
|         # and is likely to be replaced with something better in a future | ||||
|         # version. | ||||
|  | @ -3911,7 +3966,7 @@ class Exif(_ExifBase): | |||
|             self._info = None | ||||
|             return | ||||
| 
 | ||||
|         self.fp = io.BytesIO(data) | ||||
|         self.fp: IO[bytes] = io.BytesIO(data) | ||||
|         self.head = self.fp.read(8) | ||||
|         # process dictionary | ||||
|         from . import TiffImagePlugin | ||||
|  | @ -3921,7 +3976,7 @@ class Exif(_ExifBase): | |||
|         self.fp.seek(self._info.next) | ||||
|         self._info.load(self.fp) | ||||
| 
 | ||||
|     def load_from_fp(self, fp, offset=None): | ||||
|     def load_from_fp(self, fp: IO[bytes], offset: int | None = None) -> None: | ||||
|         self._loaded_exif = None | ||||
|         self._data.clear() | ||||
|         self._hidden_data.clear() | ||||
|  | @ -3944,7 +3999,7 @@ class Exif(_ExifBase): | |||
|         self.fp.seek(offset) | ||||
|         self._info.load(self.fp) | ||||
| 
 | ||||
|     def _get_merged_dict(self): | ||||
|     def _get_merged_dict(self) -> dict[int, Any]: | ||||
|         merged_dict = dict(self) | ||||
| 
 | ||||
|         # get EXIF extension | ||||
|  | @ -3982,15 +4037,19 @@ class Exif(_ExifBase): | |||
|             ifd[tag] = value | ||||
|         return b"Exif\x00\x00" + head + ifd.tobytes(offset) | ||||
| 
 | ||||
|     def get_ifd(self, tag): | ||||
|     def get_ifd(self, tag: int) -> dict[int, Any]: | ||||
|         if tag not in self._ifds: | ||||
|             if tag == ExifTags.IFD.IFD1: | ||||
|                 if self._info is not None and self._info.next != 0: | ||||
|                     self._ifds[tag] = self._get_ifd_dict(self._info.next) | ||||
|                     ifd = self._get_ifd_dict(self._info.next) | ||||
|                     if ifd is not None: | ||||
|                         self._ifds[tag] = ifd | ||||
|             elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: | ||||
|                 offset = self._hidden_data.get(tag, self.get(tag)) | ||||
|                 if offset is not None: | ||||
|                     self._ifds[tag] = self._get_ifd_dict(offset, tag) | ||||
|                     ifd = self._get_ifd_dict(offset, tag) | ||||
|                     if ifd is not None: | ||||
|                         self._ifds[tag] = ifd | ||||
|             elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: | ||||
|                 if ExifTags.IFD.Exif not in self._ifds: | ||||
|                     self.get_ifd(ExifTags.IFD.Exif) | ||||
|  | @ -4047,7 +4106,9 @@ class Exif(_ExifBase): | |||
|                                 (offset,) = struct.unpack(">L", data) | ||||
|                                 self.fp.seek(offset) | ||||
| 
 | ||||
|                                 camerainfo = {"ModelID": self.fp.read(4)} | ||||
|                                 camerainfo: dict[str, int | bytes] = { | ||||
|                                     "ModelID": self.fp.read(4) | ||||
|                                 } | ||||
| 
 | ||||
|                                 self.fp.read(4) | ||||
|                                 # Seconds since 2000 | ||||
|  | @ -4063,16 +4124,18 @@ class Exif(_ExifBase): | |||
|                                 ][1] | ||||
|                                 camerainfo["Parallax"] = handler( | ||||
|                                     ImageFileDirectory_v2(), parallax, False | ||||
|                                 ) | ||||
|                                 )[0] | ||||
| 
 | ||||
|                                 self.fp.read(4) | ||||
|                                 camerainfo["Category"] = self.fp.read(2) | ||||
| 
 | ||||
|                                 makernote = {0x1101: dict(self._fixup_dict(camerainfo))} | ||||
|                                 makernote = {0x1101: camerainfo} | ||||
|                         self._ifds[tag] = makernote | ||||
|                 else: | ||||
|                     # Interop | ||||
|                     self._ifds[tag] = self._get_ifd_dict(tag_data, tag) | ||||
|                     ifd = self._get_ifd_dict(tag_data, tag) | ||||
|                     if ifd is not None: | ||||
|                         self._ifds[tag] = ifd | ||||
|         ifd = self._ifds.get(tag, {}) | ||||
|         if tag == ExifTags.IFD.Exif and self._hidden_data: | ||||
|             ifd = { | ||||
|  | @ -4102,7 +4165,7 @@ class Exif(_ExifBase): | |||
|             keys.update(self._info) | ||||
|         return len(keys) | ||||
| 
 | ||||
|     def __getitem__(self, tag: int): | ||||
|     def __getitem__(self, tag: int) -> Any: | ||||
|         if self._info is not None and tag not in self._data and tag in self._info: | ||||
|             self._data[tag] = self._fixup(self._info[tag]) | ||||
|             del self._info[tag] | ||||
|  | @ -4111,7 +4174,7 @@ class Exif(_ExifBase): | |||
|     def __contains__(self, tag: object) -> bool: | ||||
|         return tag in self._data or (self._info is not None and tag in self._info) | ||||
| 
 | ||||
|     def __setitem__(self, tag: int, value) -> None: | ||||
|     def __setitem__(self, tag: int, value: Any) -> None: | ||||
|         if self._info is not None and tag in self._info: | ||||
|             del self._info[tag] | ||||
|         self._data[tag] = value | ||||
|  | @ -4122,7 +4185,7 @@ class Exif(_ExifBase): | |||
|         else: | ||||
|             del self._data[tag] | ||||
| 
 | ||||
|     def __iter__(self): | ||||
|     def __iter__(self) -> Iterator[int]: | ||||
|         keys = set(self._data) | ||||
|         if self._info is not None: | ||||
|             keys.update(self._info) | ||||
|  |  | |||
|  | @ -32,11 +32,10 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| import math | ||||
| import numbers | ||||
| import struct | ||||
| from collections.abc import Sequence | ||||
| from types import ModuleType | ||||
| from typing import TYPE_CHECKING, AnyStr, Callable, Union, cast | ||||
| from typing import TYPE_CHECKING, Any, AnyStr, Callable, Union, cast | ||||
| 
 | ||||
| from . import Image, ImageColor | ||||
| from ._deprecate import deprecate | ||||
|  | @ -160,13 +159,13 @@ class ImageDraw: | |||
|             if ink is not None: | ||||
|                 if isinstance(ink, str): | ||||
|                     ink = ImageColor.getcolor(ink, self.mode) | ||||
|                 if self.palette and not isinstance(ink, numbers.Number): | ||||
|                 if self.palette and isinstance(ink, tuple): | ||||
|                     ink = self.palette.getcolor(ink, self._image) | ||||
|                 result_ink = self.draw.draw_ink(ink) | ||||
|             if fill is not None: | ||||
|                 if isinstance(fill, str): | ||||
|                     fill = ImageColor.getcolor(fill, self.mode) | ||||
|                 if self.palette and not isinstance(fill, numbers.Number): | ||||
|                 if self.palette and isinstance(fill, tuple): | ||||
|                     fill = self.palette.getcolor(fill, self._image) | ||||
|                 result_fill = self.draw.draw_ink(fill) | ||||
|         return result_ink, result_fill | ||||
|  | @ -505,7 +504,7 @@ class ImageDraw: | |||
| 
 | ||||
|             if full_x: | ||||
|                 self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1) | ||||
|             else: | ||||
|             elif x1 - r - 1 > x0 + r + 1: | ||||
|                 self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1) | ||||
|             if not full_x and not full_y: | ||||
|                 left = [x0, y0, x0 + r, y1] | ||||
|  | @ -561,7 +560,12 @@ class ImageDraw: | |||
|     def _multiline_split(self, text: AnyStr) -> list[AnyStr]: | ||||
|         return text.split("\n" if isinstance(text, str) else b"\n") | ||||
| 
 | ||||
|     def _multiline_spacing(self, font, spacing, stroke_width): | ||||
|     def _multiline_spacing( | ||||
|         self, | ||||
|         font: ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, | ||||
|         spacing: float, | ||||
|         stroke_width: float, | ||||
|     ) -> float: | ||||
|         return ( | ||||
|             self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] | ||||
|             + stroke_width | ||||
|  | @ -571,25 +575,25 @@ class ImageDraw: | |||
|     def text( | ||||
|         self, | ||||
|         xy: tuple[float, float], | ||||
|         text: str, | ||||
|         fill=None, | ||||
|         text: AnyStr, | ||||
|         fill: _Ink | None = None, | ||||
|         font: ( | ||||
|             ImageFont.ImageFont | ||||
|             | ImageFont.FreeTypeFont | ||||
|             | ImageFont.TransposedFont | ||||
|             | None | ||||
|         ) = None, | ||||
|         anchor=None, | ||||
|         spacing=4, | ||||
|         align="left", | ||||
|         direction=None, | ||||
|         features=None, | ||||
|         language=None, | ||||
|         stroke_width=0, | ||||
|         stroke_fill=None, | ||||
|         embedded_color=False, | ||||
|         *args, | ||||
|         **kwargs, | ||||
|         anchor: str | None = None, | ||||
|         spacing: float = 4, | ||||
|         align: str = "left", | ||||
|         direction: str | None = None, | ||||
|         features: list[str] | None = None, | ||||
|         language: str | None = None, | ||||
|         stroke_width: float = 0, | ||||
|         stroke_fill: _Ink | None = None, | ||||
|         embedded_color: bool = False, | ||||
|         *args: Any, | ||||
|         **kwargs: Any, | ||||
|     ) -> None: | ||||
|         """Draw text.""" | ||||
|         if embedded_color and self.mode not in ("RGB", "RGBA"): | ||||
|  | @ -623,15 +627,14 @@ class ImageDraw: | |||
|                 return fill_ink | ||||
|             return ink | ||||
| 
 | ||||
|         def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: | ||||
|         def draw_text(ink: int, stroke_width: float = 0) -> None: | ||||
|             mode = self.fontmode | ||||
|             if stroke_width == 0 and embedded_color: | ||||
|                 mode = "RGBA" | ||||
|             coord = [] | ||||
|             start = [] | ||||
|             for i in range(2): | ||||
|                 coord.append(int(xy[i])) | ||||
|                 start.append(math.modf(xy[i])[0]) | ||||
|             start = (math.modf(xy[0])[0], math.modf(xy[1])[0]) | ||||
|             try: | ||||
|                 mask, offset = font.getmask2(  # type: ignore[union-attr,misc] | ||||
|                     text, | ||||
|  | @ -664,8 +667,6 @@ class ImageDraw: | |||
|                     ) | ||||
|                 except TypeError: | ||||
|                     mask = font.getmask(text) | ||||
|             if stroke_offset: | ||||
|                 coord = [coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]] | ||||
|             if mode == "RGBA": | ||||
|                 # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A | ||||
|                 # extract mask and set text alpha | ||||
|  | @ -699,25 +700,25 @@ class ImageDraw: | |||
|     def multiline_text( | ||||
|         self, | ||||
|         xy: tuple[float, float], | ||||
|         text: str, | ||||
|         fill=None, | ||||
|         text: AnyStr, | ||||
|         fill: _Ink | None = None, | ||||
|         font: ( | ||||
|             ImageFont.ImageFont | ||||
|             | ImageFont.FreeTypeFont | ||||
|             | ImageFont.TransposedFont | ||||
|             | None | ||||
|         ) = None, | ||||
|         anchor=None, | ||||
|         spacing=4, | ||||
|         align="left", | ||||
|         direction=None, | ||||
|         features=None, | ||||
|         language=None, | ||||
|         stroke_width=0, | ||||
|         stroke_fill=None, | ||||
|         embedded_color=False, | ||||
|         anchor: str | None = None, | ||||
|         spacing: float = 4, | ||||
|         align: str = "left", | ||||
|         direction: str | None = None, | ||||
|         features: list[str] | None = None, | ||||
|         language: str | None = None, | ||||
|         stroke_width: float = 0, | ||||
|         stroke_fill: _Ink | None = None, | ||||
|         embedded_color: bool = False, | ||||
|         *, | ||||
|         font_size=None, | ||||
|         font_size: float | None = None, | ||||
|     ) -> None: | ||||
|         if direction == "ttb": | ||||
|             msg = "ttb direction is unsupported for multiline text" | ||||
|  | @ -790,19 +791,19 @@ class ImageDraw: | |||
| 
 | ||||
|     def textlength( | ||||
|         self, | ||||
|         text: str, | ||||
|         text: AnyStr, | ||||
|         font: ( | ||||
|             ImageFont.ImageFont | ||||
|             | ImageFont.FreeTypeFont | ||||
|             | ImageFont.TransposedFont | ||||
|             | None | ||||
|         ) = None, | ||||
|         direction=None, | ||||
|         features=None, | ||||
|         language=None, | ||||
|         embedded_color=False, | ||||
|         direction: str | None = None, | ||||
|         features: list[str] | None = None, | ||||
|         language: str | None = None, | ||||
|         embedded_color: bool = False, | ||||
|         *, | ||||
|         font_size=None, | ||||
|         font_size: float | None = None, | ||||
|     ) -> float: | ||||
|         """Get the length of a given string, in pixels with 1/64 precision.""" | ||||
|         if self._multiline_check(text): | ||||
|  | @ -819,20 +820,25 @@ class ImageDraw: | |||
| 
 | ||||
|     def textbbox( | ||||
|         self, | ||||
|         xy, | ||||
|         text, | ||||
|         font=None, | ||||
|         anchor=None, | ||||
|         spacing=4, | ||||
|         align="left", | ||||
|         direction=None, | ||||
|         features=None, | ||||
|         language=None, | ||||
|         stroke_width=0, | ||||
|         embedded_color=False, | ||||
|         xy: tuple[float, float], | ||||
|         text: AnyStr, | ||||
|         font: ( | ||||
|             ImageFont.ImageFont | ||||
|             | ImageFont.FreeTypeFont | ||||
|             | ImageFont.TransposedFont | ||||
|             | None | ||||
|         ) = None, | ||||
|         anchor: str | None = None, | ||||
|         spacing: float = 4, | ||||
|         align: str = "left", | ||||
|         direction: str | None = None, | ||||
|         features: list[str] | None = None, | ||||
|         language: str | None = None, | ||||
|         stroke_width: float = 0, | ||||
|         embedded_color: bool = False, | ||||
|         *, | ||||
|         font_size=None, | ||||
|     ) -> tuple[int, int, int, int]: | ||||
|         font_size: float | None = None, | ||||
|     ) -> tuple[float, float, float, float]: | ||||
|         """Get the bounding box of a given string, in pixels.""" | ||||
|         if embedded_color and self.mode not in ("RGB", "RGBA"): | ||||
|             msg = "Embedded color supported only in RGB and RGBA modes" | ||||
|  | @ -864,20 +870,25 @@ class ImageDraw: | |||
| 
 | ||||
|     def multiline_textbbox( | ||||
|         self, | ||||
|         xy, | ||||
|         text, | ||||
|         font=None, | ||||
|         anchor=None, | ||||
|         spacing=4, | ||||
|         align="left", | ||||
|         direction=None, | ||||
|         features=None, | ||||
|         language=None, | ||||
|         stroke_width=0, | ||||
|         embedded_color=False, | ||||
|         xy: tuple[float, float], | ||||
|         text: AnyStr, | ||||
|         font: ( | ||||
|             ImageFont.ImageFont | ||||
|             | ImageFont.FreeTypeFont | ||||
|             | ImageFont.TransposedFont | ||||
|             | None | ||||
|         ) = None, | ||||
|         anchor: str | None = None, | ||||
|         spacing: float = 4, | ||||
|         align: str = "left", | ||||
|         direction: str | None = None, | ||||
|         features: list[str] | None = None, | ||||
|         language: str | None = None, | ||||
|         stroke_width: float = 0, | ||||
|         embedded_color: bool = False, | ||||
|         *, | ||||
|         font_size=None, | ||||
|     ) -> tuple[int, int, int, int]: | ||||
|         font_size: float | None = None, | ||||
|     ) -> tuple[float, float, float, float]: | ||||
|         if direction == "ttb": | ||||
|             msg = "ttb direction is unsupported for multiline text" | ||||
|             raise ValueError(msg) | ||||
|  | @ -916,7 +927,7 @@ class ImageDraw: | |||
|         elif anchor[1] == "d": | ||||
|             top -= (len(lines) - 1) * line_spacing | ||||
| 
 | ||||
|         bbox: tuple[int, int, int, int] | None = None | ||||
|         bbox: tuple[float, float, float, float] | None = None | ||||
| 
 | ||||
|         for idx, line in enumerate(lines): | ||||
|             left = xy[0] | ||||
|  |  | |||
|  | @ -24,10 +24,10 @@ | |||
| """ | ||||
| from __future__ import annotations | ||||
| 
 | ||||
| from typing import BinaryIO | ||||
| from typing import Any, AnyStr, BinaryIO | ||||
| 
 | ||||
| from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath | ||||
| from ._typing import StrOrBytesPath | ||||
| from ._typing import Coords, StrOrBytesPath | ||||
| 
 | ||||
| 
 | ||||
| class Pen: | ||||
|  | @ -74,12 +74,19 @@ class Draw: | |||
|             image = Image.new(image, size, color) | ||||
|         self.draw = ImageDraw.Draw(image) | ||||
|         self.image = image | ||||
|         self.transform = None | ||||
|         self.transform: tuple[float, float, float, float, float, float] | None = None | ||||
| 
 | ||||
|     def flush(self) -> Image.Image: | ||||
|         return self.image | ||||
| 
 | ||||
|     def render(self, op, xy, pen, brush=None): | ||||
|     def render( | ||||
|         self, | ||||
|         op: str, | ||||
|         xy: Coords, | ||||
|         pen: Pen | Brush | None, | ||||
|         brush: Brush | Pen | None = None, | ||||
|         **kwargs: Any, | ||||
|     ) -> None: | ||||
|         # handle color arguments | ||||
|         outline = fill = None | ||||
|         width = 1 | ||||
|  | @ -95,63 +102,89 @@ class Draw: | |||
|             fill = pen.color | ||||
|         # handle transformation | ||||
|         if self.transform: | ||||
|             xy = ImagePath.Path(xy) | ||||
|             xy.transform(self.transform) | ||||
|             path = ImagePath.Path(xy) | ||||
|             path.transform(self.transform) | ||||
|             xy = path | ||||
|         # render the item | ||||
|         if op == "line": | ||||
|             self.draw.line(xy, fill=outline, width=width) | ||||
|         if op in ("arc", "line"): | ||||
|             kwargs.setdefault("fill", outline) | ||||
|         else: | ||||
|             getattr(self.draw, op)(xy, fill=fill, outline=outline) | ||||
|             kwargs.setdefault("fill", fill) | ||||
|             kwargs.setdefault("outline", outline) | ||||
|         if op == "line": | ||||
|             kwargs.setdefault("width", width) | ||||
|         getattr(self.draw, op)(xy, **kwargs) | ||||
| 
 | ||||
|     def settransform(self, offset): | ||||
|     def settransform(self, offset: tuple[float, float]) -> None: | ||||
|         """Sets a transformation offset.""" | ||||
|         (xoffset, yoffset) = offset | ||||
|         self.transform = (1, 0, xoffset, 0, 1, yoffset) | ||||
| 
 | ||||
|     def arc(self, xy, start, end, *options): | ||||
|     def arc( | ||||
|         self, | ||||
|         xy: Coords, | ||||
|         pen: Pen | Brush | None, | ||||
|         start: float, | ||||
|         end: float, | ||||
|         *options: Any, | ||||
|     ) -> None: | ||||
|         """ | ||||
|         Draws an arc (a portion of a circle outline) between the start and end | ||||
|         angles, inside the given bounding box. | ||||
| 
 | ||||
|         .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc` | ||||
|         """ | ||||
|         self.render("arc", xy, start, end, *options) | ||||
|         self.render("arc", xy, pen, *options, start=start, end=end) | ||||
| 
 | ||||
|     def chord(self, xy, start, end, *options): | ||||
|     def chord( | ||||
|         self, | ||||
|         xy: Coords, | ||||
|         pen: Pen | Brush | None, | ||||
|         start: float, | ||||
|         end: float, | ||||
|         *options: Any, | ||||
|     ) -> None: | ||||
|         """ | ||||
|         Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points | ||||
|         with a straight line. | ||||
| 
 | ||||
|         .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord` | ||||
|         """ | ||||
|         self.render("chord", xy, start, end, *options) | ||||
|         self.render("chord", xy, pen, *options, start=start, end=end) | ||||
| 
 | ||||
|     def ellipse(self, xy, *options): | ||||
|     def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: | ||||
|         """ | ||||
|         Draws an ellipse inside the given bounding box. | ||||
| 
 | ||||
|         .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse` | ||||
|         """ | ||||
|         self.render("ellipse", xy, *options) | ||||
|         self.render("ellipse", xy, pen, *options) | ||||
| 
 | ||||
|     def line(self, xy, *options): | ||||
|     def line(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: | ||||
|         """ | ||||
|         Draws a line between the coordinates in the ``xy`` list. | ||||
| 
 | ||||
|         .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line` | ||||
|         """ | ||||
|         self.render("line", xy, *options) | ||||
|         self.render("line", xy, pen, *options) | ||||
| 
 | ||||
|     def pieslice(self, xy, start, end, *options): | ||||
|     def pieslice( | ||||
|         self, | ||||
|         xy: Coords, | ||||
|         pen: Pen | Brush | None, | ||||
|         start: float, | ||||
|         end: float, | ||||
|         *options: Any, | ||||
|     ) -> None: | ||||
|         """ | ||||
|         Same as arc, but also draws straight lines between the end points and the | ||||
|         center of the bounding box. | ||||
| 
 | ||||
|         .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice` | ||||
|         """ | ||||
|         self.render("pieslice", xy, start, end, *options) | ||||
|         self.render("pieslice", xy, pen, *options, start=start, end=end) | ||||
| 
 | ||||
|     def polygon(self, xy, *options): | ||||
|     def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: | ||||
|         """ | ||||
|         Draws a polygon. | ||||
| 
 | ||||
|  | @ -162,28 +195,31 @@ class Draw: | |||
| 
 | ||||
|         .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon` | ||||
|         """ | ||||
|         self.render("polygon", xy, *options) | ||||
|         self.render("polygon", xy, pen, *options) | ||||
| 
 | ||||
|     def rectangle(self, xy, *options): | ||||
|     def rectangle(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None: | ||||
|         """ | ||||
|         Draws a rectangle. | ||||
| 
 | ||||
|         .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle` | ||||
|         """ | ||||
|         self.render("rectangle", xy, *options) | ||||
|         self.render("rectangle", xy, pen, *options) | ||||
| 
 | ||||
|     def text(self, xy, text, font): | ||||
|     def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None: | ||||
|         """ | ||||
|         Draws the string at the given position. | ||||
| 
 | ||||
|         .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text` | ||||
|         """ | ||||
|         if self.transform: | ||||
|             xy = ImagePath.Path(xy) | ||||
|             xy.transform(self.transform) | ||||
|             path = ImagePath.Path(xy) | ||||
|             path.transform(self.transform) | ||||
|             xy = path | ||||
|         self.draw.text(xy, text, font=font.font, fill=font.color) | ||||
| 
 | ||||
|     def textbbox(self, xy, text, font): | ||||
|     def textbbox( | ||||
|         self, xy: tuple[float, float], text: AnyStr, font: Font | ||||
|     ) -> tuple[float, float, float, float]: | ||||
|         """ | ||||
|         Returns bounding box (in pixels) of given text. | ||||
| 
 | ||||
|  | @ -192,11 +228,12 @@ class Draw: | |||
|         .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox` | ||||
|         """ | ||||
|         if self.transform: | ||||
|             xy = ImagePath.Path(xy) | ||||
|             xy.transform(self.transform) | ||||
|             path = ImagePath.Path(xy) | ||||
|             path.transform(self.transform) | ||||
|             xy = path | ||||
|         return self.draw.textbbox(xy, text, font=font.font) | ||||
| 
 | ||||
|     def textlength(self, text, font): | ||||
|     def textlength(self, text: AnyStr, font: Font) -> float: | ||||
|         """ | ||||
|         Returns length (in pixels) of given text. | ||||
|         This is the amount by which following text should be offset. | ||||
|  |  | |||
|  | @ -31,6 +31,7 @@ from __future__ import annotations | |||
| import abc | ||||
| import io | ||||
| import itertools | ||||
| import os | ||||
| import struct | ||||
| import sys | ||||
| from typing import IO, Any, NamedTuple | ||||
|  | @ -86,14 +87,14 @@ def raise_oserror(error: int) -> OSError: | |||
|     raise _get_oserror(error, encoder=False) | ||||
| 
 | ||||
| 
 | ||||
| def _tilesort(t) -> int: | ||||
| def _tilesort(t: _Tile) -> int: | ||||
|     # sort on offset | ||||
|     return t[2] | ||||
| 
 | ||||
| 
 | ||||
| class _Tile(NamedTuple): | ||||
|     codec_name: str | ||||
|     extents: tuple[int, int, int, int] | ||||
|     extents: tuple[int, int, int, int] | None | ||||
|     offset: int | ||||
|     args: tuple[Any, ...] | str | None | ||||
| 
 | ||||
|  | @ -163,7 +164,7 @@ class ImageFile(Image.Image): | |||
|             return Image.MIME.get(self.format.upper()) | ||||
|         return None | ||||
| 
 | ||||
|     def __setstate__(self, state) -> None: | ||||
|     def __setstate__(self, state: list[Any]) -> None: | ||||
|         self.tile = [] | ||||
|         super().__setstate__(state) | ||||
| 
 | ||||
|  | @ -176,7 +177,7 @@ class ImageFile(Image.Image): | |||
|             self.fp.close() | ||||
|         self.fp = None | ||||
| 
 | ||||
|     def load(self): | ||||
|     def load(self) -> Image.core.PixelAccess | None: | ||||
|         """Load image data based on tile list""" | ||||
| 
 | ||||
|         if self.tile is None: | ||||
|  | @ -187,7 +188,7 @@ class ImageFile(Image.Image): | |||
|         if not self.tile: | ||||
|             return pixel | ||||
| 
 | ||||
|         self.map = None | ||||
|         self.map: mmap.mmap | None = None | ||||
|         use_mmap = self.filename and len(self.tile) == 1 | ||||
|         # As of pypy 2.1.0, memory mapping was failing here. | ||||
|         use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") | ||||
|  | @ -195,17 +196,17 @@ class ImageFile(Image.Image): | |||
|         readonly = 0 | ||||
| 
 | ||||
|         # look for read/seek overrides | ||||
|         try: | ||||
|         if hasattr(self, "load_read"): | ||||
|             read = self.load_read | ||||
|             # don't use mmap if there are custom read/seek functions | ||||
|             use_mmap = False | ||||
|         except AttributeError: | ||||
|         else: | ||||
|             read = self.fp.read | ||||
| 
 | ||||
|         try: | ||||
|         if hasattr(self, "load_seek"): | ||||
|             seek = self.load_seek | ||||
|             use_mmap = False | ||||
|         except AttributeError: | ||||
|         else: | ||||
|             seek = self.fp.seek | ||||
| 
 | ||||
|         if use_mmap: | ||||
|  | @ -245,11 +246,8 @@ class ImageFile(Image.Image): | |||
|             # sort tiles in file order | ||||
|             self.tile.sort(key=_tilesort) | ||||
| 
 | ||||
|             try: | ||||
|                 # FIXME: This is a hack to handle TIFF's JpegTables tag. | ||||
|                 prefix = self.tile_prefix | ||||
|             except AttributeError: | ||||
|                 prefix = b"" | ||||
|             # FIXME: This is a hack to handle TIFF's JpegTables tag. | ||||
|             prefix = getattr(self, "tile_prefix", b"") | ||||
| 
 | ||||
|             # Remove consecutive duplicates that only differ by their offset | ||||
|             self.tile = [ | ||||
|  | @ -317,7 +315,7 @@ class ImageFile(Image.Image): | |||
| 
 | ||||
|     def load_prepare(self) -> None: | ||||
|         # create image memory if necessary | ||||
|         if not self.im or self.im.mode != self.mode or self.im.size != self.size: | ||||
|         if self._im is None or self.im.mode != self.mode or self.im.size != self.size: | ||||
|             self.im = Image.core.new(self.mode, self.size, *self.mb_config) | ||||
|         # create palette (optional) | ||||
|         if self.mode == "P": | ||||
|  | @ -527,7 +525,7 @@ class Parser: | |||
| # -------------------------------------------------------------------- | ||||
| 
 | ||||
| 
 | ||||
| def _save(im, fp, tile, bufsize: int = 0) -> None: | ||||
| def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None: | ||||
|     """Helper to save image based on tile list | ||||
| 
 | ||||
|     :param im: Image object. | ||||
|  | @ -556,7 +554,12 @@ def _save(im, fp, tile, bufsize: int = 0) -> None: | |||
| 
 | ||||
| 
 | ||||
| def _encode_tile( | ||||
|     im, fp: IO[bytes], tile: list[_Tile], bufsize: int, fh, exc=None | ||||
|     im: Image.Image, | ||||
|     fp: IO[bytes], | ||||
|     tile: list[_Tile], | ||||
|     bufsize: int, | ||||
|     fh: int | None, | ||||
|     exc: BaseException | None = None, | ||||
| ) -> None: | ||||
|     for encoder_name, extents, offset, args in tile: | ||||
|         if offset > 0: | ||||
|  | @ -577,6 +580,7 @@ def _encode_tile( | |||
|                             break | ||||
|                 else: | ||||
|                     # slight speedup: compress to real file object | ||||
|                     assert fh is not None | ||||
|                     errcode = encoder.encode_to_file(fh, bufsize) | ||||
|             if errcode < 0: | ||||
|                 raise _get_oserror(errcode, encoder=True) from exc | ||||
|  | @ -666,7 +670,11 @@ class PyCodec: | |||
|         """ | ||||
|         self.fd = fd | ||||
| 
 | ||||
|     def setimage(self, im, extents=None): | ||||
|     def setimage( | ||||
|         self, | ||||
|         im: Image.core.ImagingCore, | ||||
|         extents: tuple[int, int, int, int] | None = None, | ||||
|     ) -> None: | ||||
|         """ | ||||
|         Called from ImageFile to set the core output image for the codec | ||||
| 
 | ||||
|  | @ -797,7 +805,7 @@ class PyEncoder(PyCodec): | |||
|             self.fd.write(data) | ||||
|         return bytes_consumed, errcode | ||||
| 
 | ||||
|     def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int: | ||||
|     def encode_to_file(self, fh: int, bufsize: int) -> int: | ||||
|         """ | ||||
|         :param fh: File handle. | ||||
|         :param bufsize: Buffer size. | ||||
|  | @ -810,5 +818,5 @@ class PyEncoder(PyCodec): | |||
|         while errcode == 0: | ||||
|             status, errcode, buf = self.encode(bufsize) | ||||
|             if status > 0: | ||||
|                 fh.write(buf[status:]) | ||||
|                 os.write(fh, buf[status:]) | ||||
|         return errcode | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ import warnings | |||
| from enum import IntEnum | ||||
| from io import BytesIO | ||||
| from types import ModuleType | ||||
| from typing import IO, TYPE_CHECKING, Any, BinaryIO | ||||
| from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict | ||||
| 
 | ||||
| from . import Image | ||||
| from ._typing import StrOrBytesPath | ||||
|  | @ -46,6 +46,13 @@ if TYPE_CHECKING: | |||
|     from ._imagingft import Font | ||||
| 
 | ||||
| 
 | ||||
| class Axis(TypedDict): | ||||
|     minimum: int | None | ||||
|     default: int | None | ||||
|     maximum: int | None | ||||
|     name: bytes | None | ||||
| 
 | ||||
| 
 | ||||
| class Layout(IntEnum): | ||||
|     BASIC = 0 | ||||
|     RAQM = 1 | ||||
|  | @ -138,7 +145,9 @@ class ImageFont: | |||
| 
 | ||||
|         self.font = Image.core.font(image.im, data) | ||||
| 
 | ||||
|     def getmask(self, text, mode="", *args, **kwargs): | ||||
|     def getmask( | ||||
|         self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any | ||||
|     ) -> Image.core.ImagingCore: | ||||
|         """ | ||||
|         Create a bitmap for the text. | ||||
| 
 | ||||
|  | @ -236,7 +245,7 @@ class FreeTypeFont: | |||
| 
 | ||||
|         self.layout_engine = layout_engine | ||||
| 
 | ||||
|         def load_from_bytes(f): | ||||
|         def load_from_bytes(f) -> None: | ||||
|             self.font_bytes = f.read() | ||||
|             self.font = core.getfont( | ||||
|                 "", size, index, encoding, self.font_bytes, layout_engine | ||||
|  | @ -260,12 +269,12 @@ class FreeTypeFont: | |||
|         else: | ||||
|             load_from_bytes(font) | ||||
| 
 | ||||
|     def __getstate__(self): | ||||
|     def __getstate__(self) -> list[Any]: | ||||
|         return [self.path, self.size, self.index, self.encoding, self.layout_engine] | ||||
| 
 | ||||
|     def __setstate__(self, state): | ||||
|     def __setstate__(self, state: list[Any]) -> None: | ||||
|         path, size, index, encoding, layout_engine = state | ||||
|         self.__init__(path, size, index, encoding, layout_engine) | ||||
|         FreeTypeFont.__init__(self, path, size, index, encoding, layout_engine) | ||||
| 
 | ||||
|     def getname(self) -> tuple[str | None, str | None]: | ||||
|         """ | ||||
|  | @ -283,7 +292,12 @@ class FreeTypeFont: | |||
|         return self.font.ascent, self.font.descent | ||||
| 
 | ||||
|     def getlength( | ||||
|         self, text: str | bytes, mode="", direction=None, features=None, language=None | ||||
|         self, | ||||
|         text: str | bytes, | ||||
|         mode: str = "", | ||||
|         direction: str | None = None, | ||||
|         features: list[str] | None = None, | ||||
|         language: str | None = None, | ||||
|     ) -> float: | ||||
|         """ | ||||
|         Returns length (in pixels with 1/64 precision) of given text when rendered | ||||
|  | @ -424,16 +438,16 @@ class FreeTypeFont: | |||
| 
 | ||||
|     def getmask( | ||||
|         self, | ||||
|         text, | ||||
|         mode="", | ||||
|         direction=None, | ||||
|         features=None, | ||||
|         language=None, | ||||
|         stroke_width=0, | ||||
|         anchor=None, | ||||
|         ink=0, | ||||
|         start=None, | ||||
|     ): | ||||
|         text: str | bytes, | ||||
|         mode: str = "", | ||||
|         direction: str | None = None, | ||||
|         features: list[str] | None = None, | ||||
|         language: str | None = None, | ||||
|         stroke_width: float = 0, | ||||
|         anchor: str | None = None, | ||||
|         ink: int = 0, | ||||
|         start: tuple[float, float] | None = None, | ||||
|     ) -> Image.core.ImagingCore: | ||||
|         """ | ||||
|         Create a bitmap for the text. | ||||
| 
 | ||||
|  | @ -516,17 +530,17 @@ class FreeTypeFont: | |||
|     def getmask2( | ||||
|         self, | ||||
|         text: str | bytes, | ||||
|         mode="", | ||||
|         direction=None, | ||||
|         features=None, | ||||
|         language=None, | ||||
|         stroke_width=0, | ||||
|         anchor=None, | ||||
|         ink=0, | ||||
|         start=None, | ||||
|         *args, | ||||
|         **kwargs, | ||||
|     ): | ||||
|         mode: str = "", | ||||
|         direction: str | None = None, | ||||
|         features: list[str] | None = None, | ||||
|         language: str | None = None, | ||||
|         stroke_width: float = 0, | ||||
|         anchor: str | None = None, | ||||
|         ink: int = 0, | ||||
|         start: tuple[float, float] | None = None, | ||||
|         *args: Any, | ||||
|         **kwargs: Any, | ||||
|     ) -> tuple[Image.core.ImagingCore, tuple[int, int]]: | ||||
|         """ | ||||
|         Create a bitmap for the text. | ||||
| 
 | ||||
|  | @ -599,7 +613,7 @@ class FreeTypeFont: | |||
|         if start is None: | ||||
|             start = (0, 0) | ||||
| 
 | ||||
|         def fill(width, height): | ||||
|         def fill(width: int, height: int) -> Image.core.ImagingCore: | ||||
|             size = (width, height) | ||||
|             Image._decompression_bomb_check(size) | ||||
|             return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) | ||||
|  | @ -619,8 +633,13 @@ class FreeTypeFont: | |||
|         ) | ||||
| 
 | ||||
|     def font_variant( | ||||
|         self, font=None, size=None, index=None, encoding=None, layout_engine=None | ||||
|     ): | ||||
|         self, | ||||
|         font: StrOrBytesPath | BinaryIO | None = None, | ||||
|         size: float | None = None, | ||||
|         index: int | None = None, | ||||
|         encoding: str | None = None, | ||||
|         layout_engine: Layout | None = None, | ||||
|     ) -> FreeTypeFont: | ||||
|         """ | ||||
|         Create a copy of this FreeTypeFont object, | ||||
|         using any specified arguments to override the settings. | ||||
|  | @ -655,7 +674,7 @@ class FreeTypeFont: | |||
|             raise NotImplementedError(msg) from e | ||||
|         return [name.replace(b"\x00", b"") for name in names] | ||||
| 
 | ||||
|     def set_variation_by_name(self, name): | ||||
|     def set_variation_by_name(self, name: str | bytes) -> None: | ||||
|         """ | ||||
|         :param name: The name of the style. | ||||
|         :exception OSError: If the font is not a variation font. | ||||
|  | @ -674,7 +693,7 @@ class FreeTypeFont: | |||
| 
 | ||||
|         self.font.setvarname(index) | ||||
| 
 | ||||
|     def get_variation_axes(self): | ||||
|     def get_variation_axes(self) -> list[Axis]: | ||||
|         """ | ||||
|         :returns: A list of the axes in a variation font. | ||||
|         :exception OSError: If the font is not a variation font. | ||||
|  | @ -704,7 +723,9 @@ class FreeTypeFont: | |||
| class TransposedFont: | ||||
|     """Wrapper for writing rotated or mirrored text""" | ||||
| 
 | ||||
|     def __init__(self, font, orientation=None): | ||||
|     def __init__( | ||||
|         self, font: ImageFont | FreeTypeFont, orientation: Image.Transpose | None = None | ||||
|     ): | ||||
|         """ | ||||
|         Wrapper that creates a transposed font from any existing font | ||||
|         object. | ||||
|  | @ -718,13 +739,17 @@ class TransposedFont: | |||
|         self.font = font | ||||
|         self.orientation = orientation  # any 'transpose' argument, or None | ||||
| 
 | ||||
|     def getmask(self, text, mode="", *args, **kwargs): | ||||
|     def getmask( | ||||
|         self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any | ||||
|     ) -> Image.core.ImagingCore: | ||||
|         im = self.font.getmask(text, mode, *args, **kwargs) | ||||
|         if self.orientation is not None: | ||||
|             return im.transpose(self.orientation) | ||||
|         return im | ||||
| 
 | ||||
|     def getbbox(self, text, *args, **kwargs): | ||||
|     def getbbox( | ||||
|         self, text: str | bytes, *args: Any, **kwargs: Any | ||||
|     ) -> tuple[int, int, float, float]: | ||||
|         # TransposedFont doesn't support getmask2, move top-left point to (0, 0) | ||||
|         # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont | ||||
|         left, top, right, bottom = self.font.getbbox(text, *args, **kwargs) | ||||
|  | @ -734,7 +759,7 @@ class TransposedFont: | |||
|             return 0, 0, height, width | ||||
|         return 0, 0, width, height | ||||
| 
 | ||||
|     def getlength(self, text: str | bytes, *args, **kwargs) -> float: | ||||
|     def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float: | ||||
|         if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): | ||||
|             msg = "text length is undefined for text rotated by 90 or 270 degrees" | ||||
|             raise ValueError(msg) | ||||
|  |  | |||
|  | @ -208,7 +208,7 @@ class ImagePalette: | |||
| # Internal | ||||
| 
 | ||||
| 
 | ||||
| def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette: | ||||
| def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette: | ||||
|     palette = ImagePalette() | ||||
|     palette.rawmode = rawmode | ||||
|     palette.palette = data | ||||
|  |  | |||
|  | @ -19,14 +19,23 @@ from __future__ import annotations | |||
| 
 | ||||
| import sys | ||||
| from io import BytesIO | ||||
| from typing import TYPE_CHECKING, Callable | ||||
| from typing import TYPE_CHECKING, Any, Callable, Union | ||||
| 
 | ||||
| from . import Image | ||||
| from ._util import is_path | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     import PyQt6 | ||||
|     import PySide6 | ||||
| 
 | ||||
|     from . import ImageFile | ||||
| 
 | ||||
|     QBuffer: type | ||||
|     QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray] | ||||
|     QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice] | ||||
|     QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] | ||||
|     QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] | ||||
| 
 | ||||
| qt_version: str | None | ||||
| qt_versions = [ | ||||
|     ["6", "PyQt6"], | ||||
|  | @ -37,10 +46,6 @@ qt_versions = [ | |||
| qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) | ||||
| for version, qt_module in qt_versions: | ||||
|     try: | ||||
|         QBuffer: type | ||||
|         QIODevice: type | ||||
|         QImage: type | ||||
|         QPixmap: type | ||||
|         qRgba: Callable[[int, int, int, int], int] | ||||
|         if qt_module == "PyQt6": | ||||
|             from PyQt6.QtCore import QBuffer, QIODevice | ||||
|  | @ -58,26 +63,27 @@ else: | |||
|     qt_version = None | ||||
| 
 | ||||
| 
 | ||||
| def rgb(r, g, b, a=255): | ||||
| def rgb(r: int, g: int, b: int, a: int = 255) -> int: | ||||
|     """(Internal) Turns an RGB color into a Qt compatible color integer.""" | ||||
|     # use qRgb to pack the colors, and then turn the resulting long | ||||
|     # into a negative integer with the same bitpattern. | ||||
|     return qRgba(r, g, b, a) & 0xFFFFFFFF | ||||
| 
 | ||||
| 
 | ||||
| def fromqimage(im): | ||||
| def fromqimage(im: QImage | QPixmap) -> ImageFile.ImageFile: | ||||
|     """ | ||||
|     :param im: QImage or PIL ImageQt object | ||||
|     """ | ||||
|     buffer = QBuffer() | ||||
|     qt_openmode: object | ||||
|     if qt_version == "6": | ||||
|         try: | ||||
|             qt_openmode = QIODevice.OpenModeFlag | ||||
|             qt_openmode = getattr(QIODevice, "OpenModeFlag") | ||||
|         except AttributeError: | ||||
|             qt_openmode = QIODevice.OpenMode | ||||
|             qt_openmode = getattr(QIODevice, "OpenMode") | ||||
|     else: | ||||
|         qt_openmode = QIODevice | ||||
|     buffer.open(qt_openmode.ReadWrite) | ||||
|     buffer.open(getattr(qt_openmode, "ReadWrite")) | ||||
|     # preserve alpha channel with png | ||||
|     # otherwise ppm is more friendly with Image.open | ||||
|     if im.hasAlphaChannel(): | ||||
|  | @ -93,7 +99,7 @@ def fromqimage(im): | |||
|     return Image.open(b) | ||||
| 
 | ||||
| 
 | ||||
| def fromqpixmap(im) -> ImageFile.ImageFile: | ||||
| def fromqpixmap(im: QPixmap) -> ImageFile.ImageFile: | ||||
|     return fromqimage(im) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -123,7 +129,7 @@ def align8to32(bytes: bytes, width: int, mode: str) -> bytes: | |||
|     return b"".join(new_data) | ||||
| 
 | ||||
| 
 | ||||
| def _toqclass_helper(im): | ||||
| def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]: | ||||
|     data = None | ||||
|     colortable = None | ||||
|     exclusive_fp = False | ||||
|  | @ -135,30 +141,32 @@ def _toqclass_helper(im): | |||
|     if is_path(im): | ||||
|         im = Image.open(im) | ||||
|         exclusive_fp = True | ||||
|     assert isinstance(im, Image.Image) | ||||
| 
 | ||||
|     qt_format = QImage.Format if qt_version == "6" else QImage | ||||
|     qt_format = getattr(QImage, "Format") if qt_version == "6" else QImage | ||||
|     if im.mode == "1": | ||||
|         format = qt_format.Format_Mono | ||||
|         format = getattr(qt_format, "Format_Mono") | ||||
|     elif im.mode == "L": | ||||
|         format = qt_format.Format_Indexed8 | ||||
|         format = getattr(qt_format, "Format_Indexed8") | ||||
|         colortable = [rgb(i, i, i) for i in range(256)] | ||||
|     elif im.mode == "P": | ||||
|         format = qt_format.Format_Indexed8 | ||||
|         format = getattr(qt_format, "Format_Indexed8") | ||||
|         palette = im.getpalette() | ||||
|         assert palette is not None | ||||
|         colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)] | ||||
|     elif im.mode == "RGB": | ||||
|         # Populate the 4th channel with 255 | ||||
|         im = im.convert("RGBA") | ||||
| 
 | ||||
|         data = im.tobytes("raw", "BGRA") | ||||
|         format = qt_format.Format_RGB32 | ||||
|         format = getattr(qt_format, "Format_RGB32") | ||||
|     elif im.mode == "RGBA": | ||||
|         data = im.tobytes("raw", "BGRA") | ||||
|         format = qt_format.Format_ARGB32 | ||||
|         format = getattr(qt_format, "Format_ARGB32") | ||||
|     elif im.mode == "I;16": | ||||
|         im = im.point(lambda i: i * 256) | ||||
| 
 | ||||
|         format = qt_format.Format_Grayscale16 | ||||
|         format = getattr(qt_format, "Format_Grayscale16") | ||||
|     else: | ||||
|         if exclusive_fp: | ||||
|             im.close() | ||||
|  | @ -174,8 +182,8 @@ def _toqclass_helper(im): | |||
| 
 | ||||
| if qt_is_installed: | ||||
| 
 | ||||
|     class ImageQt(QImage): | ||||
|         def __init__(self, im) -> None: | ||||
|     class ImageQt(QImage):  # type: ignore[misc] | ||||
|         def __init__(self, im: Image.Image | str | QByteArray) -> None: | ||||
|             """ | ||||
|             An PIL image wrapper for Qt.  This is a subclass of PyQt's QImage | ||||
|             class. | ||||
|  | @ -199,10 +207,10 @@ if qt_is_installed: | |||
|                 self.setColorTable(im_data["colortable"]) | ||||
| 
 | ||||
| 
 | ||||
| def toqimage(im) -> ImageQt: | ||||
| def toqimage(im: Image.Image | str | QByteArray) -> ImageQt: | ||||
|     return ImageQt(im) | ||||
| 
 | ||||
| 
 | ||||
| def toqpixmap(im): | ||||
| def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap: | ||||
|     qimage = toqimage(im) | ||||
|     return QPixmap.fromImage(qimage) | ||||
|     return getattr(QPixmap, "fromImage")(qimage) | ||||
|  |  | |||
|  | @ -127,10 +127,7 @@ class PhotoImage: | |||
|                 # palette mapped data | ||||
|                 image.apply_transparency() | ||||
|                 image.load() | ||||
|                 try: | ||||
|                     mode = image.palette.mode | ||||
|                 except AttributeError: | ||||
|                     mode = "RGB"  # default | ||||
|                 mode = image.palette.mode if image.palette else "RGB" | ||||
|             size = image.size | ||||
|             kw["width"], kw["height"] = size | ||||
| 
 | ||||
|  |  | |||