Merge branch 'main' into build-editable
|  | @ -6,6 +6,7 @@ init: | |||
| # Uncomment previous line to get RDP access during the build. | ||||
| 
 | ||||
| environment: | ||||
|   COVERAGE_CORE: sysmon | ||||
|   EXECUTABLE: python.exe | ||||
|   TEST_OPTIONS: | ||||
|   DEPLOY: YES | ||||
|  |  | |||
							
								
								
									
										1
									
								
								.ci/requirements-mypy.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1 @@ | |||
| mypy==1.8.0 | ||||
							
								
								
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -1 +1 @@ | |||
| tidelift: "pypi/Pillow" | ||||
| tidelift: "pypi/pillow" | ||||
|  |  | |||
							
								
								
									
										2
									
								
								.github/workflows/docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -7,10 +7,12 @@ on: | |||
|     paths: | ||||
|       - ".github/workflows/docs.yml" | ||||
|       - "docs/**" | ||||
|       - "src/PIL/**" | ||||
|   pull_request: | ||||
|     paths: | ||||
|       - ".github/workflows/docs.yml" | ||||
|       - "docs/**" | ||||
|       - "src/PIL/**" | ||||
|   workflow_dispatch: | ||||
| 
 | ||||
| permissions: | ||||
|  |  | |||
							
								
								
									
										3
									
								
								.github/workflows/test-cygwin.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -26,6 +26,9 @@ concurrency: | |||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| env: | ||||
|   COVERAGE_CORE: sysmon | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: windows-latest | ||||
|  |  | |||
							
								
								
									
										5
									
								
								.github/workflows/test-mingw.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -26,6 +26,9 @@ concurrency: | |||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| env: | ||||
|   COVERAGE_CORE: sysmon | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: windows-latest | ||||
|  | @ -64,10 +67,10 @@ jobs: | |||
|               mingw-w64-x86_64-python3-cffi \ | ||||
|               mingw-w64-x86_64-python3-numpy \ | ||||
|               mingw-w64-x86_64-python3-olefile \ | ||||
|               mingw-w64-x86_64-python3-pip \ | ||||
|               mingw-w64-x86_64-python3-setuptools \ | ||||
|               mingw-w64-x86_64-python-pyqt6 | ||||
| 
 | ||||
|           python3 -m ensurepip | ||||
|           python3 -m pip install pyroma pytest pytest-cov pytest-timeout | ||||
| 
 | ||||
|           pushd depends && ./install_extra_test_images.sh && popd | ||||
|  |  | |||
							
								
								
									
										17
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -26,13 +26,16 @@ concurrency: | |||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| env: | ||||
|   COVERAGE_CORE: sysmon | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: windows-latest | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] | ||||
|         python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"] | ||||
| 
 | ||||
|     timeout-minutes: 30 | ||||
| 
 | ||||
|  | @ -66,8 +69,16 @@ jobs: | |||
|     - name: Print build system information | ||||
|       run: python3 .github/workflows/system-info.py | ||||
| 
 | ||||
|     - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma | ||||
|       run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma | ||||
|     - name: Install Python dependencies | ||||
|       run: > | ||||
|         python3 -m pip install | ||||
|         coverage>=7.4.2 | ||||
|         defusedxml | ||||
|         olefile | ||||
|         pyroma | ||||
|         pytest | ||||
|         pytest-cov | ||||
|         pytest-timeout | ||||
| 
 | ||||
|     - name: Install dependencies | ||||
|       id: install | ||||
|  |  | |||
							
								
								
									
										1
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -27,6 +27,7 @@ concurrency: | |||
|   cancel-in-progress: true | ||||
| 
 | ||||
| env: | ||||
|   COVERAGE_CORE: sysmon | ||||
|   FORCE_COLOR: 1 | ||||
| 
 | ||||
| jobs: | ||||
|  |  | |||
							
								
								
									
										31
									
								
								.github/workflows/wheels-dependencies.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -19,7 +19,7 @@ FREETYPE_VERSION=2.13.2 | |||
| HARFBUZZ_VERSION=8.3.0 | ||||
| LIBPNG_VERSION=1.6.40 | ||||
| JPEGTURBO_VERSION=3.0.1 | ||||
| OPENJPEG_VERSION=2.5.0 | ||||
| OPENJPEG_VERSION=2.5.2 | ||||
| XZ_VERSION=5.4.5 | ||||
| TIFF_VERSION=4.6.0 | ||||
| LCMS2_VERSION=2.16 | ||||
|  | @ -40,7 +40,7 @@ BROTLI_VERSION=1.1.0 | |||
| 
 | ||||
| if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then | ||||
|     function build_openjpeg { | ||||
|         local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz) | ||||
|         local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz) | ||||
|         (cd $out_dir \ | ||||
|             && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ | ||||
|             && make install) | ||||
|  | @ -62,7 +62,7 @@ function build_brotli { | |||
| 
 | ||||
| function build { | ||||
|     if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then | ||||
|         export BUILD_PREFIX="/usr/local" | ||||
|         sudo chown -R runner /usr/local | ||||
|     fi | ||||
|     build_xz | ||||
|     if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then | ||||
|  | @ -75,8 +75,8 @@ function build { | |||
|         build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto | ||||
|         build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib | ||||
|         build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist | ||||
|         if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then | ||||
|             cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc | ||||
|         if [[ "$CIBW_ARCHS" == "arm64" ]]; then | ||||
|             cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig | ||||
|         fi | ||||
|     else | ||||
|         sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc | ||||
|  | @ -87,12 +87,10 @@ function build { | |||
|     build_tiff | ||||
|     build_libpng | ||||
|     build_lcms2 | ||||
|     if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then | ||||
|         for dylib in libjpeg.dylib libtiff.dylib liblcms2.dylib; do | ||||
|             cp $BUILD_PREFIX/lib/$dylib /opt/arm64-builds/lib | ||||
|         done | ||||
|     fi | ||||
|     build_openjpeg | ||||
|     if [ -f /usr/local/lib64/libopenjp2.so ]; then | ||||
|         cp /usr/local/lib64/libopenjp2.so /usr/local/lib | ||||
|     fi | ||||
| 
 | ||||
|     ORIGINAL_CFLAGS=$CFLAGS | ||||
|     CFLAGS="$CFLAGS -O3 -DNDEBUG" | ||||
|  | @ -128,14 +126,19 @@ curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-de | |||
| untar pillow-depends-main.zip | ||||
| 
 | ||||
| if [[ -n "$IS_MACOS" ]]; then | ||||
|   # webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb | ||||
|   # libtiff and libxcb cause a conflict with building libtiff and libxcb | ||||
|   # libxau and libxdmcp cause an issue on macOS < 11 | ||||
|   # if php is installed, brew tries to reinstall these after installing openblas | ||||
|   # remove cairo to fix building harfbuzz on arm64 | ||||
|   # remove lcms2 and libpng to fix building openjpeg on arm64 | ||||
|   # remove zstd to avoid inclusion on x86_64 | ||||
|   # remove jpeg-turbo to avoid inclusion on arm64 | ||||
|   # remove webp and zstd to avoid inclusion on x86_64 | ||||
|   # curl from brew requires zstd, use system curl | ||||
|   brew remove --ignore-dependencies webp libpng libtiff libxcb libxau libxdmcp curl php cairo lcms2 ghostscript zstd | ||||
|   brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd | ||||
|   if [[ "$CIBW_ARCHS" == "arm64" ]]; then | ||||
|     brew remove --ignore-dependencies jpeg-turbo | ||||
|   else | ||||
|     brew remove --ignore-dependencies webp | ||||
|   fi | ||||
| 
 | ||||
|   brew install pkg-config | ||||
| fi | ||||
|  |  | |||
							
								
								
									
										3
									
								
								.github/workflows/wheels-test.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -4,6 +4,9 @@ set -e | |||
| if [[ "$OSTYPE" == "darwin"* ]]; then | ||||
|     brew install fribidi | ||||
|     export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" | ||||
|     if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then | ||||
|         sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib | ||||
|     fi | ||||
| elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then | ||||
|     apk add curl fribidi | ||||
| else | ||||
|  |  | |||
							
								
								
									
										4
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -101,7 +101,7 @@ jobs: | |||
|             cibw_arch: x86_64 | ||||
|             macosx_deployment_target: "10.10" | ||||
|           - name: "macOS arm64" | ||||
|             os: macos-latest | ||||
|             os: macos-14 | ||||
|             cibw_arch: arm64 | ||||
|             macosx_deployment_target: "11.0" | ||||
|           - name: "manylinux2014 and musllinux x86_64" | ||||
|  | @ -134,7 +134,7 @@ jobs: | |||
|           CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} | ||||
|           CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} | ||||
|           CIBW_SKIP: pp38-* | ||||
|           CIBW_TEST_SKIP: "*-macosx_arm64" | ||||
|           CIBW_TEST_SKIP: cp38-macosx_arm64 | ||||
|           MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} | ||||
| 
 | ||||
|       - uses: actions/upload-artifact@v4 | ||||
|  |  | |||
							
								
								
									
										15
									
								
								CHANGES.rst
									
									
									
									
									
								
							
							
						
						|  | @ -5,6 +5,21 @@ Changelog (Pillow) | |||
| 10.3.0 (unreleased) | ||||
| ------------------- | ||||
| 
 | ||||
| - Open 16-bit grayscale PNGs as I;16 #7849 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Handle truncated chunks at the end of PNG images #7709 | ||||
|   [lajiyuan, radarhere] | ||||
| 
 | ||||
| - Match mask size to pasted image size in GifImagePlugin #7779 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Release GIL while calling ``WebPAnimDecoderGetNext`` #7782 | ||||
|   [evanmiller, radarhere] | ||||
| 
 | ||||
| - Fixed reading FLI/FLC images with a prefix chunk #7804 | ||||
|   [twolife] | ||||
| 
 | ||||
| - Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745 | ||||
|   [nik012003, radarhere] | ||||
| 
 | ||||
|  |  | |||
|  | @ -64,7 +64,7 @@ As of 2019, Pillow development is | |||
|                 src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"></a> | ||||
|             <a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img | ||||
|                 alt="Tidelift" | ||||
|                 src="https://tidelift.com/badges/package/pypi/Pillow?style=flat"></a> | ||||
|                 src="https://tidelift.com/badges/package/pypi/pillow?style=flat"></a> | ||||
|             <a href="https://pypi.org/project/pillow/"><img | ||||
|                 alt="Newest PyPI version" | ||||
|                 src="https://img.shields.io/pypi/v/pillow.svg"></a> | ||||
|  | @ -82,9 +82,6 @@ As of 2019, Pillow development is | |||
|             <a href="https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img | ||||
|                 alt="Join the chat at https://gitter.im/python-pillow/Pillow" | ||||
|                 src="https://badges.gitter.im/python-pillow/Pillow.svg"></a> | ||||
|             <a href="https://twitter.com/PythonPillow"><img | ||||
|                 alt="Follow on https://twitter.com/PythonPillow" | ||||
|                 src="https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg"></a> | ||||
|             <a href="https://fosstodon.org/@pillow"><img | ||||
|                 alt="Follow on https://fosstodon.org/@pillow" | ||||
|                 src="https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg" | ||||
|  |  | |||
|  | @ -86,7 +86,7 @@ Released as needed privately to individual vendors for critical security-related | |||
| 
 | ||||
| ## Publicize Release | ||||
| 
 | ||||
| * [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 | ||||
| * [ ] Announce release availability via [Mastodon](https://fosstodon.org/@pillow) e.g. https://fosstodon.org/@pillow/110639450470725321 | ||||
| 
 | ||||
| ## Documentation | ||||
| 
 | ||||
|  |  | |||
|  | @ -23,7 +23,10 @@ def _get_mem_usage() -> float: | |||
| 
 | ||||
| 
 | ||||
| def _test_leak( | ||||
|     min_iterations: int, max_iterations: int, fn: Callable[..., None], *args: Any | ||||
|     min_iterations: int, | ||||
|     max_iterations: int, | ||||
|     fn: Callable[..., Image.Image | None], | ||||
|     *args: Any, | ||||
| ) -> None: | ||||
|     mem_limit = None | ||||
|     for i in range(max_iterations): | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ def test_ignore_dos_text() -> None: | |||
|     finally: | ||||
|         ImageFile.LOAD_TRUNCATED_IMAGES = False | ||||
| 
 | ||||
|     assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|     for s in im.text.values(): | ||||
|         assert len(s) < 1024 * 1024, "Text chunk larger than 1M" | ||||
| 
 | ||||
|  | @ -32,6 +33,7 @@ def test_dos_text() -> None: | |||
|         assert msg, "Decompressed Data Too Large" | ||||
|         return | ||||
| 
 | ||||
|     assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|     for s in im.text.values(): | ||||
|         assert len(s) < 1024 * 1024, "Text chunk larger than 1M" | ||||
| 
 | ||||
|  | @ -57,6 +59,7 @@ def test_dos_total_memory() -> None: | |||
|         return | ||||
| 
 | ||||
|     total_len = 0 | ||||
|     assert isinstance(im2, PngImagePlugin.PngImageFile) | ||||
|     for txt in im2.text.values(): | ||||
|         total_len += len(txt) | ||||
|     assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M" | ||||
|  |  | |||
|  | @ -351,7 +351,7 @@ def is_mingw() -> bool: | |||
| 
 | ||||
| 
 | ||||
| class CachedProperty: | ||||
|     def __init__(self, func: Callable[[Any], None]) -> None: | ||||
|     def __init__(self, func: Callable[[Any], Any]) -> None: | ||||
|         self.func = func | ||||
| 
 | ||||
|     def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any: | ||||
|  |  | |||
| Before Width: | Height: | Size: 578 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/16_bit_binary_pgm.tiff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/2422.flc
									
									
									
									
									
										Normal file
									
								
							
							
						
						| Before Width: | Height: | Size: 298 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff
									
									
									
									
									
										Normal file
									
								
							
							
						
						| Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 14 KiB | 
| Before Width: | Height: | Size: 180 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/imagedraw_rectangle_I.tiff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/truncated_end_chunk.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 30 KiB | 
|  | @ -20,7 +20,7 @@ from PIL import _deprecate | |||
|         ), | ||||
|     ], | ||||
| ) | ||||
| def test_version(version, expected) -> None: | ||||
| def test_version(version: int | None, expected: str) -> None: | ||||
|     with pytest.warns(DeprecationWarning, match=expected): | ||||
|         _deprecate.deprecate("Old thing", version, "new thing") | ||||
| 
 | ||||
|  | @ -46,7 +46,7 @@ def test_unknown_version() -> None: | |||
|         ), | ||||
|     ], | ||||
| ) | ||||
| def test_old_version(deprecated, plural, expected) -> None: | ||||
| def test_old_version(deprecated: str, plural: bool, expected: str) -> None: | ||||
|     expected = r"" | ||||
|     with pytest.raises(RuntimeError, match=expected): | ||||
|         _deprecate.deprecate(deprecated, 1, plural=plural) | ||||
|  | @ -76,7 +76,7 @@ def test_replacement_and_action() -> None: | |||
|         "Upgrade to new thing.", | ||||
|     ], | ||||
| ) | ||||
| def test_action(action) -> None: | ||||
| def test_action(action: str) -> None: | ||||
|     expected = ( | ||||
|         r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " | ||||
|         r"Upgrade to new thing\." | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ from __future__ import annotations | |||
| 
 | ||||
| import io | ||||
| import re | ||||
| from typing import Callable | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
|  | @ -29,7 +30,7 @@ def test_version() -> None: | |||
|     # Check the correctness of the convenience function | ||||
|     # and the format of version numbers | ||||
| 
 | ||||
|     def test(name, function) -> None: | ||||
|     def test(name: str, function: Callable[[str], bool]) -> None: | ||||
|         version = features.version(name) | ||||
|         if not features.check(name): | ||||
|             assert version is None | ||||
|  | @ -73,12 +74,12 @@ def test_libimagequant_version() -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("feature", features.modules) | ||||
| def test_check_modules(feature) -> None: | ||||
| def test_check_modules(feature: str) -> None: | ||||
|     assert features.check_module(feature) in [True, False] | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("feature", features.codecs) | ||||
| def test_check_codecs(feature) -> None: | ||||
| def test_check_codecs(feature: str) -> None: | ||||
|     assert features.check_codec(feature) in [True, False] | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -71,7 +71,7 @@ def test_save(tmp_path: Path) -> None: | |||
|         "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", | ||||
|     ], | ||||
| ) | ||||
| def test_crashes(test_file) -> None: | ||||
| def test_crashes(test_file: str) -> None: | ||||
|     with open(test_file, "rb") as f: | ||||
|         with Image.open(f) as im: | ||||
|             with pytest.raises(OSError): | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ from .helper import ( | |||
| 
 | ||||
| 
 | ||||
| def test_sanity(tmp_path: Path) -> None: | ||||
|     def roundtrip(im) -> None: | ||||
|     def roundtrip(im: Image.Image) -> None: | ||||
|         outfile = str(tmp_path / "temp.bmp") | ||||
| 
 | ||||
|         im.save(outfile, "BMP") | ||||
|  | @ -194,7 +194,7 @@ def test_rle4() -> None: | |||
|         ("Tests/images/bmp/g/pal8rle.bmp", 1064), | ||||
|     ), | ||||
| ) | ||||
| def test_rle8_eof(file_name, length) -> None: | ||||
| def test_rle8_eof(file_name: str, length: int) -> None: | ||||
|     with open(file_name, "rb") as fp: | ||||
|         data = fp.read(length) | ||||
|         with Image.open(io.BytesIO(data)) as im: | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| from typing import Literal | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import ContainerIO, Image | ||||
|  | @ -21,9 +23,16 @@ def test_isatty() -> None: | |||
|     assert container.isatty() is False | ||||
| 
 | ||||
| 
 | ||||
| def test_seek_mode_0() -> None: | ||||
| @pytest.mark.parametrize( | ||||
|     "mode, expected_position", | ||||
|     ( | ||||
|         (0, 33), | ||||
|         (1, 66), | ||||
|         (2, 100), | ||||
|     ), | ||||
| ) | ||||
| def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None: | ||||
|     # Arrange | ||||
|     mode = 0 | ||||
|     with open(TEST_FILE, "rb") as fh: | ||||
|         container = ContainerIO.ContainerIO(fh, 22, 100) | ||||
| 
 | ||||
|  | @ -32,35 +41,7 @@ def test_seek_mode_0() -> None: | |||
|         container.seek(33, mode) | ||||
| 
 | ||||
|         # Assert | ||||
|         assert container.tell() == 33 | ||||
| 
 | ||||
| 
 | ||||
| def test_seek_mode_1() -> None: | ||||
|     # Arrange | ||||
|     mode = 1 | ||||
|     with open(TEST_FILE, "rb") as fh: | ||||
|         container = ContainerIO.ContainerIO(fh, 22, 100) | ||||
| 
 | ||||
|         # Act | ||||
|         container.seek(33, mode) | ||||
|         container.seek(33, mode) | ||||
| 
 | ||||
|         # Assert | ||||
|         assert container.tell() == 66 | ||||
| 
 | ||||
| 
 | ||||
| def test_seek_mode_2() -> None: | ||||
|     # Arrange | ||||
|     mode = 2 | ||||
|     with open(TEST_FILE, "rb") as fh: | ||||
|         container = ContainerIO.ContainerIO(fh, 22, 100) | ||||
| 
 | ||||
|         # Act | ||||
|         container.seek(33, mode) | ||||
|         container.seek(33, mode) | ||||
| 
 | ||||
|         # Assert | ||||
|         assert container.tell() == 100 | ||||
|         assert container.tell() == expected_position | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("bytesmode", (True, False)) | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ import warnings | |||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import FliImagePlugin, Image | ||||
| from PIL import FliImagePlugin, Image, ImageFile | ||||
| 
 | ||||
| from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy | ||||
| 
 | ||||
|  | @ -12,9 +12,12 @@ from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy | |||
| # save as...-> hopper.fli, default options. | ||||
| static_test_file = "Tests/images/hopper.fli" | ||||
| 
 | ||||
| # From https://samples.libav.org/fli-flc/ | ||||
| # From https://samples.ffmpeg.org/fli-flc/ | ||||
| animated_test_file = "Tests/images/a.fli" | ||||
| 
 | ||||
| # From https://samples.ffmpeg.org/fli-flc/ | ||||
| animated_test_file_with_prefix_chunk = "Tests/images/2422.flc" | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity() -> None: | ||||
|     with Image.open(static_test_file) as im: | ||||
|  | @ -32,6 +35,24 @@ def test_sanity() -> None: | |||
|         assert im.is_animated | ||||
| 
 | ||||
| 
 | ||||
| def test_prefix_chunk() -> None: | ||||
|     ImageFile.LOAD_TRUNCATED_IMAGES = True | ||||
|     try: | ||||
|         with Image.open(animated_test_file_with_prefix_chunk) as im: | ||||
|             assert im.mode == "P" | ||||
|             assert im.size == (320, 200) | ||||
|             assert im.format == "FLI" | ||||
|             assert im.info["duration"] == 171 | ||||
|             assert im.is_animated | ||||
| 
 | ||||
|             palette = im.getpalette() | ||||
|             assert palette[3:6] == [255, 255, 255] | ||||
|             assert palette[381:384] == [204, 204, 12] | ||||
|             assert palette[765:] == [252, 0, 0] | ||||
|     finally: | ||||
|         ImageFile.LOAD_TRUNCATED_IMAGES = False | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(is_pypy(), reason="Requires CPython") | ||||
| def test_unclosed_file() -> None: | ||||
|     def open() -> None: | ||||
|  |  | |||
|  | @ -1113,6 +1113,21 @@ def test_append_images(tmp_path: Path) -> None: | |||
|         assert reread.n_frames == 10 | ||||
| 
 | ||||
| 
 | ||||
| def test_append_different_size_image(tmp_path: Path) -> None: | ||||
|     out = str(tmp_path / "temp.gif") | ||||
| 
 | ||||
|     im = Image.new("RGB", (100, 100)) | ||||
|     bigger_im = Image.new("RGB", (200, 200), "#f00") | ||||
| 
 | ||||
|     im.save(out, save_all=True, append_images=[bigger_im]) | ||||
| 
 | ||||
|     with Image.open(out) as reread: | ||||
|         assert reread.size == (100, 100) | ||||
| 
 | ||||
|         reread.seek(1) | ||||
|         assert reread.size == (100, 100) | ||||
| 
 | ||||
| 
 | ||||
| def test_transparent_optimize(tmp_path: Path) -> None: | ||||
|     # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses | ||||
|     # transparency. | ||||
|  |  | |||
|  | @ -135,7 +135,7 @@ def test_different_bit_depths(tmp_path: Path) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) | ||||
| def test_save_to_bytes_bmp(mode) -> None: | ||||
| def test_save_to_bytes_bmp(mode: str) -> None: | ||||
|     output = io.BytesIO() | ||||
|     im = hopper(mode) | ||||
|     im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) | ||||
|  |  | |||
|  | @ -82,7 +82,7 @@ def test_eoferror() -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) | ||||
| def test_roundtrip(mode, tmp_path: Path) -> None: | ||||
| def test_roundtrip(mode: str, tmp_path: Path) -> None: | ||||
|     out = str(tmp_path / "temp.im") | ||||
|     im = hopper(mode) | ||||
|     im.save(out) | ||||
|  |  | |||
|  | @ -98,7 +98,7 @@ def test_i() -> None: | |||
|     assert ret == 97 | ||||
| 
 | ||||
| 
 | ||||
| def test_dump(monkeypatch) -> None: | ||||
| def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|     # Arrange | ||||
|     c = b"abc" | ||||
|     # Temporarily redirect stdout | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ import warnings | |||
| from io import BytesIO | ||||
| from pathlib import Path | ||||
| from types import ModuleType | ||||
| from typing import Any | ||||
| from typing import Any, cast | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
|  | @ -45,14 +45,20 @@ TEST_FILE = "Tests/images/hopper.jpg" | |||
| 
 | ||||
| @skip_unless_feature("jpg") | ||||
| class TestFileJpeg: | ||||
|     def roundtrip(self, im: Image.Image, **options: Any) -> Image.Image: | ||||
|     def roundtrip_with_bytes( | ||||
|         self, im: Image.Image, **options: Any | ||||
|     ) -> tuple[JpegImagePlugin.JpegImageFile, int]: | ||||
|         out = BytesIO() | ||||
|         im.save(out, "JPEG", **options) | ||||
|         test_bytes = out.tell() | ||||
|         out.seek(0) | ||||
|         im = Image.open(out) | ||||
|         im.bytes = test_bytes  # for testing only | ||||
|         return im | ||||
|         reloaded = cast(JpegImagePlugin.JpegImageFile, Image.open(out)) | ||||
|         return reloaded, test_bytes | ||||
| 
 | ||||
|     def roundtrip( | ||||
|         self, im: Image.Image, **options: Any | ||||
|     ) -> JpegImagePlugin.JpegImageFile: | ||||
|         return self.roundtrip_with_bytes(im, **options)[0] | ||||
| 
 | ||||
|     def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image: | ||||
|         """Generates a very hard to compress file | ||||
|  | @ -246,13 +252,13 @@ class TestFileJpeg: | |||
|             im.save(f, progressive=True, quality=94, exif=b" " * 43668) | ||||
| 
 | ||||
|     def test_optimize(self) -> None: | ||||
|         im1 = self.roundtrip(hopper()) | ||||
|         im2 = self.roundtrip(hopper(), optimize=0) | ||||
|         im3 = self.roundtrip(hopper(), optimize=1) | ||||
|         im1, im1_bytes = self.roundtrip_with_bytes(hopper()) | ||||
|         im2, im2_bytes = self.roundtrip_with_bytes(hopper(), optimize=0) | ||||
|         im3, im3_bytes = self.roundtrip_with_bytes(hopper(), optimize=1) | ||||
|         assert_image_equal(im1, im2) | ||||
|         assert_image_equal(im1, im3) | ||||
|         assert im1.bytes >= im2.bytes | ||||
|         assert im1.bytes >= im3.bytes | ||||
|         assert im1_bytes >= im2_bytes | ||||
|         assert im1_bytes >= im3_bytes | ||||
| 
 | ||||
|     def test_optimize_large_buffer(self, tmp_path: Path) -> None: | ||||
|         # https://github.com/python-pillow/Pillow/issues/148 | ||||
|  | @ -262,15 +268,15 @@ class TestFileJpeg: | |||
|         im.save(f, format="JPEG", optimize=True) | ||||
| 
 | ||||
|     def test_progressive(self) -> None: | ||||
|         im1 = self.roundtrip(hopper()) | ||||
|         im1, im1_bytes = self.roundtrip_with_bytes(hopper()) | ||||
|         im2 = self.roundtrip(hopper(), progressive=False) | ||||
|         im3 = self.roundtrip(hopper(), progressive=True) | ||||
|         im3, im3_bytes = self.roundtrip_with_bytes(hopper(), progressive=True) | ||||
|         assert not im1.info.get("progressive") | ||||
|         assert not im2.info.get("progressive") | ||||
|         assert im3.info.get("progressive") | ||||
| 
 | ||||
|         assert_image_equal(im1, im3) | ||||
|         assert im1.bytes >= im3.bytes | ||||
|         assert im1_bytes >= im3_bytes | ||||
| 
 | ||||
|     def test_progressive_large_buffer(self, tmp_path: Path) -> None: | ||||
|         f = str(tmp_path / "temp.jpg") | ||||
|  | @ -341,6 +347,7 @@ class TestFileJpeg: | |||
|             assert exif.get_ifd(0x8825) == {} | ||||
| 
 | ||||
|             transposed = ImageOps.exif_transpose(im) | ||||
|         assert transposed is not None | ||||
|         exif = transposed.getexif() | ||||
|         assert exif.get_ifd(0x8825) == {} | ||||
| 
 | ||||
|  | @ -419,14 +426,14 @@ class TestFileJpeg: | |||
|         assert im3.info.get("progression") | ||||
| 
 | ||||
|     def test_quality(self) -> None: | ||||
|         im1 = self.roundtrip(hopper()) | ||||
|         im2 = self.roundtrip(hopper(), quality=50) | ||||
|         im1, im1_bytes = self.roundtrip_with_bytes(hopper()) | ||||
|         im2, im2_bytes = self.roundtrip_with_bytes(hopper(), quality=50) | ||||
|         assert_image(im1, im2.mode, im2.size) | ||||
|         assert im1.bytes >= im2.bytes | ||||
|         assert im1_bytes >= im2_bytes | ||||
| 
 | ||||
|         im3 = self.roundtrip(hopper(), quality=0) | ||||
|         im3, im3_bytes = self.roundtrip_with_bytes(hopper(), quality=0) | ||||
|         assert_image(im1, im3.mode, im3.size) | ||||
|         assert im2.bytes > im3.bytes | ||||
|         assert im2_bytes > im3_bytes | ||||
| 
 | ||||
|     def test_smooth(self) -> None: | ||||
|         im1 = self.roundtrip(hopper()) | ||||
|  |  | |||
|  | @ -40,10 +40,8 @@ test_card.load() | |||
| def roundtrip(im: Image.Image, **options: Any) -> Image.Image: | ||||
|     out = BytesIO() | ||||
|     im.save(out, "JPEG2000", **options) | ||||
|     test_bytes = out.tell() | ||||
|     out.seek(0) | ||||
|     with Image.open(out) as im: | ||||
|         im.bytes = test_bytes  # for testing only | ||||
|         im.load() | ||||
|     return im | ||||
| 
 | ||||
|  | @ -77,7 +75,9 @@ def test_invalid_file() -> None: | |||
| def test_bytesio() -> None: | ||||
|     with open("Tests/images/test-card-lossless.jp2", "rb") as f: | ||||
|         data = BytesIO(f.read()) | ||||
|     assert_image_similar_tofile(test_card, data, 1.0e-3) | ||||
|     with Image.open(data) as im: | ||||
|         im.load() | ||||
|         assert_image_similar(im, test_card, 1.0e-3) | ||||
| 
 | ||||
| 
 | ||||
| # These two test pre-written JPEG 2000 files that were not written with | ||||
|  | @ -340,6 +340,7 @@ def test_parser_feed() -> None: | |||
|     p.feed(data) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert p.image is not None | ||||
|     assert p.image.size == (640, 480) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ from .helper import ( | |||
| 
 | ||||
| @skip_unless_feature("libtiff") | ||||
| class LibTiffTestCase: | ||||
|     def _assert_noerr(self, tmp_path: Path, im: Image.Image) -> None: | ||||
|     def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None: | ||||
|         """Helper tests that assert basic sanity about the g4 tiff reading""" | ||||
|         # 1 bit | ||||
|         assert im.mode == "1" | ||||
|  | @ -524,7 +524,8 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|             im.save(out, compression=compression) | ||||
| 
 | ||||
|     def test_fp_leak(self) -> None: | ||||
|         im = Image.open("Tests/images/hopper_g4_500.tif") | ||||
|         im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif") | ||||
|         assert im is not None | ||||
|         fn = im.fp.fileno() | ||||
| 
 | ||||
|         os.fstat(fn) | ||||
|  | @ -716,6 +717,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|                 f.write(src.read()) | ||||
| 
 | ||||
|         im = Image.open(tmpfile) | ||||
|         assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|         im.n_frames | ||||
|         im.close() | ||||
|         # Should not raise PermissionError. | ||||
|  | @ -1097,6 +1099,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
| 
 | ||||
|         with Image.open(out) as im: | ||||
|             # Assert that there are multiple strips | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             assert len(im.tag_v2[STRIPOFFSETS]) > 1 | ||||
| 
 | ||||
|     @pytest.mark.parametrize("argument", (True, False)) | ||||
|  | @ -1113,6 +1116,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|             im.save(out, **arguments) | ||||
| 
 | ||||
|             with Image.open(out) as im: | ||||
|                 assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|                 assert len(im.tag_v2[STRIPOFFSETS]) == 1 | ||||
|         finally: | ||||
|             TiffImagePlugin.STRIP_SIZE = 65536 | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ def test_valid_file() -> None: | |||
|     # https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8 | ||||
|     # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ | ||||
|     test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara" | ||||
|     saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png" | ||||
|     saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.tiff" | ||||
| 
 | ||||
|     # Act | ||||
|     with Image.open(test_file) as im: | ||||
|  |  | |||
|  | @ -2,11 +2,11 @@ from __future__ import annotations | |||
| 
 | ||||
| import warnings | ||||
| from io import BytesIO | ||||
| from typing import Any | ||||
| from typing import Any, cast | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image | ||||
| from PIL import Image, MpoImagePlugin | ||||
| 
 | ||||
| from .helper import ( | ||||
|     assert_image_equal, | ||||
|  | @ -20,14 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] | |||
| pytestmark = skip_unless_feature("jpg") | ||||
| 
 | ||||
| 
 | ||||
| def roundtrip(im: Image.Image, **options: Any) -> Image.Image: | ||||
| def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile: | ||||
|     out = BytesIO() | ||||
|     im.save(out, "MPO", **options) | ||||
|     test_bytes = out.tell() | ||||
|     out.seek(0) | ||||
|     im = Image.open(out) | ||||
|     im.bytes = test_bytes  # for testing only | ||||
|     return im | ||||
|     return cast(MpoImagePlugin.MpoImageFile, Image.open(out)) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("test_file", test_files) | ||||
|  |  | |||
|  | @ -52,7 +52,7 @@ def test_open_windows_v1() -> None: | |||
|         assert isinstance(im, MspImagePlugin.MspImageFile) | ||||
| 
 | ||||
| 
 | ||||
| def _assert_file_image_equal(source_path, target_path) -> None: | ||||
| def _assert_file_image_equal(source_path: str, target_path: str) -> None: | ||||
|     with Image.open(source_path) as im: | ||||
|         assert_image_equal_tofile(im, target_path) | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ from PIL import Image, ImageFile, PcxImagePlugin | |||
| from .helper import assert_image_equal, hopper | ||||
| 
 | ||||
| 
 | ||||
| def _roundtrip(tmp_path: Path, im) -> None: | ||||
| def _roundtrip(tmp_path: Path, im: Image.Image) -> None: | ||||
|     f = str(tmp_path / "temp.pcx") | ||||
|     im.save(f) | ||||
|     with Image.open(f) as im2: | ||||
|  | @ -44,7 +44,7 @@ def test_invalid_file() -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) | ||||
| def test_odd(tmp_path: Path, mode) -> None: | ||||
| def test_odd(tmp_path: Path, mode: str) -> None: | ||||
|     # See issue #523, odd sized images should have a stride that's even. | ||||
|     # Not that ImageMagick or GIMP write PCX that way. | ||||
|     # We were not handling properly. | ||||
|  | @ -89,7 +89,7 @@ def test_large_count(tmp_path: Path) -> None: | |||
|     _roundtrip(tmp_path, im) | ||||
| 
 | ||||
| 
 | ||||
| def _test_buffer_overflow(tmp_path: Path, im, size: int = 1024) -> None: | ||||
| def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None: | ||||
|     _last = ImageFile.MAXBLOCK | ||||
|     ImageFile.MAXBLOCK = size | ||||
|     try: | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import os.path | |||
| import tempfile | ||||
| import time | ||||
| from pathlib import Path | ||||
| from typing import Any, Generator | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
|  | @ -14,7 +15,7 @@ from PIL import Image, PdfParser, features | |||
| from .helper import hopper, mark_if_feature_version, skip_unless_feature | ||||
| 
 | ||||
| 
 | ||||
| def helper_save_as_pdf(tmp_path: Path, mode, **kwargs): | ||||
| def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: | ||||
|     # Arrange | ||||
|     im = hopper(mode) | ||||
|     outfile = str(tmp_path / ("temp_" + mode + ".pdf")) | ||||
|  | @ -41,13 +42,13 @@ def helper_save_as_pdf(tmp_path: Path, mode, **kwargs): | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) | ||||
| def test_save(tmp_path: Path, mode) -> None: | ||||
| def test_save(tmp_path: Path, mode: str) -> None: | ||||
|     helper_save_as_pdf(tmp_path, mode) | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("jpg_2000") | ||||
| @pytest.mark.parametrize("mode", ("LA", "RGBA")) | ||||
| def test_save_alpha(tmp_path: Path, mode) -> None: | ||||
| def test_save_alpha(tmp_path: Path, mode: str) -> None: | ||||
|     helper_save_as_pdf(tmp_path, mode) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -112,7 +113,7 @@ def test_resolution(tmp_path: Path) -> None: | |||
|         {"dpi": (75, 150), "resolution": 200}, | ||||
|     ), | ||||
| ) | ||||
| def test_dpi(params, tmp_path: Path) -> None: | ||||
| def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: | ||||
|     im = hopper() | ||||
| 
 | ||||
|     outfile = str(tmp_path / "temp.pdf") | ||||
|  | @ -156,7 +157,7 @@ def test_save_all(tmp_path: Path) -> None: | |||
|         assert os.path.getsize(outfile) > 0 | ||||
| 
 | ||||
|         # Test appending using a generator | ||||
|         def im_generator(ims): | ||||
|         def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: | ||||
|             yield from ims | ||||
| 
 | ||||
|         im.save(outfile, save_all=True, append_images=im_generator(ims)) | ||||
|  | @ -226,7 +227,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None: | |||
|             im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) | ||||
| 
 | ||||
| 
 | ||||
| def check_pdf_pages_consistency(pdf) -> None: | ||||
| def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None: | ||||
|     pages_info = pdf.read_indirect(pdf.pages_ref) | ||||
|     assert b"Parent" not in pages_info | ||||
|     assert b"Kids" in pages_info | ||||
|  | @ -339,7 +340,7 @@ def test_pdf_append_to_bytesio() -> None: | |||
| @pytest.mark.timeout(1) | ||||
| @pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") | ||||
| @pytest.mark.parametrize("newline", (b"\r", b"\n")) | ||||
| def test_redos(newline) -> None: | ||||
| def test_redos(newline: bytes) -> None: | ||||
|     malicious = b" trailer<<>>" + newline * 3456 | ||||
| 
 | ||||
|     # This particular exception isn't relevant here. | ||||
|  |  | |||
|  | @ -6,7 +6,8 @@ import warnings | |||
| import zlib | ||||
| from io import BytesIO | ||||
| from pathlib import Path | ||||
| from typing import Any | ||||
| from types import ModuleType | ||||
| from typing import Any, cast | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
|  | @ -23,6 +24,7 @@ from .helper import ( | |||
|     skip_unless_feature, | ||||
| ) | ||||
| 
 | ||||
| ElementTree: ModuleType | None | ||||
| try: | ||||
|     from defusedxml import ElementTree | ||||
| except ImportError: | ||||
|  | @ -57,11 +59,11 @@ def load(data: bytes) -> Image.Image: | |||
|     return Image.open(BytesIO(data)) | ||||
| 
 | ||||
| 
 | ||||
| def roundtrip(im: Image.Image, **options: Any) -> Image.Image: | ||||
| def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile: | ||||
|     out = BytesIO() | ||||
|     im.save(out, "PNG", **options) | ||||
|     out.seek(0) | ||||
|     return Image.open(out) | ||||
|     return cast(PngImagePlugin.PngImageFile, Image.open(out)) | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("zlib") | ||||
|  | @ -100,7 +102,7 @@ class TestFilePng: | |||
|             im = hopper(mode) | ||||
|             im.save(test_file) | ||||
|             with Image.open(test_file) as reloaded: | ||||
|                 if mode in ("I;16", "I;16B"): | ||||
|                 if mode in ("I", "I;16B"): | ||||
|                     reloaded = reloaded.convert(mode) | ||||
|                 assert_image_equal(reloaded, im) | ||||
| 
 | ||||
|  | @ -302,8 +304,8 @@ class TestFilePng: | |||
|         assert im.getcolors() == [(100, (0, 0, 0, 0))] | ||||
| 
 | ||||
|     def test_save_grayscale_transparency(self, tmp_path: Path) -> None: | ||||
|         for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items(): | ||||
|             in_file = "Tests/images/" + mode.lower() + "_trns.png" | ||||
|         for mode, num_transparent in {"1": 1994, "L": 559, "I;16": 559}.items(): | ||||
|             in_file = "Tests/images/" + mode.split(";")[0].lower() + "_trns.png" | ||||
|             with Image.open(in_file) as im: | ||||
|                 assert im.mode == mode | ||||
|                 assert im.info["transparency"] == 255 | ||||
|  | @ -781,6 +783,18 @@ class TestFilePng: | |||
|         with Image.open(mystdout) as reloaded: | ||||
|             assert_image_equal_tofile(reloaded, TEST_PNG_FILE) | ||||
| 
 | ||||
|     def test_truncated_end_chunk(self) -> None: | ||||
|         with Image.open("Tests/images/truncated_end_chunk.png") as im: | ||||
|             with pytest.raises(OSError): | ||||
|                 im.load() | ||||
| 
 | ||||
|         ImageFile.LOAD_TRUNCATED_IMAGES = True | ||||
|         try: | ||||
|             with Image.open("Tests/images/truncated_end_chunk.png") as im: | ||||
|                 assert_image_equal_tofile(im, "Tests/images/hopper.png") | ||||
|         finally: | ||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = False | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") | ||||
| @skip_unless_feature("zlib") | ||||
|  |  | |||
|  | @ -88,7 +88,7 @@ def test_16bit_pgm() -> None: | |||
|         assert im.size == (20, 100) | ||||
|         assert im.get_format_mimetype() == "image/x-portable-graymap" | ||||
| 
 | ||||
|         assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.png") | ||||
|         assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.tiff") | ||||
| 
 | ||||
| 
 | ||||
| def test_16bit_pgm_write(tmp_path: Path) -> None: | ||||
|  |  | |||
|  | @ -157,7 +157,7 @@ def test_combined_larger_than_size() -> None: | |||
|         ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), | ||||
|     ], | ||||
| ) | ||||
| def test_crashes(test_file, raises) -> None: | ||||
| def test_crashes(test_file: str, raises) -> None: | ||||
|     with open(test_file, "rb") as f: | ||||
|         with pytest.raises(raises): | ||||
|             with Image.open(f): | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ import pytest | |||
| 
 | ||||
| from PIL import Image, ImageSequence, SpiderImagePlugin | ||||
| 
 | ||||
| from .helper import assert_image_equal_tofile, hopper, is_pypy | ||||
| from .helper import assert_image_equal, hopper, is_pypy | ||||
| 
 | ||||
| TEST_FILE = "Tests/images/hopper.spider" | ||||
| 
 | ||||
|  | @ -160,4 +160,5 @@ def test_odd_size() -> None: | |||
|     im.save(data, format="SPIDER") | ||||
| 
 | ||||
|     data.seek(0) | ||||
|     assert_image_equal_tofile(im, data) | ||||
|     with Image.open(data) as im2: | ||||
|         assert_image_equal(im, im2) | ||||
|  |  | |||
|  | @ -22,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", _MODES) | ||||
| def test_sanity(mode, tmp_path: Path) -> None: | ||||
|     def roundtrip(original_im) -> None: | ||||
| def test_sanity(mode: str, tmp_path: Path) -> None: | ||||
|     def roundtrip(original_im: Image.Image) -> None: | ||||
|         out = str(tmp_path / "temp.tga") | ||||
| 
 | ||||
|         original_im.save(out, rle=rle) | ||||
|  |  | |||
|  | @ -4,6 +4,8 @@ import os | |||
| import warnings | ||||
| from io import BytesIO | ||||
| from pathlib import Path | ||||
| from types import ModuleType | ||||
| from typing import Generator | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
|  | @ -20,6 +22,7 @@ from .helper import ( | |||
|     is_win32, | ||||
| ) | ||||
| 
 | ||||
| ElementTree: ModuleType | None | ||||
| try: | ||||
|     from defusedxml import ElementTree | ||||
| except ImportError: | ||||
|  | @ -156,7 +159,7 @@ class TestFileTiff: | |||
|         "resolution_unit, dpi", | ||||
|         [(None, 72.8), (2, 72.8), (3, 184.912)], | ||||
|     ) | ||||
|     def test_load_float_dpi(self, resolution_unit, dpi) -> None: | ||||
|     def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None: | ||||
|         with Image.open( | ||||
|             "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" | ||||
|         ) as im: | ||||
|  | @ -284,7 +287,7 @@ class TestFileTiff: | |||
|             ("Tests/images/multipage.tiff", 3), | ||||
|         ), | ||||
|     ) | ||||
|     def test_n_frames(self, path, n_frames) -> None: | ||||
|     def test_n_frames(self, path: str, n_frames: int) -> None: | ||||
|         with Image.open(path) as im: | ||||
|             assert im.n_frames == n_frames | ||||
|             assert im.is_animated == (n_frames != 1) | ||||
|  | @ -402,7 +405,7 @@ class TestFileTiff: | |||
|             assert len_before == len_after + 1 | ||||
| 
 | ||||
|     @pytest.mark.parametrize("legacy_api", (False, True)) | ||||
|     def test_load_byte(self, legacy_api) -> None: | ||||
|     def test_load_byte(self, legacy_api: bool) -> None: | ||||
|         ifd = TiffImagePlugin.ImageFileDirectory_v2() | ||||
|         data = b"abc" | ||||
|         ret = ifd.load_byte(data, legacy_api) | ||||
|  | @ -431,7 +434,7 @@ class TestFileTiff: | |||
|             assert 0x8825 in im.tag_v2 | ||||
| 
 | ||||
|     def test_exif(self, tmp_path: Path) -> None: | ||||
|         def check_exif(exif) -> None: | ||||
|         def check_exif(exif: Image.Exif) -> None: | ||||
|             assert sorted(exif.keys()) == [ | ||||
|                 256, | ||||
|                 257, | ||||
|  | @ -511,7 +514,7 @@ class TestFileTiff: | |||
|             assert im.getexif()[273] == (1408, 1907) | ||||
| 
 | ||||
|     @pytest.mark.parametrize("mode", ("1", "L")) | ||||
|     def test_photometric(self, mode, tmp_path: Path) -> None: | ||||
|     def test_photometric(self, mode: str, tmp_path: Path) -> None: | ||||
|         filename = str(tmp_path / "temp.tif") | ||||
|         im = hopper(mode) | ||||
|         im.save(filename, tiffinfo={262: 0}) | ||||
|  | @ -620,6 +623,7 @@ class TestFileTiff: | |||
|         im.save(outfile, tiffinfo={278: 256}) | ||||
| 
 | ||||
|         with Image.open(outfile) as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             assert im.tag_v2[278] == 256 | ||||
| 
 | ||||
|     def test_strip_raw(self) -> None: | ||||
|  | @ -660,7 +664,7 @@ class TestFileTiff: | |||
|                 assert_image_equal_tofile(reloaded, infile) | ||||
| 
 | ||||
|     @pytest.mark.parametrize("mode", ("P", "PA")) | ||||
|     def test_palette(self, mode, tmp_path: Path) -> None: | ||||
|     def test_palette(self, mode: str, tmp_path: Path) -> None: | ||||
|         outfile = str(tmp_path / "temp.tif") | ||||
| 
 | ||||
|         im = hopper(mode) | ||||
|  | @ -689,7 +693,7 @@ class TestFileTiff: | |||
|             assert reread.n_frames == 3 | ||||
| 
 | ||||
|         # Test appending using a generator | ||||
|         def im_generator(ims): | ||||
|         def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: | ||||
|             yield from ims | ||||
| 
 | ||||
|         mp = BytesIO() | ||||
|  | @ -860,7 +864,7 @@ class TestFileTiff: | |||
|         ], | ||||
|     ) | ||||
|     @pytest.mark.timeout(2) | ||||
|     def test_oom(self, test_file) -> None: | ||||
|     def test_oom(self, test_file: str) -> None: | ||||
|         with pytest.raises(UnidentifiedImageError): | ||||
|             with pytest.warns(UserWarning): | ||||
|                 with Image.open(test_file): | ||||
|  |  | |||
|  | @ -189,7 +189,9 @@ def test_iptc(tmp_path: Path) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) | ||||
| def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: | ||||
| def test_writing_other_types_to_ascii( | ||||
|     value: bytes | int, expected: str, tmp_path: Path | ||||
| ) -> None: | ||||
|     info = TiffImagePlugin.ImageFileDirectory_v2() | ||||
| 
 | ||||
|     tag = TiffTags.TAGS_V2[271] | ||||
|  | @ -206,7 +208,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("value", (1, IFDRational(1))) | ||||
| def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None: | ||||
| def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None: | ||||
|     im = hopper() | ||||
|     info = TiffImagePlugin.ImageFileDirectory_v2() | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,21 +2,18 @@ from __future__ import annotations | |||
| 
 | ||||
| import colorsys | ||||
| import itertools | ||||
| from typing import Callable | ||||
| 
 | ||||
| from PIL import Image | ||||
| 
 | ||||
| from .helper import assert_image_similar, hopper | ||||
| 
 | ||||
| 
 | ||||
| def int_to_float(i): | ||||
| def int_to_float(i: int) -> float: | ||||
|     return i / 255 | ||||
| 
 | ||||
| 
 | ||||
| def str_to_float(i): | ||||
|     return ord(i) / 255 | ||||
| 
 | ||||
| 
 | ||||
| def tuple_to_ints(tp): | ||||
| def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]: | ||||
|     x, y, z = tp | ||||
|     return int(x * 255.0), int(y * 255.0), int(z * 255.0) | ||||
| 
 | ||||
|  | @ -25,7 +22,7 @@ def test_sanity() -> None: | |||
|     Image.new("HSV", (100, 100)) | ||||
| 
 | ||||
| 
 | ||||
| def wedge(): | ||||
| def wedge() -> Image.Image: | ||||
|     w = Image._wedge() | ||||
|     w90 = w.rotate(90) | ||||
| 
 | ||||
|  | @ -49,7 +46,11 @@ def wedge(): | |||
|     return img | ||||
| 
 | ||||
| 
 | ||||
| def to_xxx_colorsys(im, func, mode): | ||||
| def to_xxx_colorsys( | ||||
|     im: Image.Image, | ||||
|     func: Callable[[float, float, float], tuple[float, float, float]], | ||||
|     mode: str, | ||||
| ) -> Image.Image: | ||||
|     # convert the hard way using the library colorsys routines. | ||||
| 
 | ||||
|     (r, g, b) = im.split() | ||||
|  | @ -70,11 +71,11 @@ def to_xxx_colorsys(im, func, mode): | |||
|     return hsv | ||||
| 
 | ||||
| 
 | ||||
| def to_hsv_colorsys(im): | ||||
| def to_hsv_colorsys(im: Image.Image) -> Image.Image: | ||||
|     return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") | ||||
| 
 | ||||
| 
 | ||||
| def to_rgb_colorsys(im): | ||||
| def to_rgb_colorsys(im: Image.Image) -> Image.Image: | ||||
|     return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -138,13 +138,13 @@ class TestImage: | |||
|         assert im.height == 2 | ||||
| 
 | ||||
|         with pytest.raises(AttributeError): | ||||
|             im.size = (3, 4) | ||||
|             im.size = (3, 4)  # type: ignore[misc] | ||||
| 
 | ||||
|     def test_set_mode(self) -> None: | ||||
|         im = Image.new("RGB", (1, 1)) | ||||
| 
 | ||||
|         with pytest.raises(AttributeError): | ||||
|             im.mode = "P" | ||||
|             im.mode = "P"  # type: ignore[misc] | ||||
| 
 | ||||
|     def test_invalid_image(self) -> None: | ||||
|         im = io.BytesIO(b"") | ||||
|  | @ -685,15 +685,18 @@ class TestImage: | |||
|         _make_new(im, blank_p, ImagePalette.ImagePalette()) | ||||
|         _make_new(im, blank_pa, ImagePalette.ImagePalette()) | ||||
| 
 | ||||
|     def test_p_from_rgb_rgba(self) -> None: | ||||
|         for mode, color in [ | ||||
|     @pytest.mark.parametrize( | ||||
|         "mode, color", | ||||
|         ( | ||||
|             ("RGB", "#DDEEFF"), | ||||
|             ("RGB", (221, 238, 255)), | ||||
|             ("RGBA", (221, 238, 255, 255)), | ||||
|         ]: | ||||
|             im = Image.new("P", (100, 100), color) | ||||
|             expected = Image.new(mode, (100, 100), color) | ||||
|             assert_image_equal(im.convert(mode), expected) | ||||
|         ), | ||||
|     ) | ||||
|     def test_p_from_rgb_rgba(self, mode: str, color: str | tuple[int, ...]) -> None: | ||||
|         im = Image.new("P", (100, 100), color) | ||||
|         expected = Image.new(mode, (100, 100), color) | ||||
|         assert_image_equal(im.convert(mode), expected) | ||||
| 
 | ||||
|     def test_no_resource_warning_on_save(self, tmp_path: Path) -> None: | ||||
|         # https://github.com/python-pillow/Pillow/issues/835 | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ from .helper import assert_image_equal, hopper, is_win32 | |||
| 
 | ||||
| # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 | ||||
| # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 | ||||
| cffi: ModuleType | None | ||||
| if os.environ.get("PYTHONOPTIMIZE") == "2": | ||||
|     cffi = None | ||||
| else: | ||||
|  |  | |||
|  | @ -148,9 +148,7 @@ def test_kernel_not_enough_coefficients() -> None: | |||
| @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) | ||||
| def test_consistency_3x3(mode: str) -> None: | ||||
|     with Image.open("Tests/images/hopper.bmp") as source: | ||||
|         reference_name = "hopper_emboss" | ||||
|         reference_name += "_I.png" if mode == "I" else ".bmp" | ||||
|         with Image.open("Tests/images/" + reference_name) as reference: | ||||
|         with Image.open("Tests/images/hopper_emboss.bmp") as reference: | ||||
|             kernel = ImageFilter.Kernel( | ||||
|                 (3, 3), | ||||
|                 # fmt: off | ||||
|  | @ -160,23 +158,13 @@ def test_consistency_3x3(mode: str) -> None: | |||
|                 # fmt: on | ||||
|                 0.3, | ||||
|             ) | ||||
|             source = source.split() * 2 | ||||
|             reference = reference.split() * 2 | ||||
| 
 | ||||
|             if mode == "I": | ||||
|                 source = source[0].convert(mode) | ||||
|             else: | ||||
|                 source = Image.merge(mode, source[: len(mode)]) | ||||
|             reference = Image.merge(mode, reference[: len(mode)]) | ||||
|             assert_image_equal(source.filter(kernel), reference) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) | ||||
| def test_consistency_5x5(mode: str) -> None: | ||||
|     with Image.open("Tests/images/hopper.bmp") as source: | ||||
|         reference_name = "hopper_emboss_more" | ||||
|         reference_name += "_I.png" if mode == "I" else ".bmp" | ||||
|         with Image.open("Tests/images/" + reference_name) as reference: | ||||
|         with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: | ||||
|             kernel = ImageFilter.Kernel( | ||||
|                 (5, 5), | ||||
|                 # fmt: off | ||||
|  | @ -188,14 +176,6 @@ def test_consistency_5x5(mode: str) -> None: | |||
|                 # fmt: on | ||||
|                 0.3, | ||||
|             ) | ||||
|             source = source.split() * 2 | ||||
|             reference = reference.split() * 2 | ||||
| 
 | ||||
|             if mode == "I": | ||||
|                 source = source[0].convert(mode) | ||||
|             else: | ||||
|                 source = Image.merge(mode, source[: len(mode)]) | ||||
|             reference = Image.merge(mode, reference[: len(mode)]) | ||||
|             assert_image_equal(source.filter(kernel), reference) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| import warnings | ||||
| from typing import Generator | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
|  | @ -17,19 +16,16 @@ pytestmark = pytest.mark.skipif( | |||
|     not ImageQt.qt_is_installed, reason="Qt bindings are not installed" | ||||
| ) | ||||
| 
 | ||||
| ims = [ | ||||
|     hopper(), | ||||
|     Image.open("Tests/images/transparent.png"), | ||||
|     Image.open("Tests/images/7x13.png"), | ||||
| ] | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_images() -> Generator[Image.Image, None, None]: | ||||
|     ims = [ | ||||
|         hopper(), | ||||
|         Image.open("Tests/images/transparent.png"), | ||||
|         Image.open("Tests/images/7x13.png"), | ||||
|     ] | ||||
|     try: | ||||
|         yield ims | ||||
|     finally: | ||||
|         for im in ims: | ||||
|             im.close() | ||||
| 
 | ||||
| def teardown_module() -> None: | ||||
|     for im in ims: | ||||
|         im.close() | ||||
| 
 | ||||
| 
 | ||||
| def roundtrip(expected: Image.Image) -> None: | ||||
|  | @ -44,26 +40,26 @@ def roundtrip(expected: Image.Image) -> None: | |||
|         assert_image_equal(result, expected.convert("RGB")) | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity_1(test_images: Generator[Image.Image, None, None]) -> None: | ||||
|     for im in test_images: | ||||
| def test_sanity_1() -> None: | ||||
|     for im in ims: | ||||
|         roundtrip(im.convert("1")) | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity_rgb(test_images: Generator[Image.Image, None, None]) -> None: | ||||
|     for im in test_images: | ||||
| def test_sanity_rgb() -> None: | ||||
|     for im in ims: | ||||
|         roundtrip(im.convert("RGB")) | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity_rgba(test_images: Generator[Image.Image, None, None]) -> None: | ||||
|     for im in test_images: | ||||
| def test_sanity_rgba() -> None: | ||||
|     for im in ims: | ||||
|         roundtrip(im.convert("RGBA")) | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity_l(test_images: Generator[Image.Image, None, None]) -> None: | ||||
|     for im in test_images: | ||||
| def test_sanity_l() -> None: | ||||
|     for im in ims: | ||||
|         roundtrip(im.convert("L")) | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity_p(test_images: Generator[Image.Image, None, None]) -> None: | ||||
|     for im in test_images: | ||||
| def test_sanity_p() -> None: | ||||
|     for im in ims: | ||||
|         roundtrip(im.convert("P")) | ||||
|  |  | |||
|  | @ -8,7 +8,6 @@ from .helper import CachedProperty, assert_image_equal | |||
| 
 | ||||
| 
 | ||||
| class TestImagingPaste: | ||||
|     masks = {} | ||||
|     size = 128 | ||||
| 
 | ||||
|     def assert_9points_image( | ||||
|  | @ -33,7 +32,7 @@ class TestImagingPaste: | |||
|     def assert_9points_paste( | ||||
|         self, | ||||
|         im: Image.Image, | ||||
|         im2: Image.Image, | ||||
|         im2: Image.Image | str | tuple[int, ...], | ||||
|         mask: Image.Image, | ||||
|         expected: list[tuple[int, int, int, int]], | ||||
|     ) -> None: | ||||
|  |  | |||
|  | @ -237,7 +237,7 @@ class TestCoreResampleConsistency: | |||
|         im = Image.new(mode, (512, 9), fill) | ||||
|         return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] | ||||
| 
 | ||||
|     def run_case(self, case: tuple[Image.Image, Image.Image]) -> None: | ||||
|     def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None: | ||||
|         channel, color = case | ||||
|         px = channel.load() | ||||
|         for x in range(channel.size[0]): | ||||
|  |  | |||
|  | @ -154,7 +154,7 @@ class TestImagingCoreResize: | |||
| 
 | ||||
|     def test_unknown_filter(self) -> None: | ||||
|         with pytest.raises(ValueError): | ||||
|             self.resize(hopper(), (10, 10), 9) | ||||
|             self.resize(hopper(), (10, 10), 9)  # type: ignore[arg-type] | ||||
| 
 | ||||
|     def test_cross_platform(self, tmp_path: Path) -> None: | ||||
|         # This test is intended for only check for consistent behaviour across | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| from typing import Callable | ||||
| 
 | ||||
| from PIL import Image, ImageChops | ||||
| 
 | ||||
| from .helper import assert_image_equal, hopper | ||||
|  | @ -387,7 +389,9 @@ def test_overlay() -> None: | |||
| 
 | ||||
| 
 | ||||
| def test_logical() -> None: | ||||
|     def table(op, a, b): | ||||
|     def table( | ||||
|         op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int | ||||
|     ) -> tuple[int, int, int, int]: | ||||
|         out = [] | ||||
|         for x in (a, b): | ||||
|             imx = Image.new("1", (1, 1), x) | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import re | |||
| import shutil | ||||
| from io import BytesIO | ||||
| from pathlib import Path | ||||
| from typing import Any | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
|  | @ -237,7 +238,7 @@ def test_invalid_color_temperature() -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("flag", ("my string", -1)) | ||||
| def test_invalid_flag(flag) -> None: | ||||
| def test_invalid_flag(flag: str | int) -> None: | ||||
|     with hopper() as im: | ||||
|         with pytest.raises( | ||||
|             ImageCms.PyCMSError, match="flags must be an integer between 0 and " | ||||
|  | @ -335,19 +336,21 @@ def test_extended_information() -> None: | |||
|     o = ImageCms.getOpenProfile(SRGB) | ||||
|     p = o.profile | ||||
| 
 | ||||
|     def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None: | ||||
|     def assert_truncated_tuple_equal( | ||||
|         tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10 | ||||
|     ) -> None: | ||||
|         # Helper function to reduce precision of tuples of floats | ||||
|         # recursively and then check equality. | ||||
|         power = 10**digits | ||||
| 
 | ||||
|         def truncate_tuple(tuple_or_float): | ||||
|         def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]: | ||||
|             return tuple( | ||||
|                 ( | ||||
|                     truncate_tuple(val) | ||||
|                     if isinstance(val, tuple) | ||||
|                     else int(val * power) / power | ||||
|                 ) | ||||
|                 for val in tuple_or_float | ||||
|                 for val in tuple_value | ||||
|             ) | ||||
| 
 | ||||
|         assert truncate_tuple(tup1) == truncate_tuple(tup2) | ||||
|  | @ -504,8 +507,10 @@ def test_profile_typesafety() -> None: | |||
|         ImageCms.ImageCmsProfile(1).tobytes() | ||||
| 
 | ||||
| 
 | ||||
| def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None: | ||||
|     def create_test_image(): | ||||
| def assert_aux_channel_preserved( | ||||
|     mode: str, transform_in_place: bool, preserved_channel: str | ||||
| ) -> None: | ||||
|     def create_test_image() -> Image.Image: | ||||
|         # set up test image with something interesting in the tested aux channel. | ||||
|         # fmt: off | ||||
|         nine_grid_deltas = [ | ||||
|  | @ -633,7 +638,7 @@ def test_auxiliary_channels_isolated() -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) | ||||
| def test_rgb_lab(mode) -> None: | ||||
| def test_rgb_lab(mode: str) -> None: | ||||
|     im = Image.new(mode, (1, 1)) | ||||
|     converted_im = im.convert("LAB") | ||||
|     assert converted_im.getpixel((0, 0)) == (0, 128, 128) | ||||
|  |  | |||
|  | @ -753,7 +753,7 @@ def test_rectangle_I16(bbox: Coords) -> None: | |||
|     draw.rectangle(bbox, outline=0xFFFF) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png") | ||||
|     assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_I.tiff") | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("bbox", BBOX) | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import os.path | |||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, ImageDraw, ImageDraw2, features | ||||
| from PIL._typing import Coords | ||||
| 
 | ||||
| from .helper import ( | ||||
|     assert_image_equal, | ||||
|  | @ -56,7 +57,7 @@ def test_sanity() -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("bbox", BBOX) | ||||
| def test_ellipse(bbox) -> None: | ||||
| def test_ellipse(bbox: Coords) -> None: | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw2.Draw(im) | ||||
|  | @ -84,7 +85,7 @@ def test_ellipse_edge() -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("points", POINTS) | ||||
| def test_line(points) -> None: | ||||
| def test_line(points: Coords) -> None: | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw2.Draw(im) | ||||
|  | @ -98,7 +99,7 @@ def test_line(points) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("points", POINTS) | ||||
| def test_line_pen_as_brush(points) -> None: | ||||
| def test_line_pen_as_brush(points: Coords) -> None: | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw2.Draw(im) | ||||
|  | @ -114,7 +115,7 @@ def test_line_pen_as_brush(points) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("points", POINTS) | ||||
| def test_polygon(points) -> None: | ||||
| def test_polygon(points: Coords) -> None: | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw2.Draw(im) | ||||
|  | @ -129,7 +130,7 @@ def test_polygon(points) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("bbox", BBOX) | ||||
| def test_rectangle(bbox) -> None: | ||||
| def test_rectangle(bbox: Coords) -> None: | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw2.Draw(im) | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ def test_crash() -> None: | |||
|     ImageEnhance.Sharpness(im).enhance(0.5) | ||||
| 
 | ||||
| 
 | ||||
| def _half_transparent_image(): | ||||
| def _half_transparent_image() -> Image.Image: | ||||
|     # returns an image, half transparent, half solid | ||||
|     im = hopper("RGB") | ||||
| 
 | ||||
|  | @ -34,7 +34,9 @@ def _half_transparent_image(): | |||
|     return im | ||||
| 
 | ||||
| 
 | ||||
| def _check_alpha(im, original, op, amount) -> None: | ||||
| def _check_alpha( | ||||
|     im: Image.Image, original: Image.Image, op: str, amount: float | ||||
| ) -> None: | ||||
|     assert im.getbands() == original.getbands() | ||||
|     assert_image_equal( | ||||
|         im.getchannel("A"), | ||||
|  | @ -44,7 +46,7 @@ def _check_alpha(im, original, op, amount) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) | ||||
| def test_alpha(op) -> None: | ||||
| def test_alpha(op: str) -> None: | ||||
|     # Issue https://github.com/python-pillow/Pillow/issues/899 | ||||
|     # Is alpha preserved through image enhancement? | ||||
| 
 | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ SAFEBLOCK = ImageFile.SAFEBLOCK | |||
| 
 | ||||
| class TestImageFile: | ||||
|     def test_parser(self) -> None: | ||||
|         def roundtrip(format): | ||||
|         def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: | ||||
|             im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) | ||||
|             if format in ("MSP", "XBM"): | ||||
|                 im = im.convert("1") | ||||
|  |  | |||
|  | @ -7,11 +7,13 @@ import shutil | |||
| import sys | ||||
| from io import BytesIO | ||||
| from pathlib import Path | ||||
| from typing import Any, BinaryIO | ||||
| 
 | ||||
| import pytest | ||||
| from packaging.version import parse as parse_version | ||||
| 
 | ||||
| from PIL import Image, ImageDraw, ImageFont, features | ||||
| from PIL._typing import StrOrBytesPath | ||||
| 
 | ||||
| from .helper import ( | ||||
|     assert_image_equal, | ||||
|  | @ -42,16 +44,16 @@ def test_sanity() -> None: | |||
|         pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), | ||||
|     ], | ||||
| ) | ||||
| def layout_engine(request): | ||||
| def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout: | ||||
|     return request.param | ||||
| 
 | ||||
| 
 | ||||
| @pytest.fixture(scope="module") | ||||
| def font(layout_engine): | ||||
| def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont: | ||||
|     return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) | ||||
| 
 | ||||
| 
 | ||||
| def test_font_properties(font) -> None: | ||||
| def test_font_properties(font: ImageFont.FreeTypeFont) -> None: | ||||
|     assert font.path == FONT_PATH | ||||
|     assert font.size == FONT_SIZE | ||||
| 
 | ||||
|  | @ -67,7 +69,9 @@ def test_font_properties(font) -> None: | |||
|     assert font_copy.path == second_font_path | ||||
| 
 | ||||
| 
 | ||||
| def _render(font, layout_engine): | ||||
| def _render( | ||||
|     font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout | ||||
| ) -> Image.Image: | ||||
|     txt = "Hello World!" | ||||
|     ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) | ||||
|     ttf.getbbox(txt) | ||||
|  | @ -80,12 +84,12 @@ def _render(font, layout_engine): | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH))) | ||||
| def test_font_with_name(layout_engine, font) -> None: | ||||
| def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None: | ||||
|     _render(font, layout_engine) | ||||
| 
 | ||||
| 
 | ||||
| def test_font_with_filelike(layout_engine) -> None: | ||||
|     def _font_as_bytes(): | ||||
| def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None: | ||||
|     def _font_as_bytes() -> BytesIO: | ||||
|         with open(FONT_PATH, "rb") as f: | ||||
|             font_bytes = BytesIO(f.read()) | ||||
|         return font_bytes | ||||
|  | @ -102,12 +106,12 @@ def test_font_with_filelike(layout_engine) -> None: | |||
|     #   _render(shared_bytes) | ||||
| 
 | ||||
| 
 | ||||
| def test_font_with_open_file(layout_engine) -> None: | ||||
| def test_font_with_open_file(layout_engine: ImageFont.Layout) -> None: | ||||
|     with open(FONT_PATH, "rb") as f: | ||||
|         _render(f, layout_engine) | ||||
| 
 | ||||
| 
 | ||||
| def test_render_equal(layout_engine) -> None: | ||||
| def test_render_equal(layout_engine: ImageFont.Layout) -> None: | ||||
|     img_path = _render(FONT_PATH, layout_engine) | ||||
|     with open(FONT_PATH, "rb") as f: | ||||
|         font_filelike = BytesIO(f.read()) | ||||
|  | @ -116,7 +120,7 @@ def test_render_equal(layout_engine) -> None: | |||
|     assert_image_equal(img_path, img_filelike) | ||||
| 
 | ||||
| 
 | ||||
| def test_non_ascii_path(tmp_path: Path, layout_engine) -> None: | ||||
| def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: | ||||
|     tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) | ||||
|     try: | ||||
|         shutil.copy(FONT_PATH, tempfile) | ||||
|  | @ -126,7 +130,7 @@ def test_non_ascii_path(tmp_path: Path, layout_engine) -> None: | |||
|     ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) | ||||
| 
 | ||||
| 
 | ||||
| def test_transparent_background(font) -> None: | ||||
| def test_transparent_background(font: ImageFont.FreeTypeFont) -> None: | ||||
|     im = Image.new(mode="RGBA", size=(300, 100)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|  | @ -140,7 +144,7 @@ def test_transparent_background(font) -> None: | |||
|     assert_image_similar_tofile(im.convert("L"), target, 0.01) | ||||
| 
 | ||||
| 
 | ||||
| def test_I16(font) -> None: | ||||
| def test_I16(font: ImageFont.FreeTypeFont) -> None: | ||||
|     im = Image.new(mode="I;16", size=(300, 100)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|  | @ -153,7 +157,7 @@ def test_I16(font) -> None: | |||
|     assert_image_similar_tofile(im.convert("L"), target, 0.01) | ||||
| 
 | ||||
| 
 | ||||
| def test_textbbox_equal(font) -> None: | ||||
| def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None: | ||||
|     im = Image.new(mode="RGB", size=(300, 100)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|  | @ -181,7 +185,13 @@ def test_textbbox_equal(font) -> None: | |||
|     ), | ||||
| ) | ||||
| def test_getlength( | ||||
|     text, mode, fontname, size, layout_engine, length_basic, length_raqm | ||||
|     text: str, | ||||
|     mode: str, | ||||
|     fontname: str, | ||||
|     size: int, | ||||
|     layout_engine: ImageFont.Layout, | ||||
|     length_basic: int, | ||||
|     length_raqm: float, | ||||
| ) -> None: | ||||
|     f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) | ||||
| 
 | ||||
|  | @ -207,7 +217,7 @@ def test_float_size() -> None: | |||
|     assert lengths[0] != lengths[1] != lengths[2] | ||||
| 
 | ||||
| 
 | ||||
| def test_render_multiline(font) -> None: | ||||
| def test_render_multiline(font: ImageFont.FreeTypeFont) -> None: | ||||
|     im = Image.new(mode="RGB", size=(300, 100)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     line_spacing = font.getbbox("A")[3] + 4 | ||||
|  | @ -223,7 +233,7 @@ def test_render_multiline(font) -> None: | |||
|     assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) | ||||
| 
 | ||||
| 
 | ||||
| def test_render_multiline_text(font) -> None: | ||||
| def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: | ||||
|     # Test that text() correctly connects to multiline_text() | ||||
|     # and that align defaults to left | ||||
|     im = Image.new(mode="RGB", size=(300, 100)) | ||||
|  | @ -243,7 +253,9 @@ def test_render_multiline_text(font) -> None: | |||
| @pytest.mark.parametrize( | ||||
|     "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) | ||||
| ) | ||||
| def test_render_multiline_text_align(font, align, ext) -> None: | ||||
| def test_render_multiline_text_align( | ||||
|     font: ImageFont.FreeTypeFont, align: str, ext: str | ||||
| ) -> None: | ||||
|     im = Image.new(mode="RGB", size=(300, 100)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) | ||||
|  | @ -251,7 +263,7 @@ def test_render_multiline_text_align(font, align, ext) -> None: | |||
|     assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) | ||||
| 
 | ||||
| 
 | ||||
| def test_unknown_align(font) -> None: | ||||
| def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: | ||||
|     im = Image.new(mode="RGB", size=(300, 100)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|  | @ -260,14 +272,14 @@ def test_unknown_align(font) -> None: | |||
|         draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") | ||||
| 
 | ||||
| 
 | ||||
| def test_draw_align(font) -> None: | ||||
| def test_draw_align(font: ImageFont.FreeTypeFont) -> None: | ||||
|     im = Image.new("RGB", (300, 100), "white") | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     line = "some text" | ||||
|     draw.text((100, 40), line, (0, 0, 0), font=font, align="left") | ||||
| 
 | ||||
| 
 | ||||
| def test_multiline_bbox(font) -> None: | ||||
| def test_multiline_bbox(font: ImageFont.FreeTypeFont) -> None: | ||||
|     im = Image.new(mode="RGB", size=(300, 100)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|  | @ -285,7 +297,7 @@ def test_multiline_bbox(font) -> None: | |||
|     draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) | ||||
| 
 | ||||
| 
 | ||||
| def test_multiline_width(font) -> None: | ||||
| def test_multiline_width(font: ImageFont.FreeTypeFont) -> None: | ||||
|     im = Image.new(mode="RGB", size=(300, 100)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|  | @ -295,7 +307,7 @@ def test_multiline_width(font) -> None: | |||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def test_multiline_spacing(font) -> None: | ||||
| def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None: | ||||
|     im = Image.new(mode="RGB", size=(300, 100)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) | ||||
|  | @ -306,7 +318,9 @@ def test_multiline_spacing(font) -> None: | |||
| @pytest.mark.parametrize( | ||||
|     "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) | ||||
| ) | ||||
| def test_rotated_transposed_font(font, orientation) -> None: | ||||
| def test_rotated_transposed_font( | ||||
|     font: ImageFont.FreeTypeFont, orientation: Image.Transpose | ||||
| ) -> None: | ||||
|     img_gray = Image.new("L", (100, 100)) | ||||
|     draw = ImageDraw.Draw(img_gray) | ||||
|     word = "testing" | ||||
|  | @ -347,7 +361,9 @@ def test_rotated_transposed_font(font, orientation) -> None: | |||
|         Image.Transpose.FLIP_TOP_BOTTOM, | ||||
|     ), | ||||
| ) | ||||
| def test_unrotated_transposed_font(font, orientation) -> None: | ||||
| def test_unrotated_transposed_font( | ||||
|     font: ImageFont.FreeTypeFont, orientation: Image.Transpose | ||||
| ) -> None: | ||||
|     img_gray = Image.new("L", (100, 100)) | ||||
|     draw = ImageDraw.Draw(img_gray) | ||||
|     word = "testing" | ||||
|  | @ -382,7 +398,9 @@ def test_unrotated_transposed_font(font, orientation) -> None: | |||
| @pytest.mark.parametrize( | ||||
|     "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) | ||||
| ) | ||||
| def test_rotated_transposed_font_get_mask(font, orientation) -> None: | ||||
| def test_rotated_transposed_font_get_mask( | ||||
|     font: ImageFont.FreeTypeFont, orientation: Image.Transpose | ||||
| ) -> None: | ||||
|     # Arrange | ||||
|     text = "mask this" | ||||
|     transposed_font = ImageFont.TransposedFont(font, orientation=orientation) | ||||
|  | @ -403,7 +421,9 @@ def test_rotated_transposed_font_get_mask(font, orientation) -> None: | |||
|         Image.Transpose.FLIP_TOP_BOTTOM, | ||||
|     ), | ||||
| ) | ||||
| def test_unrotated_transposed_font_get_mask(font, orientation) -> None: | ||||
| def test_unrotated_transposed_font_get_mask( | ||||
|     font: ImageFont.FreeTypeFont, orientation: Image.Transpose | ||||
| ) -> None: | ||||
|     # Arrange | ||||
|     text = "mask this" | ||||
|     transposed_font = ImageFont.TransposedFont(font, orientation=orientation) | ||||
|  | @ -415,11 +435,11 @@ def test_unrotated_transposed_font_get_mask(font, orientation) -> None: | |||
|     assert mask.size == (108, 13) | ||||
| 
 | ||||
| 
 | ||||
| def test_free_type_font_get_name(font) -> None: | ||||
| def test_free_type_font_get_name(font: ImageFont.FreeTypeFont) -> None: | ||||
|     assert ("FreeMono", "Regular") == font.getname() | ||||
| 
 | ||||
| 
 | ||||
| def test_free_type_font_get_metrics(font) -> None: | ||||
| def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None: | ||||
|     ascent, descent = font.getmetrics() | ||||
| 
 | ||||
|     assert isinstance(ascent, int) | ||||
|  | @ -427,7 +447,7 @@ def test_free_type_font_get_metrics(font) -> None: | |||
|     assert (ascent, descent) == (16, 4) | ||||
| 
 | ||||
| 
 | ||||
| def test_free_type_font_get_mask(font) -> None: | ||||
| def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: | ||||
|     # Arrange | ||||
|     text = "mask this" | ||||
| 
 | ||||
|  | @ -473,16 +493,16 @@ def test_default_font() -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", (None, "1", "RGBA")) | ||||
| def test_getbbox(font, mode) -> None: | ||||
| def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None: | ||||
|     assert (0, 4, 12, 16) == font.getbbox("A", mode) | ||||
| 
 | ||||
| 
 | ||||
| def test_getbbox_empty(font) -> None: | ||||
| def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None: | ||||
|     # issue #2614, should not crash. | ||||
|     assert (0, 0, 0, 0) == font.getbbox("") | ||||
| 
 | ||||
| 
 | ||||
| def test_render_empty(font) -> None: | ||||
| def test_render_empty(font: ImageFont.FreeTypeFont) -> None: | ||||
|     # issue 2666 | ||||
|     im = Image.new(mode="RGB", size=(300, 100)) | ||||
|     target = im.copy() | ||||
|  | @ -492,7 +512,7 @@ def test_render_empty(font) -> None: | |||
|     assert_image_equal(im, target) | ||||
| 
 | ||||
| 
 | ||||
| def test_unicode_extended(layout_engine) -> None: | ||||
| def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: | ||||
|     # issue #3777 | ||||
|     text = "A\u278A\U0001F12B" | ||||
|     target = "Tests/images/unicode_extended.png" | ||||
|  | @ -515,21 +535,23 @@ def test_unicode_extended(layout_engine) -> None: | |||
|     (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), | ||||
| ) | ||||
| @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") | ||||
| def test_find_font(monkeypatch, platform, font_directory) -> None: | ||||
|     def _test_fake_loading_font(path_to_fake, fontname) -> None: | ||||
| def test_find_font( | ||||
|     monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str | ||||
| ) -> None: | ||||
|     def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: | ||||
|         # Make a copy of FreeTypeFont so we can patch the original | ||||
|         free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) | ||||
|         with monkeypatch.context() as m: | ||||
|             m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) | ||||
| 
 | ||||
|             def loadable_font(filepath, size, index, encoding, *args, **kwargs): | ||||
|             def loadable_font( | ||||
|                 filepath: str, size: int, index: int, encoding: str, *args: Any | ||||
|             ): | ||||
|                 if filepath == path_to_fake: | ||||
|                     return ImageFont._FreeTypeFont( | ||||
|                         FONT_PATH, size, index, encoding, *args, **kwargs | ||||
|                         FONT_PATH, size, index, encoding, *args | ||||
|                     ) | ||||
|                 return ImageFont._FreeTypeFont( | ||||
|                     filepath, size, index, encoding, *args, **kwargs | ||||
|                 ) | ||||
|                 return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args) | ||||
| 
 | ||||
|             m.setattr(ImageFont, "FreeTypeFont", loadable_font) | ||||
|             font = ImageFont.truetype(fontname) | ||||
|  | @ -543,7 +565,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None: | |||
|     if platform == "linux": | ||||
|         monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") | ||||
| 
 | ||||
|     def fake_walker(path): | ||||
|     def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]: | ||||
|         if path == font_directory: | ||||
|             return [ | ||||
|                 ( | ||||
|  | @ -567,7 +589,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None: | |||
|     _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") | ||||
| 
 | ||||
| 
 | ||||
| def test_imagefont_getters(font) -> None: | ||||
| def test_imagefont_getters(font: ImageFont.FreeTypeFont) -> None: | ||||
|     assert font.getmetrics() == (16, 4) | ||||
|     assert font.font.ascent == 16 | ||||
|     assert font.font.descent == 4 | ||||
|  | @ -588,7 +610,7 @@ def test_imagefont_getters(font) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("stroke_width", (0, 2)) | ||||
| def test_getsize_stroke(font, stroke_width) -> None: | ||||
| def test_getsize_stroke(font: ImageFont.FreeTypeFont, stroke_width: int) -> None: | ||||
|     assert font.getbbox("A", stroke_width=stroke_width) == ( | ||||
|         0 - stroke_width, | ||||
|         4 - stroke_width, | ||||
|  | @ -607,7 +629,7 @@ def test_complex_font_settings() -> None: | |||
|         t.getmask("абвг", language="sr") | ||||
| 
 | ||||
| 
 | ||||
| def test_variation_get(font) -> None: | ||||
| def test_variation_get(font: ImageFont.FreeTypeFont) -> None: | ||||
|     freetype = parse_version(features.version_module("freetype2")) | ||||
|     if freetype < parse_version("2.9.1"): | ||||
|         with pytest.raises(NotImplementedError): | ||||
|  | @ -662,7 +684,7 @@ def test_variation_get(font) -> None: | |||
|     ] | ||||
| 
 | ||||
| 
 | ||||
| def _check_text(font, path, epsilon): | ||||
| def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None: | ||||
|     im = Image.new("RGB", (100, 75), "white") | ||||
|     d = ImageDraw.Draw(im) | ||||
|     d.text((10, 10), "Text", font=font, fill="black") | ||||
|  | @ -677,7 +699,7 @@ def _check_text(font, path, epsilon): | |||
|             raise | ||||
| 
 | ||||
| 
 | ||||
| def test_variation_set_by_name(font) -> None: | ||||
| def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: | ||||
|     freetype = parse_version(features.version_module("freetype2")) | ||||
|     if freetype < parse_version("2.9.1"): | ||||
|         with pytest.raises(NotImplementedError): | ||||
|  | @ -702,7 +724,7 @@ def test_variation_set_by_name(font) -> None: | |||
|     _check_text(font, "Tests/images/variation_tiny_name.png", 40) | ||||
| 
 | ||||
| 
 | ||||
| def test_variation_set_by_axes(font) -> None: | ||||
| def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: | ||||
|     freetype = parse_version(features.version_module("freetype2")) | ||||
|     if freetype < parse_version("2.9.1"): | ||||
|         with pytest.raises(NotImplementedError): | ||||
|  | @ -737,7 +759,9 @@ def test_variation_set_by_axes(font) -> None: | |||
|     ), | ||||
|     ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), | ||||
| ) | ||||
| def test_anchor(layout_engine, anchor, left, top) -> None: | ||||
| def test_anchor( | ||||
|     layout_engine: ImageFont.Layout, anchor: str, left: int, top: int | ||||
| ) -> None: | ||||
|     name, text = "quick", "Quick" | ||||
|     path = f"Tests/images/test_anchor_{name}_{anchor}.png" | ||||
| 
 | ||||
|  | @ -782,7 +806,9 @@ def test_anchor(layout_engine, anchor, left, top) -> None: | |||
|         ("md", "center"), | ||||
|     ), | ||||
| ) | ||||
| def test_anchor_multiline(layout_engine, anchor, align) -> None: | ||||
| def test_anchor_multiline( | ||||
|     layout_engine: ImageFont.Layout, anchor: str, align: str | ||||
| ) -> None: | ||||
|     target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" | ||||
|     text = "a\nlong\ntext sample" | ||||
| 
 | ||||
|  | @ -800,7 +826,7 @@ def test_anchor_multiline(layout_engine, anchor, align) -> None: | |||
|     assert_image_similar_tofile(im, target, 4) | ||||
| 
 | ||||
| 
 | ||||
| def test_anchor_invalid(font) -> None: | ||||
| def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None: | ||||
|     im = Image.new("RGB", (100, 100), "white") | ||||
|     d = ImageDraw.Draw(im) | ||||
|     d.font = font | ||||
|  | @ -826,7 +852,7 @@ def test_anchor_invalid(font) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) | ||||
| def test_bitmap_font(layout_engine, bpp) -> None: | ||||
| def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None: | ||||
|     text = "Bitmap Font" | ||||
|     layout_name = ["basic", "raqm"][layout_engine] | ||||
|     target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" | ||||
|  | @ -843,7 +869,7 @@ def test_bitmap_font(layout_engine, bpp) -> None: | |||
|     assert_image_equal_tofile(im, target) | ||||
| 
 | ||||
| 
 | ||||
| def test_bitmap_font_stroke(layout_engine) -> None: | ||||
| def test_bitmap_font_stroke(layout_engine: ImageFont.Layout) -> None: | ||||
|     text = "Bitmap Font" | ||||
|     layout_name = ["basic", "raqm"][layout_engine] | ||||
|     target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" | ||||
|  | @ -861,7 +887,7 @@ def test_bitmap_font_stroke(layout_engine) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("embedded_color", (False, True)) | ||||
| def test_bitmap_blend(layout_engine, embedded_color) -> None: | ||||
| def test_bitmap_blend(layout_engine: ImageFont.Layout, embedded_color: bool) -> None: | ||||
|     font = ImageFont.truetype( | ||||
|         "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine | ||||
|     ) | ||||
|  | @ -873,7 +899,7 @@ def test_bitmap_blend(layout_engine, embedded_color) -> None: | |||
|     assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png") | ||||
| 
 | ||||
| 
 | ||||
| def test_standard_embedded_color(layout_engine) -> None: | ||||
| def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: | ||||
|     txt = "Hello World!" | ||||
|     ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) | ||||
|     ttf.getbbox(txt) | ||||
|  | @ -886,7 +912,7 @@ def test_standard_embedded_color(layout_engine) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("fontmode", ("1", "L", "RGBA")) | ||||
| def test_float_coord(layout_engine, fontmode): | ||||
| def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None: | ||||
|     txt = "Hello World!" | ||||
|     ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) | ||||
| 
 | ||||
|  | @ -908,7 +934,7 @@ def test_float_coord(layout_engine, fontmode): | |||
|             raise | ||||
| 
 | ||||
| 
 | ||||
| def test_cbdt(layout_engine) -> None: | ||||
| def test_cbdt(layout_engine: ImageFont.Layout) -> None: | ||||
|     try: | ||||
|         font = ImageFont.truetype( | ||||
|             "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine | ||||
|  | @ -925,7 +951,7 @@ def test_cbdt(layout_engine) -> None: | |||
|         pytest.skip("freetype compiled without libpng or CBDT support") | ||||
| 
 | ||||
| 
 | ||||
| def test_cbdt_mask(layout_engine) -> None: | ||||
| def test_cbdt_mask(layout_engine: ImageFont.Layout) -> None: | ||||
|     try: | ||||
|         font = ImageFont.truetype( | ||||
|             "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine | ||||
|  | @ -942,7 +968,7 @@ def test_cbdt_mask(layout_engine) -> None: | |||
|         pytest.skip("freetype compiled without libpng or CBDT support") | ||||
| 
 | ||||
| 
 | ||||
| def test_sbix(layout_engine) -> None: | ||||
| def test_sbix(layout_engine: ImageFont.Layout) -> None: | ||||
|     try: | ||||
|         font = ImageFont.truetype( | ||||
|             "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine | ||||
|  | @ -959,7 +985,7 @@ def test_sbix(layout_engine) -> None: | |||
|         pytest.skip("freetype compiled without libpng or SBIX support") | ||||
| 
 | ||||
| 
 | ||||
| def test_sbix_mask(layout_engine) -> None: | ||||
| def test_sbix_mask(layout_engine: ImageFont.Layout) -> None: | ||||
|     try: | ||||
|         font = ImageFont.truetype( | ||||
|             "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine | ||||
|  | @ -977,7 +1003,7 @@ def test_sbix_mask(layout_engine) -> None: | |||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature_version("freetype2", "2.10.0") | ||||
| def test_colr(layout_engine) -> None: | ||||
| def test_colr(layout_engine: ImageFont.Layout) -> None: | ||||
|     font = ImageFont.truetype( | ||||
|         "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", | ||||
|         size=64, | ||||
|  | @ -993,7 +1019,7 @@ def test_colr(layout_engine) -> None: | |||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature_version("freetype2", "2.10.0") | ||||
| def test_colr_mask(layout_engine) -> None: | ||||
| def test_colr_mask(layout_engine: ImageFont.Layout) -> None: | ||||
|     font = ImageFont.truetype( | ||||
|         "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", | ||||
|         size=64, | ||||
|  | @ -1008,7 +1034,7 @@ def test_colr_mask(layout_engine) -> None: | |||
|     assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) | ||||
| 
 | ||||
| 
 | ||||
| def test_woff2(layout_engine) -> None: | ||||
| def test_woff2(layout_engine: ImageFont.Layout) -> None: | ||||
|     try: | ||||
|         font = ImageFont.truetype( | ||||
|             "Tests/fonts/OpenSans.woff2", | ||||
|  | @ -1042,7 +1068,7 @@ def test_render_mono_size() -> None: | |||
|     assert_image_equal_tofile(im, "Tests/images/text_mono.gif") | ||||
| 
 | ||||
| 
 | ||||
| def test_too_many_characters(font) -> None: | ||||
| def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None: | ||||
|     with pytest.raises(ValueError): | ||||
|         font.getlength("A" * 1_000_001) | ||||
|     with pytest.raises(ValueError): | ||||
|  | @ -1070,14 +1096,14 @@ def test_too_many_characters(font) -> None: | |||
|         "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", | ||||
|     ], | ||||
| ) | ||||
| def test_oom(test_file) -> None: | ||||
| def test_oom(test_file: str) -> None: | ||||
|     with open(test_file, "rb") as f: | ||||
|         font = ImageFont.truetype(BytesIO(f.read())) | ||||
|         with pytest.raises(Image.DecompressionBombError): | ||||
|             font.getmask("Test Text") | ||||
| 
 | ||||
| 
 | ||||
| def test_raqm_missing_warning(monkeypatch) -> None: | ||||
| def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|     monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) | ||||
|     with pytest.warns(UserWarning) as record: | ||||
|         font = ImageFont.truetype( | ||||
|  | @ -1091,6 +1117,8 @@ def test_raqm_missing_warning(monkeypatch) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("size", [-1, 0]) | ||||
| def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size) -> None: | ||||
| def test_invalid_truetype_sizes_raise_valueerror( | ||||
|     layout_engine: ImageFont.Layout, size: int | ||||
| ) -> None: | ||||
|     with pytest.raises(ValueError): | ||||
|         ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) | ||||
|  |  | |||
|  | @ -84,6 +84,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200 | |||
|     @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") | ||||
|     def test_grabclipboard_file(self) -> None: | ||||
|         p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) | ||||
|         assert p.stdin is not None | ||||
|         p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') | ||||
|         p.communicate() | ||||
| 
 | ||||
|  | @ -94,6 +95,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200 | |||
|     @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") | ||||
|     def test_grabclipboard_png(self) -> None: | ||||
|         p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) | ||||
|         assert p.stdin is not None | ||||
|         p.stdin.write( | ||||
|             rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") | ||||
| $ms = new-object System.IO.MemoryStream(, $bytes) | ||||
|  | @ -113,7 +115,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes) | |||
|         reason="Linux with wl-clipboard only", | ||||
|     ) | ||||
|     @pytest.mark.parametrize("ext", ("gif", "png", "ico")) | ||||
|     def test_grabclipboard_wl_clipboard(self, ext) -> None: | ||||
|     def test_grabclipboard_wl_clipboard(self, ext: str) -> None: | ||||
|         image_path = "Tests/images/hopper." + ext | ||||
|         with open(image_path, "rb") as fp: | ||||
|             subprocess.call(["wl-copy"], stdin=fp) | ||||
|  | @ -128,6 +130,6 @@ $ms = new-object System.IO.MemoryStream(, $bytes) | |||
|         reason="Linux with wl-clipboard only", | ||||
|     ) | ||||
|     @pytest.mark.parametrize("arg", ("text", "--clear")) | ||||
|     def test_grabclipboard_wl_clipboard_errors(self, arg): | ||||
|     def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None: | ||||
|         subprocess.call(["wl-copy", arg]) | ||||
|         assert ImageGrab.grabclipboard() is None | ||||
|  |  | |||
|  | @ -73,15 +73,16 @@ def test_lut(op: str) -> None: | |||
| 
 | ||||
| 
 | ||||
| def test_no_operator_loaded() -> None: | ||||
|     im = Image.new("L", (1, 1)) | ||||
|     mop = ImageMorph.MorphOp() | ||||
|     with pytest.raises(Exception) as e: | ||||
|         mop.apply(None) | ||||
|         mop.apply(im) | ||||
|     assert str(e.value) == "No operator loaded" | ||||
|     with pytest.raises(Exception) as e: | ||||
|         mop.match(None) | ||||
|         mop.match(im) | ||||
|     assert str(e.value) == "No operator loaded" | ||||
|     with pytest.raises(Exception) as e: | ||||
|         mop.save_lut(None) | ||||
|         mop.save_lut("") | ||||
|     assert str(e.value) == "No operator loaded" | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,8 +13,12 @@ from .helper import ( | |||
| ) | ||||
| 
 | ||||
| 
 | ||||
| class Deformer: | ||||
|     def getmesh(self, im): | ||||
| class Deformer(ImageOps.SupportsGetMesh): | ||||
|     def getmesh( | ||||
|         self, im: Image.Image | ||||
|     ) -> list[ | ||||
|         tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] | ||||
|     ]: | ||||
|         x, y = im.size | ||||
|         return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] | ||||
| 
 | ||||
|  | @ -108,7 +112,7 @@ def test_fit_same_ratio() -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) | ||||
| def test_contain(new_size) -> None: | ||||
| def test_contain(new_size: tuple[int, int]) -> None: | ||||
|     im = hopper() | ||||
|     new_im = ImageOps.contain(im, new_size) | ||||
|     assert new_im.size == (256, 256) | ||||
|  | @ -132,7 +136,7 @@ def test_contain_round() -> None: | |||
|         ("hopper.png", (256, 256)),  # square | ||||
|     ), | ||||
| ) | ||||
| def test_cover(image_name, expected_size) -> None: | ||||
| def test_cover(image_name: str, expected_size: tuple[int, int]) -> None: | ||||
|     with Image.open("Tests/images/" + image_name) as im: | ||||
|         new_im = ImageOps.cover(im, (256, 256)) | ||||
|         assert new_im.size == expected_size | ||||
|  | @ -168,7 +172,7 @@ def test_pad_round() -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("P", "PA")) | ||||
| def test_palette(mode) -> None: | ||||
| def test_palette(mode: str) -> None: | ||||
|     im = hopper(mode) | ||||
| 
 | ||||
|     # Expand | ||||
|  | @ -210,7 +214,7 @@ def test_scale() -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) | ||||
| def test_expand_palette(border) -> None: | ||||
| def test_expand_palette(border: int | tuple[int, int, int, int]) -> None: | ||||
|     with Image.open("Tests/images/p_16.tga") as im: | ||||
|         im_expanded = ImageOps.expand(im, border, (255, 0, 0)) | ||||
| 
 | ||||
|  | @ -366,7 +370,7 @@ def test_exif_transpose() -> None: | |||
|     for ext in exts: | ||||
|         with Image.open("Tests/images/hopper" + ext) as base_im: | ||||
| 
 | ||||
|             def check(orientation_im) -> None: | ||||
|             def check(orientation_im: Image.Image) -> None: | ||||
|                 for im in [ | ||||
|                     orientation_im, | ||||
|                     orientation_im.copy(), | ||||
|  | @ -376,6 +380,7 @@ def test_exif_transpose() -> None: | |||
|                     else: | ||||
|                         original_exif = im.info["exif"] | ||||
|                     transposed_im = ImageOps.exif_transpose(im) | ||||
|                     assert transposed_im is not None | ||||
|                     assert_image_similar(base_im, transposed_im, 17) | ||||
|                     if orientation_im is base_im: | ||||
|                         assert "exif" not in im.info | ||||
|  | @ -387,6 +392,7 @@ def test_exif_transpose() -> None: | |||
| 
 | ||||
|                     # Repeat the operation to test that it does not keep transposing | ||||
|                     transposed_im2 = ImageOps.exif_transpose(transposed_im) | ||||
|                     assert transposed_im2 is not None | ||||
|                     assert_image_equal(transposed_im2, transposed_im) | ||||
| 
 | ||||
|             check(base_im) | ||||
|  | @ -402,6 +408,7 @@ def test_exif_transpose() -> None: | |||
|             assert im.getexif()[0x0112] == 3 | ||||
| 
 | ||||
|             transposed_im = ImageOps.exif_transpose(im) | ||||
|             assert transposed_im is not None | ||||
|             assert 0x0112 not in transposed_im.getexif() | ||||
| 
 | ||||
|             transposed_im._reload_exif() | ||||
|  | @ -414,12 +421,14 @@ def test_exif_transpose() -> None: | |||
|         assert im.getexif()[0x0112] == 3 | ||||
| 
 | ||||
|         transposed_im = ImageOps.exif_transpose(im) | ||||
|         assert transposed_im is not None | ||||
|         assert 0x0112 not in transposed_im.getexif() | ||||
| 
 | ||||
|     # Orientation set directly on Image.Exif | ||||
|     im = hopper() | ||||
|     im.getexif()[0x0112] = 3 | ||||
|     transposed_im = ImageOps.exif_transpose(im) | ||||
|     assert transposed_im is not None | ||||
|     assert 0x0112 not in transposed_im.getexif() | ||||
| 
 | ||||
| 
 | ||||
|  | @ -445,7 +454,7 @@ def test_autocontrast_cutoff() -> None: | |||
|     # Test the cutoff argument of autocontrast | ||||
|     with Image.open("Tests/images/bw_gradient.png") as img: | ||||
| 
 | ||||
|         def autocontrast(cutoff): | ||||
|         def autocontrast(cutoff: int | tuple[int, int]): | ||||
|             return ImageOps.autocontrast(img, cutoff).histogram() | ||||
| 
 | ||||
|         assert autocontrast(10) == autocontrast((10, 10)) | ||||
|  | @ -486,20 +495,20 @@ def test_autocontrast_mask_real_input() -> None: | |||
|         assert result_nomask != result | ||||
|         assert_tuple_approx_equal( | ||||
|             ImageStat.Stat(result, mask=rect_mask).median, | ||||
|             [195, 202, 184], | ||||
|             (195, 202, 184), | ||||
|             threshold=2, | ||||
|             msg="autocontrast with mask pixel incorrect", | ||||
|         ) | ||||
|         assert_tuple_approx_equal( | ||||
|             ImageStat.Stat(result_nomask).median, | ||||
|             [119, 106, 79], | ||||
|             (119, 106, 79), | ||||
|             threshold=2, | ||||
|             msg="autocontrast without mask pixel incorrect", | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| def test_autocontrast_preserve_tone() -> None: | ||||
|     def autocontrast(mode, preserve_tone): | ||||
|     def autocontrast(mode: str, preserve_tone: bool) -> list[int]: | ||||
|         im = hopper(mode) | ||||
|         return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() | ||||
| 
 | ||||
|  | @ -533,7 +542,7 @@ def test_autocontrast_preserve_gradient() -> None: | |||
| @pytest.mark.parametrize( | ||||
|     "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) | ||||
| ) | ||||
| def test_autocontrast_preserve_one_color(color) -> None: | ||||
| def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None: | ||||
|     img = Image.new("RGB", (10, 10), color) | ||||
| 
 | ||||
|     # single color images shouldn't change | ||||
|  |  | |||
|  | @ -1,12 +1,14 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| from typing import Generator | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, ImageFilter | ||||
| 
 | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_images(): | ||||
| def test_images() -> Generator[dict[str, Image.Image], None, None]: | ||||
|     ims = { | ||||
|         "im": Image.open("Tests/images/hopper.ppm"), | ||||
|         "snakes": Image.open("Tests/images/color_snakes.png"), | ||||
|  | @ -18,7 +20,7 @@ def test_images(): | |||
|             im.close() | ||||
| 
 | ||||
| 
 | ||||
| def test_filter_api(test_images) -> None: | ||||
| def test_filter_api(test_images: dict[str, Image.Image]) -> None: | ||||
|     im = test_images["im"] | ||||
| 
 | ||||
|     test_filter = ImageFilter.GaussianBlur(2.0) | ||||
|  | @ -26,13 +28,13 @@ def test_filter_api(test_images) -> None: | |||
|     assert i.mode == "RGB" | ||||
|     assert i.size == (128, 128) | ||||
| 
 | ||||
|     test_filter = ImageFilter.UnsharpMask(2.0, 125, 8) | ||||
|     i = im.filter(test_filter) | ||||
|     test_filter2 = ImageFilter.UnsharpMask(2.0, 125, 8) | ||||
|     i = im.filter(test_filter2) | ||||
|     assert i.mode == "RGB" | ||||
|     assert i.size == (128, 128) | ||||
| 
 | ||||
| 
 | ||||
| def test_usm_formats(test_images) -> None: | ||||
| def test_usm_formats(test_images: dict[str, Image.Image]) -> None: | ||||
|     im = test_images["im"] | ||||
| 
 | ||||
|     usm = ImageFilter.UnsharpMask | ||||
|  | @ -50,7 +52,7 @@ def test_usm_formats(test_images) -> None: | |||
|         im.convert("YCbCr").filter(usm) | ||||
| 
 | ||||
| 
 | ||||
| def test_blur_formats(test_images) -> None: | ||||
| def test_blur_formats(test_images: dict[str, Image.Image]) -> None: | ||||
|     im = test_images["im"] | ||||
| 
 | ||||
|     blur = ImageFilter.GaussianBlur | ||||
|  | @ -68,7 +70,7 @@ def test_blur_formats(test_images) -> None: | |||
|         im.convert("YCbCr").filter(blur) | ||||
| 
 | ||||
| 
 | ||||
| def test_usm_accuracy(test_images) -> None: | ||||
| def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None: | ||||
|     snakes = test_images["snakes"] | ||||
| 
 | ||||
|     src = snakes.convert("RGB") | ||||
|  | @ -77,7 +79,7 @@ def test_usm_accuracy(test_images) -> None: | |||
|     assert i.tobytes() == src.tobytes() | ||||
| 
 | ||||
| 
 | ||||
| def test_blur_accuracy(test_images) -> None: | ||||
| def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None: | ||||
|     snakes = test_images["snakes"] | ||||
| 
 | ||||
|     i = snakes.filter(ImageFilter.GaussianBlur(0.4)) | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ from __future__ import annotations | |||
| import array | ||||
| import math | ||||
| import struct | ||||
| from typing import Sequence | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
|  | @ -57,7 +58,9 @@ def test_path() -> None: | |||
|         ImagePath.Path((0, 1)), | ||||
|     ), | ||||
| ) | ||||
| def test_path_constructors(coords) -> None: | ||||
| def test_path_constructors( | ||||
|     coords: Sequence[float] | array.array[float] | ImagePath.Path, | ||||
| ) -> None: | ||||
|     # Arrange / Act | ||||
|     p = ImagePath.Path(coords) | ||||
| 
 | ||||
|  | @ -75,7 +78,9 @@ def test_path_constructors(coords) -> None: | |||
|         [[0.0, 1.0]], | ||||
|     ), | ||||
| ) | ||||
| def test_invalid_path_constructors(coords) -> None: | ||||
| def test_invalid_path_constructors( | ||||
|     coords: tuple[str, str] | Sequence[Sequence[int]] | ||||
| ) -> None: | ||||
|     # Act | ||||
|     with pytest.raises(ValueError) as e: | ||||
|         ImagePath.Path(coords) | ||||
|  | @ -93,7 +98,7 @@ def test_invalid_path_constructors(coords) -> None: | |||
|         [0, 1, 2], | ||||
|     ), | ||||
| ) | ||||
| def test_path_odd_number_of_coordinates(coords) -> None: | ||||
| def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: | ||||
|     # Act | ||||
|     with pytest.raises(ValueError) as e: | ||||
|         ImagePath.Path(coords) | ||||
|  | @ -111,7 +116,9 @@ def test_path_odd_number_of_coordinates(coords) -> None: | |||
|         (1, (0.0, 0.0, 0.0, 0.0)), | ||||
|     ], | ||||
| ) | ||||
| def test_getbbox(coords, expected) -> None: | ||||
| def test_getbbox( | ||||
|     coords: int | list[int], expected: tuple[float, float, float, float] | ||||
| ) -> None: | ||||
|     # Arrange | ||||
|     p = ImagePath.Path(coords) | ||||
| 
 | ||||
|  | @ -135,7 +142,7 @@ def test_getbbox_no_args() -> None: | |||
|         (list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]), | ||||
|     ], | ||||
| ) | ||||
| def test_map(coords, expected) -> None: | ||||
| def test_map(coords: int | list[int], expected: list[tuple[float, float]]) -> None: | ||||
|     # Arrange | ||||
|     p = ImagePath.Path(coords) | ||||
| 
 | ||||
|  | @ -201,9 +208,9 @@ class Evil: | |||
|     def __init__(self) -> None: | ||||
|         self.corrupt = Image.core.path(0x4000000000000000) | ||||
| 
 | ||||
|     def __getitem__(self, i): | ||||
|     def __getitem__(self, i: int) -> bytes: | ||||
|         x = self.corrupt[i] | ||||
|         return struct.pack("dd", x[0], x[1]) | ||||
| 
 | ||||
|     def __setitem__(self, i, x) -> None: | ||||
|     def __setitem__(self, i: int, x: bytes) -> None: | ||||
|         self.corrupt[i] = struct.unpack("dd", x) | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ def test_rgb() -> None: | |||
| 
 | ||||
|     assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) | ||||
| 
 | ||||
|     def checkrgb(r, g, b) -> None: | ||||
|     def checkrgb(r: int, g: int, b: int) -> None: | ||||
|         val = ImageQt.rgb(r, g, b) | ||||
|         val = val % 2**24  # drop the alpha | ||||
|         assert val >> 16 == r | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ def test_sanity(tmp_path: Path) -> None: | |||
|     assert index == 1 | ||||
| 
 | ||||
|     with pytest.raises(AttributeError): | ||||
|         ImageSequence.Iterator(0) | ||||
|         ImageSequence.Iterator(0)  # type: ignore[arg-type] | ||||
| 
 | ||||
| 
 | ||||
| def test_iterator() -> None: | ||||
|  | @ -72,6 +72,7 @@ def test_consecutive() -> None: | |||
|         for frame in ImageSequence.Iterator(im): | ||||
|             if first_frame is None: | ||||
|                 first_frame = frame.copy() | ||||
|         assert first_frame is not None | ||||
|         for frame in ImageSequence.Iterator(im): | ||||
|             assert_image_equal(frame, first_frame) | ||||
|             break | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| from typing import Any | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, ImageShow | ||||
|  | @ -24,9 +26,9 @@ def test_register() -> None: | |||
|     "order", | ||||
|     [-1, 0], | ||||
| ) | ||||
| def test_viewer_show(order) -> None: | ||||
| def test_viewer_show(order: int) -> None: | ||||
|     class TestViewer(ImageShow.Viewer): | ||||
|         def show_image(self, image, **options) -> bool: | ||||
|         def show_image(self, image: Image.Image, **options: Any) -> bool: | ||||
|             self.methodCalled = True | ||||
|             return True | ||||
| 
 | ||||
|  | @ -48,7 +50,7 @@ def test_viewer_show(order) -> None: | |||
|     reason="Only run on CIs; hangs on Windows CIs", | ||||
| ) | ||||
| @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) | ||||
| def test_show(mode) -> None: | ||||
| def test_show(mode: str) -> None: | ||||
|     im = hopper(mode) | ||||
|     assert ImageShow.show(im) | ||||
| 
 | ||||
|  | @ -66,14 +68,15 @@ def test_show_without_viewers() -> None: | |||
| def test_viewer() -> None: | ||||
|     viewer = ImageShow.Viewer() | ||||
| 
 | ||||
|     assert viewer.get_format(None) is None | ||||
|     im = Image.new("L", (1, 1)) | ||||
|     assert viewer.get_format(im) is None | ||||
| 
 | ||||
|     with pytest.raises(NotImplementedError): | ||||
|         viewer.get_command(None) | ||||
|         viewer.get_command("") | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("viewer", ImageShow._viewers) | ||||
| def test_viewers(viewer) -> None: | ||||
| def test_viewers(viewer: ImageShow.Viewer) -> None: | ||||
|     try: | ||||
|         viewer.get_command("test.jpg") | ||||
|     except NotImplementedError: | ||||
|  |  | |||
|  | @ -70,7 +70,7 @@ if is_win32(): | |||
|     ] | ||||
|     CreateDIBSection.restype = ctypes.wintypes.HBITMAP | ||||
| 
 | ||||
|     def serialize_dib(bi, pixels): | ||||
|     def serialize_dib(bi, pixels) -> bytearray: | ||||
|         bf = BITMAPFILEHEADER() | ||||
|         bf.bfType = 0x4D42 | ||||
|         bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize | ||||
|  |  | |||
|  | @ -78,7 +78,7 @@ def test_basic(tmp_path: Path, mode: str) -> None: | |||
| 
 | ||||
| 
 | ||||
| def test_tobytes() -> None: | ||||
|     def tobytes(mode: str) -> Image.Image: | ||||
|     def tobytes(mode: str) -> bytes: | ||||
|         return Image.new(mode, (1, 1), 1).tobytes() | ||||
| 
 | ||||
|     order = 1 if Image._ENDIAN == "<" else -1 | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ TEST_IMAGE_SIZE = (10, 10) | |||
| 
 | ||||
| 
 | ||||
| def test_numpy_to_image() -> None: | ||||
|     def to_image(dtype, bands: int = 1, boolean: int = 0): | ||||
|     def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image: | ||||
|         if bands == 1: | ||||
|             if boolean: | ||||
|                 data = [0, 255] * 50 | ||||
|  | @ -99,7 +99,7 @@ def test_1d_array() -> None: | |||
|     assert_image(Image.fromarray(a), "L", (1, 5)) | ||||
| 
 | ||||
| 
 | ||||
| def _test_img_equals_nparray(img, np) -> None: | ||||
| def _test_img_equals_nparray(img: Image.Image, np) -> None: | ||||
|     assert len(np.shape) >= 2 | ||||
|     np_size = np.shape[1], np.shape[0] | ||||
|     assert img.size == np_size | ||||
|  | @ -157,7 +157,7 @@ def test_save_tiff_uint16() -> None: | |||
|         ("HSV", numpy.uint8), | ||||
|     ), | ||||
| ) | ||||
| def test_to_array(mode, dtype) -> None: | ||||
| def test_to_array(mode: str, dtype) -> None: | ||||
|     img = hopper(mode) | ||||
| 
 | ||||
|     # Resize to non-square | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ from pathlib import Path | |||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import ImageQt | ||||
| from PIL import Image, ImageQt | ||||
| 
 | ||||
| from .helper import assert_image_equal_tofile, assert_image_similar, hopper | ||||
| 
 | ||||
|  | @ -37,7 +37,7 @@ if ImageQt.qt_is_installed: | |||
|             lbl.setPixmap(pixmap1.copy()) | ||||
| 
 | ||||
| 
 | ||||
| def roundtrip(expected) -> None: | ||||
| def roundtrip(expected: Image.Image) -> None: | ||||
|     result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) | ||||
|     # Qt saves all pixmaps as rgb | ||||
|     assert_image_similar(result, expected.convert("RGB"), 1) | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ if ImageQt.qt_is_installed: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) | ||||
| def test_sanity(mode, tmp_path: Path) -> None: | ||||
| def test_sanity(mode: str, tmp_path: Path) -> None: | ||||
|     src = hopper(mode) | ||||
|     data = ImageQt.toqimage(src) | ||||
| 
 | ||||
|  |  | |||
|  | @ -47,9 +47,8 @@ def test_tiff_crashes(test_file: str) -> None: | |||
|         with Image.open(test_file) as im: | ||||
|             im.load() | ||||
|     except FileNotFoundError: | ||||
|         if not on_ci(): | ||||
|             pytest.skip("test image not found") | ||||
|             return | ||||
|         raise | ||||
|         if on_ci(): | ||||
|             raise | ||||
|         pytest.skip("test image not found") | ||||
|     except OSError: | ||||
|         pass | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ from PIL import _util | |||
| @pytest.mark.parametrize( | ||||
|     "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] | ||||
| ) | ||||
| def test_is_path(test_path) -> None: | ||||
| def test_is_path(test_path: str | Path | PurePath) -> None: | ||||
|     # Act | ||||
|     it_is = _util.is_path(test_path) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| #!/bin/bash | ||||
| # install openjpeg | ||||
| 
 | ||||
| archive=openjpeg-2.5.0 | ||||
| archive=openjpeg-2.5.2 | ||||
| 
 | ||||
| ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz | ||||
| 
 | ||||
|  |  | |||
|  | @ -326,7 +326,7 @@ linkcheck_allowed_redirects = { | |||
|     r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", | ||||
|     r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", | ||||
|     r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", | ||||
|     r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", | ||||
|     r"https://tidelift.com/badges/package/pypi/pillow?.*": r"https://img.shields.io/badge/.*", | ||||
|     r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", | ||||
|     r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", | ||||
| } | ||||
|  |  | |||
|  | @ -504,3 +504,27 @@ PIL.OleFileIO | |||
| the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0 | ||||
| (2018-01). The deprecated file has now been removed from Pillow. If needed, install from | ||||
| PyPI (eg. ``python3 -m pip install olefile``). | ||||
| 
 | ||||
| import _imaging | ||||
| ~~~~~~~~~~~~~~~ | ||||
| 
 | ||||
| .. versionremoved:: 2.1.0 | ||||
| 
 | ||||
| Pillow >= 2.1.0 no longer supports ``import _imaging``. | ||||
| Please use ``from PIL.Image import core as _imaging`` instead. | ||||
| 
 | ||||
| Pillow and PIL | ||||
| ~~~~~~~~~~~~~~ | ||||
| 
 | ||||
| .. versionremoved:: 1.0.0 | ||||
| 
 | ||||
| Pillow and PIL cannot co-exist in the same environment. | ||||
| Before installing Pillow, please uninstall PIL. | ||||
| 
 | ||||
| import Image | ||||
| ~~~~~~~~~~~~ | ||||
| 
 | ||||
| .. versionremoved:: 1.0.0 | ||||
| 
 | ||||
| Pillow >= 1.0 no longer supports ``import Image``. | ||||
| Please use ``from PIL import Image`` instead. | ||||
|  |  | |||
|  | @ -49,7 +49,7 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h | |||
|    :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow | ||||
|    :alt: Zenodo | ||||
| 
 | ||||
| .. image:: https://tidelift.com/badges/package/pypi/Pillow?style=flat | ||||
| .. image:: https://tidelift.com/badges/package/pypi/pillow?style=flat | ||||
|    :target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge | ||||
|    :alt: Tidelift | ||||
| 
 | ||||
|  | @ -73,10 +73,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h | |||
|    :target: https://gitter.im/python-pillow/Pillow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge | ||||
|    :alt: Join the chat at https://gitter.im/python-pillow/Pillow | ||||
| 
 | ||||
| .. image:: https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg | ||||
|    :target: https://twitter.com/PythonPillow | ||||
|    :alt: Follow on https://twitter.com/PythonPillow | ||||
| 
 | ||||
| .. image:: https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg | ||||
|    :target: https://fosstodon.org/@pillow | ||||
|    :alt: Follow on https://fosstodon.org/@pillow | ||||
|  | @ -97,7 +93,7 @@ The core image library is designed for fast access to data stored in a few basic | |||
| .. toctree:: | ||||
|    :maxdepth: 2 | ||||
| 
 | ||||
|    installation.rst | ||||
|    installation/index.rst | ||||
|    handbook/index.rst | ||||
|    reference/index.rst | ||||
|    porting.rst | ||||
|  |  | |||
|  | @ -1,606 +1,29 @@ | |||
| :orphan: | ||||
| 
 | ||||
| Installation | ||||
| ============ | ||||
| 
 | ||||
| .. raw:: html | ||||
| 
 | ||||
|     <script> | ||||
|     document.addEventListener('DOMContentLoaded', function() { | ||||
|       activateTab(getOS()); | ||||
|     }); | ||||
|     </script> | ||||
| 
 | ||||
| Warnings | ||||
| -------- | ||||
| 
 | ||||
| .. warning:: Pillow and PIL cannot co-exist in the same environment. Before installing Pillow, please uninstall PIL. | ||||
| 
 | ||||
| .. warning:: Pillow >= 1.0 no longer supports ``import Image``. Please use ``from PIL import Image`` instead. | ||||
| 
 | ||||
| .. warning:: Pillow >= 2.1.0 no longer supports ``import _imaging``. Please use ``from PIL.Image import core as _imaging`` instead. | ||||
| 
 | ||||
| Python Support | ||||
| -------------- | ||||
| 
 | ||||
| Pillow supports these Python versions. | ||||
| 
 | ||||
| .. csv-table:: Newer versions | ||||
|    :file: newer-versions.csv | ||||
|    :header-rows: 1 | ||||
| 
 | ||||
| .. csv-table:: Older versions | ||||
|    :file: older-versions.csv | ||||
|    :header-rows: 1 | ||||
| 
 | ||||
| .. _Linux Installation: | ||||
| .. _macOS Installation: | ||||
| .. _Windows Installation: | ||||
| .. _FreeBSD Installation: | ||||
| 
 | ||||
| Basic Installation | ||||
| ------------------ | ||||
| 
 | ||||
| .. note:: | ||||
| .. Note:: This section has moved to :ref:`basic-installation`. Please update references accordingly. | ||||
| 
 | ||||
|     The following instructions will install Pillow with support for | ||||
|     most common image formats. See :ref:`external-libraries` for a | ||||
|     full list of external libraries supported. | ||||
| Python Support | ||||
| -------------- | ||||
| 
 | ||||
| Install Pillow with :command:`pip`:: | ||||
| 
 | ||||
|     python3 -m pip install --upgrade pip | ||||
|     python3 -m pip install --upgrade Pillow | ||||
| 
 | ||||
| Optionally, install :pypi:`defusedxml` for Pillow to read XMP data, | ||||
| and :pypi:`olefile` for Pillow to read FPX and MIC images:: | ||||
| 
 | ||||
|     python3 -m pip install --upgrade defusedxml olefile | ||||
| 
 | ||||
| 
 | ||||
| .. tab:: Linux | ||||
| 
 | ||||
|     We provide binaries for Linux for each of the supported Python | ||||
|     versions in the manylinux wheel format. These include support for all | ||||
|     optional libraries except libimagequant. Raqm support requires | ||||
|     FriBiDi to be installed separately:: | ||||
| 
 | ||||
|         python3 -m pip install --upgrade pip | ||||
|         python3 -m pip install --upgrade Pillow | ||||
| 
 | ||||
|     Most major Linux distributions, including Fedora, Ubuntu and ArchLinux | ||||
|     also include Pillow in packages that previously contained PIL e.g. | ||||
|     ``python-imaging``. Debian splits it into two packages, ``python3-pil`` | ||||
|     and ``python3-pil.imagetk``. | ||||
| 
 | ||||
| .. tab:: macOS | ||||
| 
 | ||||
|     We provide binaries for macOS for each of the supported Python | ||||
|     versions in the wheel format. These include support for all optional | ||||
|     libraries except libimagequant. Raqm support requires | ||||
|     FriBiDi to be installed separately:: | ||||
| 
 | ||||
|         python3 -m pip install --upgrade pip | ||||
|         python3 -m pip install --upgrade Pillow | ||||
| 
 | ||||
|     While we provide binaries for both x86-64 and arm64, we do not provide universal2 | ||||
|     binaries. However, it is simple to combine our current binaries to create one:: | ||||
| 
 | ||||
|         python3 -m pip download --only-binary=:all: --platform macosx_10_10_x86_64 Pillow | ||||
|         python3 -m pip download --only-binary=:all: --platform macosx_11_0_arm64 Pillow | ||||
|         python3 -m pip install delocate | ||||
| 
 | ||||
|     Then, with the names of the downloaded wheels, use Python to combine them:: | ||||
| 
 | ||||
|         from delocate.fuse import fuse_wheels | ||||
|         fuse_wheels('Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_universal2.whl') | ||||
| 
 | ||||
| .. tab:: Windows | ||||
| 
 | ||||
|     We provide Pillow binaries for Windows compiled for the matrix of supported | ||||
|     Pythons in the wheel format. These include x86, x86-64 and arm64 versions | ||||
|     (with the exception of Python 3.8 on arm64). These binaries include support | ||||
|     for all optional libraries except libimagequant and libxcb. Raqm support | ||||
|     requires FriBiDi to be installed separately:: | ||||
| 
 | ||||
|         python3 -m pip install --upgrade pip | ||||
|         python3 -m pip install --upgrade Pillow | ||||
| 
 | ||||
|     To install Pillow in MSYS2, see `Building on Windows using MSYS2/MinGW`_. | ||||
| 
 | ||||
| .. tab:: FreeBSD | ||||
| 
 | ||||
|     Pillow can be installed on FreeBSD via the official Ports or Packages systems: | ||||
| 
 | ||||
|     **Ports**:: | ||||
| 
 | ||||
|         cd /usr/ports/graphics/py-pillow && make install clean | ||||
| 
 | ||||
|     **Packages**:: | ||||
| 
 | ||||
|         pkg install py38-pillow | ||||
| 
 | ||||
|     .. note:: | ||||
| 
 | ||||
|         The `Pillow FreeBSD port | ||||
|         <https://www.freshports.org/graphics/py-pillow/>`_ and packages | ||||
|         are tested by the ports team with all supported FreeBSD versions. | ||||
| 
 | ||||
| 
 | ||||
| .. _Building on Linux: | ||||
| .. _Building on macOS: | ||||
| .. _Building on Windows: | ||||
| .. _Building on Windows using MSYS2/MinGW: | ||||
| .. _Building on FreeBSD: | ||||
| .. _Building on Android: | ||||
| 
 | ||||
| Building From Source | ||||
| -------------------- | ||||
| 
 | ||||
| .. _external-libraries: | ||||
| 
 | ||||
| External Libraries | ||||
| ^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| .. note:: | ||||
| 
 | ||||
|     You **do not need to install all supported external libraries** to | ||||
|     use Pillow's basic features. **Zlib** and **libjpeg** are required | ||||
|     by default. | ||||
| 
 | ||||
| .. note:: | ||||
| 
 | ||||
|    There are Dockerfiles in our `Docker images repo | ||||
|    <https://github.com/python-pillow/docker-images>`_ to install the | ||||
|    dependencies for some operating systems. | ||||
| 
 | ||||
| Many of Pillow's features require external libraries: | ||||
| 
 | ||||
| * **libjpeg** provides JPEG functionality. | ||||
| 
 | ||||
|   * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and | ||||
|     libjpeg-turbo version **8**. | ||||
|   * Starting with Pillow 3.0.0, libjpeg is required by default. It can be | ||||
|     disabled with the ``-C jpeg=disable`` flag. | ||||
| 
 | ||||
| * **zlib** provides access to compressed PNGs | ||||
| 
 | ||||
|   * Starting with Pillow 3.0.0, zlib is required by default. It can be | ||||
|     disabled with the ``-C zlib=disable`` flag. | ||||
| 
 | ||||
| * **libtiff** provides compressed TIFF functionality | ||||
| 
 | ||||
|   * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0** | ||||
| 
 | ||||
| * **libfreetype** provides type related services | ||||
| 
 | ||||
| * **littlecms** provides color management | ||||
| 
 | ||||
|   * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and | ||||
|     above uses liblcms2. Tested with **1.19** and **2.7-2.16**. | ||||
| 
 | ||||
| * **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**, | ||||
|     **2.4.0** and **2.5.0**. | ||||
|   * Pillow does **not** support the earlier **1.5** series which ships | ||||
|     with Debian Jessie. | ||||
| 
 | ||||
| * **libimagequant** provides improved color quantization | ||||
| 
 | ||||
|   * Pillow has been tested with libimagequant **2.6-4.2.2** | ||||
|   * Libimagequant is licensed GPLv3, which is more restrictive than | ||||
|     the Pillow license, therefore we will not be distributing binaries | ||||
|     with libimagequant support enabled. | ||||
| 
 | ||||
| * **libraqm** provides complex text layout support. | ||||
| 
 | ||||
|   * libraqm provides bidirectional text support (using FriBiDi), | ||||
|     shaping (using HarfBuzz), and proper script itemization. As a | ||||
|     result, Raqm can support most writing systems covered by Unicode. | ||||
|   * libraqm depends on the following libraries: FreeType, HarfBuzz, | ||||
|     FriBiDi, make sure that you install them before installing libraqm | ||||
|     if not available as package in your system. | ||||
|   * Setting text direction or font features is not supported without libraqm. | ||||
|   * Pillow wheels since version 8.2.0 include a modified version of libraqm that | ||||
|     loads libfribidi at runtime if it is installed. | ||||
|     On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` | ||||
|     into a directory listed in the `Dynamic-link library search order (Microsoft Learn) | ||||
|     <https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#search-order-for-unpackaged-apps>`_ | ||||
|     (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). | ||||
|     See `Build Options`_ to see how to build this version. | ||||
|   * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. | ||||
| 
 | ||||
| * **libxcb** provides X11 screengrab support. | ||||
| 
 | ||||
| .. tab:: Linux | ||||
| 
 | ||||
|     If you didn't build Python from source, make sure you have Python's | ||||
|     development libraries installed. | ||||
| 
 | ||||
|     In Debian or Ubuntu:: | ||||
| 
 | ||||
|         sudo apt-get install python3-dev python3-setuptools | ||||
| 
 | ||||
|     In Fedora, the command is:: | ||||
| 
 | ||||
|         sudo dnf install python3-devel redhat-rpm-config | ||||
| 
 | ||||
|     In Alpine, the command is:: | ||||
| 
 | ||||
|         sudo apk add python3-dev py3-setuptools | ||||
| 
 | ||||
|     .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. | ||||
| 
 | ||||
|     Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: | ||||
| 
 | ||||
|         sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ | ||||
|             libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ | ||||
|             libharfbuzz-dev libfribidi-dev libxcb1-dev | ||||
| 
 | ||||
|     To install libraqm, ``sudo apt-get install meson`` and then see | ||||
|     ``depends/install_raqm.sh``. | ||||
| 
 | ||||
|     Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: | ||||
| 
 | ||||
|         sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ | ||||
|             freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ | ||||
|             harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel | ||||
| 
 | ||||
|     Note that the package manager may be yum or DNF, depending on the | ||||
|     exact distribution. | ||||
| 
 | ||||
|     Prerequisites are installed for **Alpine** with:: | ||||
| 
 | ||||
|         sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ | ||||
|             libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ | ||||
|             libxcb-dev libpng-dev | ||||
| 
 | ||||
|     See also the ``Dockerfile``\s in the Test Infrastructure repo | ||||
|     (https://github.com/python-pillow/docker-images) for a known working | ||||
|     install process for other tested distros. | ||||
| 
 | ||||
| .. tab:: macOS | ||||
| 
 | ||||
|     The Xcode command line tools are required to compile portions of | ||||
|     Pillow. The tools are installed by running ``xcode-select --install`` | ||||
|     from the command line. The command line tools are required even if you | ||||
|     have the full Xcode package installed.  It may be necessary to run | ||||
|     ``sudo xcodebuild -license`` to accept the license prior to using the | ||||
|     tools. | ||||
| 
 | ||||
|     The easiest way to install external libraries is via `Homebrew | ||||
|     <https://brew.sh/>`_. After you install Homebrew, run:: | ||||
| 
 | ||||
|         brew install libjpeg libtiff little-cms2 openjpeg webp | ||||
| 
 | ||||
|     To install libraqm on macOS use Homebrew to install its dependencies:: | ||||
| 
 | ||||
|         brew install freetype harfbuzz fribidi | ||||
| 
 | ||||
|     Then see ``depends/install_raqm_cmake.sh`` to install libraqm. | ||||
| 
 | ||||
| .. tab:: Windows | ||||
| 
 | ||||
|     We recommend you use prebuilt wheels from PyPI. | ||||
|     If you wish to compile Pillow manually, you can use the build scripts | ||||
|     in the ``winbuild`` directory used for CI testing and development. | ||||
|     These scripts require Visual Studio 2017 or newer and NASM. | ||||
| 
 | ||||
|     The scripts also install Pillow from the local copy of the source code, so the | ||||
|     `Installing`_ instructions will not be necessary afterwards. | ||||
| 
 | ||||
| .. tab:: Windows using MSYS2/MinGW | ||||
| 
 | ||||
|     To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or | ||||
|     **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. | ||||
| 
 | ||||
|     The following instructions target the 64-bit build, for 32-bit | ||||
|     replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. | ||||
| 
 | ||||
|     Make sure you have Python and GCC installed:: | ||||
| 
 | ||||
|         pacman -S \ | ||||
|             mingw-w64-x86_64-gcc \ | ||||
|             mingw-w64-x86_64-python3 \ | ||||
|             mingw-w64-x86_64-python3-pip \ | ||||
|             mingw-w64-x86_64-python3-setuptools | ||||
| 
 | ||||
|     Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: | ||||
| 
 | ||||
|         pacman -S \ | ||||
|             mingw-w64-x86_64-libjpeg-turbo \ | ||||
|             mingw-w64-x86_64-zlib \ | ||||
|             mingw-w64-x86_64-libtiff \ | ||||
|             mingw-w64-x86_64-freetype \ | ||||
|             mingw-w64-x86_64-lcms2 \ | ||||
|             mingw-w64-x86_64-libwebp \ | ||||
|             mingw-w64-x86_64-openjpeg2 \ | ||||
|             mingw-w64-x86_64-libimagequant \ | ||||
|             mingw-w64-x86_64-libraqm | ||||
| 
 | ||||
|     https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with | ||||
|     MSYS2. To workaround this, before installing Pillow you must run:: | ||||
| 
 | ||||
|         export SETUPTOOLS_USE_DISTUTILS=stdlib | ||||
| 
 | ||||
| .. tab:: FreeBSD | ||||
| 
 | ||||
|     .. Note:: Only FreeBSD 10 and 11 tested | ||||
| 
 | ||||
|     Make sure you have Python's development libraries installed:: | ||||
| 
 | ||||
|         sudo pkg install python3 | ||||
| 
 | ||||
|     Prerequisites are installed on **FreeBSD 10 or 11** with:: | ||||
| 
 | ||||
|         sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb | ||||
| 
 | ||||
|     Then see ``depends/install_raqm_cmake.sh`` to install libraqm. | ||||
| 
 | ||||
| .. tab:: Android | ||||
| 
 | ||||
|     Basic Android support has been added for compilation within the Termux | ||||
|     environment. The dependencies can be installed by:: | ||||
| 
 | ||||
|         pkg install -y python ndk-sysroot clang make \ | ||||
|             libjpeg-turbo | ||||
| 
 | ||||
|     This has been tested within the Termux app on ChromeOS, on x86. | ||||
| 
 | ||||
| Installing | ||||
| ^^^^^^^^^^ | ||||
| 
 | ||||
| Once you have installed the prerequisites, to install Pillow from the source | ||||
| code on PyPI, run:: | ||||
| 
 | ||||
|     python3 -m pip install --upgrade pip | ||||
|     python3 -m pip install --upgrade Pillow --no-binary :all: | ||||
| 
 | ||||
| If the prerequisites are installed in the standard library locations | ||||
| for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no | ||||
| additional configuration should be required. If they are installed in | ||||
| a non-standard location, you may need to configure setuptools to use | ||||
| those locations by editing :file:`setup.py` or | ||||
| :file:`pyproject.toml`, or by adding environment variables on the command | ||||
| line:: | ||||
| 
 | ||||
|     CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: | ||||
| 
 | ||||
| If Pillow has been previously built without the required | ||||
| prerequisites, it may be necessary to manually clear the pip cache or | ||||
| build without cache using the ``--no-cache-dir`` option to force a | ||||
| build with newly installed external libraries. | ||||
| 
 | ||||
| If you would like to install from a local copy of the source code instead, you | ||||
| can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow`` | ||||
| or download and extract the `compressed archive from PyPI`_. | ||||
| 
 | ||||
| After navigating to the Pillow directory, run:: | ||||
| 
 | ||||
|     python3 -m pip install --upgrade pip | ||||
|     python3 -m pip install . | ||||
| 
 | ||||
| .. _compressed archive from PyPI: https://pypi.org/project/pillow/#files | ||||
| 
 | ||||
| Build Options | ||||
| """"""""""""" | ||||
| 
 | ||||
| * Config setting: ``-C parallel=n``. Can also be given | ||||
|   with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use | ||||
|   multiprocessing to build the extension. Setting ``-C parallel=n`` | ||||
|   sets the number of CPUs to use to ``n``, or can disable parallel building by | ||||
|   using a setting of 1. By default, it uses 4 CPUs, or if 4 are not | ||||
|   available, as many as are present. | ||||
| 
 | ||||
| * 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 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 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. | ||||
| 
 | ||||
| * Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. | ||||
|   These flags are used to compile a modified version of libraqm and | ||||
|   a shim that dynamically loads libfribidi at runtime. These are | ||||
|   used to compile the standard Pillow wheels. Compiling libraqm requires | ||||
|   a C99-compliant compiler. | ||||
| 
 | ||||
| * Config setting: ``-C platform-guessing=disable``. Skips all of the | ||||
|   platform dependent guessing of include and library directories for | ||||
|   automated build systems that configure the proper paths in the | ||||
|   environment variables (e.g. Buildroot). | ||||
| 
 | ||||
| * Config setting: ``-C debug=true``. Adds a debugging flag to the include and | ||||
|   library search process to dump all paths searched for and found to stdout. | ||||
| 
 | ||||
| 
 | ||||
| Sample usage:: | ||||
| 
 | ||||
|     python3 -m pip install --upgrade Pillow -C [feature]=enable | ||||
| .. Note:: This section has moved to :ref:`python-support`. Please update references accordingly. | ||||
| 
 | ||||
| Platform Support | ||||
| ---------------- | ||||
| 
 | ||||
| Current platform support for Pillow. Binary distributions are | ||||
| contributed for each release on a volunteer basis, but the source | ||||
| should compile and run everywhere platform support is listed. In | ||||
| general, we aim to support all current versions of Linux, macOS, and | ||||
| Windows. | ||||
| .. Note:: This section has moved to :ref:`platform-support`. Please update references accordingly. | ||||
| 
 | ||||
| Continuous Integration Targets | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| Building From Source | ||||
| -------------------- | ||||
| 
 | ||||
| These platforms are built and tested for every change. | ||||
| 
 | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Operating system                 | Tested Python versions     | Tested architecture | | ||||
| +==================================+============================+=====================+ | ||||
| | Alpine                           | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Amazon Linux 2                   | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Amazon Linux 2023                | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Arch                             | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | CentOS 7                         | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | CentOS Stream 8                  | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | CentOS Stream 9                  | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Debian 11 Bullseye               | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Debian 12 Bookworm               | 3.11                       | x86, x86-64         | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Fedora 38                        | 3.11                       | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Fedora 39                        | 3.12                       | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Gentoo                           | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | macOS 12 Monterey                | 3.8, 3.9, 3.10, 3.11,      | x86-64              | | ||||
| |                                  | 3.12, PyPy3                |                     | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Ubuntu Linux 20.04 LTS (Focal)   | 3.8                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Ubuntu Linux 22.04 LTS (Jammy)   | 3.8, 3.9, 3.10, 3.11,      | x86-64              | | ||||
| |                                  | 3.12, PyPy3                |                     | | ||||
| |                                  +----------------------------+---------------------+ | ||||
| |                                  | 3.10                       | arm64v8, ppc64le,   | | ||||
| |                                  |                            | s390x               | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Windows Server 2016              | 3.8                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Windows Server 2022              | 3.8, 3.9, 3.10, 3.11,      | x86-64              | | ||||
| |                                  | 3.12, PyPy3                |                     | | ||||
| |                                  +----------------------------+---------------------+ | ||||
| |                                  | 3.12                       | x86                 | | ||||
| |                                  +----------------------------+---------------------+ | ||||
| |                                  | 3.9 (MinGW)                | x86-64              | | ||||
| |                                  +----------------------------+---------------------+ | ||||
| |                                  | 3.8, 3.9 (Cygwin)          | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| 
 | ||||
| 
 | ||||
| Other Platforms | ||||
| ^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| These platforms have been reported to work at the versions mentioned. | ||||
| 
 | ||||
| .. note:: | ||||
| 
 | ||||
|     Contributors please test Pillow on your platform then update this | ||||
|     document and send a pull request. | ||||
| 
 | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Operating system                 | | Tested Python            | | Latest tested  | | Tested     | | ||||
| |                                  | | versions                 | | Pillow version | | processors | | ||||
| +==================================+============================+==================+==============+ | ||||
| | macOS 14 Sonoma                  | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0           |arm           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 13 Ventura                 | 3.8, 3.9, 3.10, 3.11       | 10.0.1           |arm           | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 3.7                        | 9.5.0            |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 12 Monterey                | 3.7, 3.8, 3.9, 3.10, 3.11  | 9.3.0            |arm           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 11 Big Sur                 | 3.7, 3.8, 3.9, 3.10        | 8.4.0            |arm           | | ||||
| |                                  +----------------------------+------------------+--------------+ | ||||
| |                                  | 3.7, 3.8, 3.9, 3.10, 3.11  | 9.4.0            |x86-64        | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 3.6                        | 8.4.0            |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 10.15 Catalina             | 3.6, 3.7, 3.8, 3.9         | 8.3.2            |x86-64        | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 3.5                        | 7.2.0            |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 10.14 Mojave               | 3.5, 3.6, 3.7, 3.8         | 7.2.0            |x86-64        | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 2.7                        | 6.0.0            |              | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 3.4                        | 5.4.1            |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 10.13 High Sierra          | 2.7, 3.4, 3.5, 3.6         | 4.2.1            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 10.12 Sierra               | 2.7, 3.4, 3.5, 3.6         | 4.1.1            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Mac OS X 10.11 El Capitan        | 2.7, 3.4, 3.5, 3.6, 3.7    | 5.4.1            |x86-64        | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 3.3                        | 4.1.0            |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Mac OS X 10.9 Mavericks          | 2.7, 3.2, 3.3, 3.4         | 3.0.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Mac OS X 10.8 Mountain Lion      | 2.6, 2.7, 3.2, 3.3         |                  |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Redhat Linux 6                   | 2.6                        |                  |x86           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | CentOS 6.3                       | 2.7, 3.3                   |                  |x86           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | CentOS 8                         | 3.9                        | 9.0.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Fedora 23                        | 2.7, 3.4                   | 3.1.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5  | 3.4.1            |x86,x86-64    | | ||||
| |                                  | | PyPy5.3.1, PyPy3 v2.4.0  |                  |              | | ||||
| |                                  +----------------------------+------------------+--------------+ | ||||
| |                                  | 2.7                        | 4.3.0            |x86-64        | | ||||
| |                                  +----------------------------+------------------+--------------+ | ||||
| |                                  | 2.7, 3.2                   | 3.4.1            |ppc           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Ubuntu Linux 10.04 LTS (Lucid)   | 2.6                        | 2.3.0            |x86,x86-64    | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Debian 8.2 Jessie                | 2.7, 3.4                   | 3.1.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Raspbian Jessie                  | 2.7, 3.4                   | 3.1.0            |arm           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Raspbian Stretch                 | 2.7, 3.5                   | 4.0.0            |arm           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Raspberry Pi OS                  | 3.6, 3.7, 3.8, 3.9         | 8.2.0            |arm           | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 2.7                        | 6.2.2            |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Gentoo Linux                     | 2.7, 3.2                   | 2.1.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | FreeBSD 11.1                     | 2.7, 3.4, 3.5, 3.6         | 4.3.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | FreeBSD 10.3                     | 2.7, 3.4, 3.5              | 4.2.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | FreeBSD 10.2                     | 2.7, 3.4                   | 3.1.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 11                       | 3.9, 3.10, 3.11, 3.12      | 10.2.0           |arm64         | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 11 Pro                   | 3.11, 3.12                 | 10.2.0           |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 10                       | 3.7                        | 7.1.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 10/Cygwin 3.3            | 3.6, 3.7, 3.8, 3.9         | 8.4.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 8.1 Pro                  | 2.6, 2.7, 3.2, 3.3, 3.4    | 2.4.0            |x86,x86-64    | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 8 Pro                    | 2.6, 2.7, 3.2, 3.3, 3.4a3  | 2.2.0            |x86,x86-64    | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 7 Professional           | 3.7                        | 7.0.0            |x86,x86-64    | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows Server 2008 R2 Enterprise| 3.3                        |                  |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| .. Note:: This section has moved to :ref:`building-from-source`. Please update references accordingly. | ||||
| 
 | ||||
| Old Versions | ||||
| ------------ | ||||
| 
 | ||||
| You can download old distributions from the `release history at PyPI | ||||
| <https://pypi.org/project/pillow/#history>`_ and by direct URL access | ||||
| eg. https://pypi.org/project/pillow/1.0/. | ||||
| .. Note:: This section has moved to :ref:`old-versions`. Please update references accordingly. | ||||
|  |  | |||
							
								
								
									
										97
									
								
								docs/installation/basic-installation.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,97 @@ | |||
| .. raw:: html | ||||
| 
 | ||||
|     <script> | ||||
|     document.addEventListener('DOMContentLoaded', function() { | ||||
|       activateTab(getOS()); | ||||
|     }); | ||||
|     </script> | ||||
| 
 | ||||
| .. _basic-installation: | ||||
| 
 | ||||
| Basic Installation | ||||
| ================== | ||||
| 
 | ||||
| .. note:: | ||||
| 
 | ||||
|     The following instructions will install Pillow with support for | ||||
|     most common image formats. See :ref:`external-libraries` for a | ||||
|     full list of external libraries supported. | ||||
| 
 | ||||
| Install Pillow with :command:`pip`:: | ||||
| 
 | ||||
|     python3 -m pip install --upgrade pip | ||||
|     python3 -m pip install --upgrade Pillow | ||||
| 
 | ||||
| Optionally, install :pypi:`defusedxml` for Pillow to read XMP data, | ||||
| and :pypi:`olefile` for Pillow to read FPX and MIC images:: | ||||
| 
 | ||||
|     python3 -m pip install --upgrade defusedxml olefile | ||||
| 
 | ||||
| 
 | ||||
| .. tab:: Linux | ||||
| 
 | ||||
|     We provide binaries for Linux for each of the supported Python | ||||
|     versions in the manylinux wheel format. These include support for all | ||||
|     optional libraries except libimagequant. Raqm support requires | ||||
|     FriBiDi to be installed separately:: | ||||
| 
 | ||||
|         python3 -m pip install --upgrade pip | ||||
|         python3 -m pip install --upgrade Pillow | ||||
| 
 | ||||
|     Most major Linux distributions, including Fedora, Ubuntu and ArchLinux | ||||
|     also include Pillow in packages that previously contained PIL e.g. | ||||
|     ``python-imaging``. Debian splits it into two packages, ``python3-pil`` | ||||
|     and ``python3-pil.imagetk``. | ||||
| 
 | ||||
| .. tab:: macOS | ||||
| 
 | ||||
|     We provide binaries for macOS for each of the supported Python | ||||
|     versions in the wheel format. These include support for all optional | ||||
|     libraries except libimagequant. Raqm support requires | ||||
|     FriBiDi to be installed separately:: | ||||
| 
 | ||||
|         python3 -m pip install --upgrade pip | ||||
|         python3 -m pip install --upgrade Pillow | ||||
| 
 | ||||
|     While we provide binaries for both x86-64 and arm64, we do not provide universal2 | ||||
|     binaries. However, it is simple to combine our current binaries to create one:: | ||||
| 
 | ||||
|         python3 -m pip download --only-binary=:all: --platform macosx_10_10_x86_64 Pillow | ||||
|         python3 -m pip download --only-binary=:all: --platform macosx_11_0_arm64 Pillow | ||||
|         python3 -m pip install delocate | ||||
| 
 | ||||
|     Then, with the names of the downloaded wheels, use Python to combine them:: | ||||
| 
 | ||||
|         from delocate.fuse import fuse_wheels | ||||
|         fuse_wheels('Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_universal2.whl') | ||||
| 
 | ||||
| .. tab:: Windows | ||||
| 
 | ||||
|     We provide Pillow binaries for Windows compiled for the matrix of supported | ||||
|     Pythons in the wheel format. These include x86, x86-64 and arm64 versions | ||||
|     (with the exception of Python 3.8 on arm64). These binaries include support | ||||
|     for all optional libraries except libimagequant and libxcb. Raqm support | ||||
|     requires FriBiDi to be installed separately:: | ||||
| 
 | ||||
|         python3 -m pip install --upgrade pip | ||||
|         python3 -m pip install --upgrade Pillow | ||||
| 
 | ||||
|     To install Pillow in MSYS2, see :ref:`building-from-source`. | ||||
| 
 | ||||
| .. tab:: FreeBSD | ||||
| 
 | ||||
|     Pillow can be installed on FreeBSD via the official Ports or Packages systems: | ||||
| 
 | ||||
|     **Ports**:: | ||||
| 
 | ||||
|         cd /usr/ports/graphics/py-pillow && make install clean | ||||
| 
 | ||||
|     **Packages**:: | ||||
| 
 | ||||
|         pkg install py38-pillow | ||||
| 
 | ||||
|     .. note:: | ||||
| 
 | ||||
|         The `Pillow FreeBSD port | ||||
|         <https://www.freshports.org/graphics/py-pillow/>`_ and packages | ||||
|         are tested by the ports team with all supported FreeBSD versions. | ||||
							
								
								
									
										317
									
								
								docs/installation/building-from-source.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,317 @@ | |||
| .. raw:: html | ||||
| 
 | ||||
|     <script> | ||||
|     document.addEventListener('DOMContentLoaded', function() { | ||||
|       activateTab(getOS()); | ||||
|     }); | ||||
|     </script> | ||||
| 
 | ||||
| .. _building-from-source: | ||||
| 
 | ||||
| Building From Source | ||||
| ==================== | ||||
| 
 | ||||
| .. _external-libraries: | ||||
| 
 | ||||
| External Libraries | ||||
| ------------------ | ||||
| 
 | ||||
| .. note:: | ||||
| 
 | ||||
|     You **do not need to install all supported external libraries** to | ||||
|     use Pillow's basic features. **Zlib** and **libjpeg** are required | ||||
|     by default. | ||||
| 
 | ||||
| .. note:: | ||||
| 
 | ||||
|    There are Dockerfiles in our `Docker images repo | ||||
|    <https://github.com/python-pillow/docker-images>`_ to install the | ||||
|    dependencies for some operating systems. | ||||
| 
 | ||||
| Many of Pillow's features require external libraries: | ||||
| 
 | ||||
| * **libjpeg** provides JPEG functionality. | ||||
| 
 | ||||
|   * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and | ||||
|     libjpeg-turbo version **8**. | ||||
|   * Starting with Pillow 3.0.0, libjpeg is required by default. It can be | ||||
|     disabled with the ``-C jpeg=disable`` flag. | ||||
| 
 | ||||
| * **zlib** provides access to compressed PNGs | ||||
| 
 | ||||
|   * Starting with Pillow 3.0.0, zlib is required by default. It can be | ||||
|     disabled with the ``-C zlib=disable`` flag. | ||||
| 
 | ||||
| * **libtiff** provides compressed TIFF functionality | ||||
| 
 | ||||
|   * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0** | ||||
| 
 | ||||
| * **libfreetype** provides type related services | ||||
| 
 | ||||
| * **littlecms** provides color management | ||||
| 
 | ||||
|   * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and | ||||
|     above uses liblcms2. Tested with **1.19** and **2.7-2.16**. | ||||
| 
 | ||||
| * **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**, | ||||
|     **2.4.0**, **2.5.0** and **2.5.2**. | ||||
|   * Pillow does **not** support the earlier **1.5** series which ships | ||||
|     with Debian Jessie. | ||||
| 
 | ||||
| * **libimagequant** provides improved color quantization | ||||
| 
 | ||||
|   * Pillow has been tested with libimagequant **2.6-4.2.2** | ||||
|   * Libimagequant is licensed GPLv3, which is more restrictive than | ||||
|     the Pillow license, therefore we will not be distributing binaries | ||||
|     with libimagequant support enabled. | ||||
| 
 | ||||
| * **libraqm** provides complex text layout support. | ||||
| 
 | ||||
|   * libraqm provides bidirectional text support (using FriBiDi), | ||||
|     shaping (using HarfBuzz), and proper script itemization. As a | ||||
|     result, Raqm can support most writing systems covered by Unicode. | ||||
|   * libraqm depends on the following libraries: FreeType, HarfBuzz, | ||||
|     FriBiDi, make sure that you install them before installing libraqm | ||||
|     if not available as package in your system. | ||||
|   * Setting text direction or font features is not supported without libraqm. | ||||
|   * Pillow wheels since version 8.2.0 include a modified version of libraqm that | ||||
|     loads libfribidi at runtime if it is installed. | ||||
|     On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` | ||||
|     into a directory listed in the `Dynamic-link library search order (Microsoft Learn) | ||||
|     <https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#search-order-for-unpackaged-apps>`_ | ||||
|     (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). | ||||
|     See `Build Options`_ to see how to build this version. | ||||
|   * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. | ||||
| 
 | ||||
| * **libxcb** provides X11 screengrab support. | ||||
| 
 | ||||
| .. tab:: Linux | ||||
| 
 | ||||
|     If you didn't build Python from source, make sure you have Python's | ||||
|     development libraries installed. | ||||
| 
 | ||||
|     In Debian or Ubuntu:: | ||||
| 
 | ||||
|         sudo apt-get install python3-dev python3-setuptools | ||||
| 
 | ||||
|     In Fedora, the command is:: | ||||
| 
 | ||||
|         sudo dnf install python3-devel redhat-rpm-config | ||||
| 
 | ||||
|     In Alpine, the command is:: | ||||
| 
 | ||||
|         sudo apk add python3-dev py3-setuptools | ||||
| 
 | ||||
|     .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. | ||||
| 
 | ||||
|     Prerequisites for **Ubuntu 16.04 LTS - 22.04 LTS** are installed with:: | ||||
| 
 | ||||
|         sudo apt-get install libtiff5-dev libjpeg8-dev libopenjp2-7-dev zlib1g-dev \ | ||||
|             libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ | ||||
|             libharfbuzz-dev libfribidi-dev libxcb1-dev | ||||
| 
 | ||||
|     To install libraqm, ``sudo apt-get install meson`` and then see | ||||
|     ``depends/install_raqm.sh``. | ||||
| 
 | ||||
|     Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: | ||||
| 
 | ||||
|         sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ | ||||
|             freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel \ | ||||
|             harfbuzz-devel fribidi-devel libraqm-devel libimagequant-devel libxcb-devel | ||||
| 
 | ||||
|     Note that the package manager may be yum or DNF, depending on the | ||||
|     exact distribution. | ||||
| 
 | ||||
|     Prerequisites are installed for **Alpine** with:: | ||||
| 
 | ||||
|         sudo apk add tiff-dev jpeg-dev openjpeg-dev zlib-dev freetype-dev lcms2-dev \ | ||||
|             libwebp-dev tcl-dev tk-dev harfbuzz-dev fribidi-dev libimagequant-dev \ | ||||
|             libxcb-dev libpng-dev | ||||
| 
 | ||||
|     See also the ``Dockerfile``\s in the Test Infrastructure repo | ||||
|     (https://github.com/python-pillow/docker-images) for a known working | ||||
|     install process for other tested distros. | ||||
| 
 | ||||
| .. tab:: macOS | ||||
| 
 | ||||
|     The Xcode command line tools are required to compile portions of | ||||
|     Pillow. The tools are installed by running ``xcode-select --install`` | ||||
|     from the command line. The command line tools are required even if you | ||||
|     have the full Xcode package installed.  It may be necessary to run | ||||
|     ``sudo xcodebuild -license`` to accept the license prior to using the | ||||
|     tools. | ||||
| 
 | ||||
|     The easiest way to install external libraries is via `Homebrew | ||||
|     <https://brew.sh/>`_. After you install Homebrew, run:: | ||||
| 
 | ||||
|         brew install libjpeg libtiff little-cms2 openjpeg webp | ||||
| 
 | ||||
|     To install libraqm on macOS use Homebrew to install its dependencies:: | ||||
| 
 | ||||
|         brew install freetype harfbuzz fribidi | ||||
| 
 | ||||
|     Then see ``depends/install_raqm_cmake.sh`` to install libraqm. | ||||
| 
 | ||||
| .. tab:: Windows | ||||
| 
 | ||||
|     We recommend you use prebuilt wheels from PyPI. | ||||
|     If you wish to compile Pillow manually, you can use the build scripts | ||||
|     in the ``winbuild`` directory used for CI testing and development. | ||||
|     These scripts require Visual Studio 2017 or newer and NASM. | ||||
| 
 | ||||
|     The scripts also install Pillow from the local copy of the source code, so the | ||||
|     `Installing`_ instructions will not be necessary afterwards. | ||||
| 
 | ||||
| .. tab:: Windows using MSYS2/MinGW | ||||
| 
 | ||||
|     To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or | ||||
|     **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. | ||||
| 
 | ||||
|     The following instructions target the 64-bit build, for 32-bit | ||||
|     replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. | ||||
| 
 | ||||
|     Make sure you have Python and GCC installed:: | ||||
| 
 | ||||
|         pacman -S \ | ||||
|             mingw-w64-x86_64-gcc \ | ||||
|             mingw-w64-x86_64-python3 \ | ||||
|             mingw-w64-x86_64-python3-pip \ | ||||
|             mingw-w64-x86_64-python3-setuptools | ||||
| 
 | ||||
|     Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: | ||||
| 
 | ||||
|         pacman -S \ | ||||
|             mingw-w64-x86_64-libjpeg-turbo \ | ||||
|             mingw-w64-x86_64-zlib \ | ||||
|             mingw-w64-x86_64-libtiff \ | ||||
|             mingw-w64-x86_64-freetype \ | ||||
|             mingw-w64-x86_64-lcms2 \ | ||||
|             mingw-w64-x86_64-libwebp \ | ||||
|             mingw-w64-x86_64-openjpeg2 \ | ||||
|             mingw-w64-x86_64-libimagequant \ | ||||
|             mingw-w64-x86_64-libraqm | ||||
| 
 | ||||
|     https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with | ||||
|     MSYS2. To workaround this, before installing Pillow you must run:: | ||||
| 
 | ||||
|         export SETUPTOOLS_USE_DISTUTILS=stdlib | ||||
| 
 | ||||
| .. tab:: FreeBSD | ||||
| 
 | ||||
|     .. Note:: Only FreeBSD 10 and 11 tested | ||||
| 
 | ||||
|     Make sure you have Python's development libraries installed:: | ||||
| 
 | ||||
|         sudo pkg install python3 | ||||
| 
 | ||||
|     Prerequisites are installed on **FreeBSD 10 or 11** with:: | ||||
| 
 | ||||
|         sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb | ||||
| 
 | ||||
|     Then see ``depends/install_raqm_cmake.sh`` to install libraqm. | ||||
| 
 | ||||
| .. tab:: Android | ||||
| 
 | ||||
|     Basic Android support has been added for compilation within the Termux | ||||
|     environment. The dependencies can be installed by:: | ||||
| 
 | ||||
|         pkg install -y python ndk-sysroot clang make \ | ||||
|             libjpeg-turbo | ||||
| 
 | ||||
|     This has been tested within the Termux app on ChromeOS, on x86. | ||||
| 
 | ||||
| Installing | ||||
| ---------- | ||||
| 
 | ||||
| Once you have installed the prerequisites, to install Pillow from the source | ||||
| code on PyPI, run:: | ||||
| 
 | ||||
|     python3 -m pip install --upgrade pip | ||||
|     python3 -m pip install --upgrade Pillow --no-binary :all: | ||||
| 
 | ||||
| If the prerequisites are installed in the standard library locations | ||||
| for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no | ||||
| additional configuration should be required. If they are installed in | ||||
| a non-standard location, you may need to configure setuptools to use | ||||
| those locations by editing :file:`setup.py` or | ||||
| :file:`pyproject.toml`, or by adding environment variables on the command | ||||
| line:: | ||||
| 
 | ||||
|     CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: | ||||
| 
 | ||||
| If Pillow has been previously built without the required | ||||
| prerequisites, it may be necessary to manually clear the pip cache or | ||||
| build without cache using the ``--no-cache-dir`` option to force a | ||||
| build with newly installed external libraries. | ||||
| 
 | ||||
| If you would like to install from a local copy of the source code instead, you | ||||
| can clone from GitHub with ``git clone https://github.com/python-pillow/Pillow`` | ||||
| or download and extract the `compressed archive from PyPI`_. | ||||
| 
 | ||||
| After navigating to the Pillow directory, run:: | ||||
| 
 | ||||
|     python3 -m pip install --upgrade pip | ||||
|     python3 -m pip install . | ||||
| 
 | ||||
| .. _compressed archive from PyPI: https://pypi.org/project/pillow/#files | ||||
| 
 | ||||
| Build Options | ||||
| ^^^^^^^^^^^^^ | ||||
| 
 | ||||
| * Config setting: ``-C parallel=n``. Can also be given | ||||
|   with environment variable: ``MAX_CONCURRENCY=n``. Pillow can use | ||||
|   multiprocessing to build the extension. Setting ``-C parallel=n`` | ||||
|   sets the number of CPUs to use to ``n``, or can disable parallel building by | ||||
|   using a setting of 1. By default, it uses 4 CPUs, or if 4 are not | ||||
|   available, as many as are present. | ||||
| 
 | ||||
| * 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 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 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. | ||||
| 
 | ||||
| * Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. | ||||
|   These flags are used to compile a modified version of libraqm and | ||||
|   a shim that dynamically loads libfribidi at runtime. These are | ||||
|   used to compile the standard Pillow wheels. Compiling libraqm requires | ||||
|   a C99-compliant compiler. | ||||
| 
 | ||||
| * Config setting: ``-C platform-guessing=disable``. Skips all of the | ||||
|   platform dependent guessing of include and library directories for | ||||
|   automated build systems that configure the proper paths in the | ||||
|   environment variables (e.g. Buildroot). | ||||
| 
 | ||||
| * Config setting: ``-C debug=true``. Adds a debugging flag to the include and | ||||
|   library search process to dump all paths searched for and found to stdout. | ||||
| 
 | ||||
| 
 | ||||
| Sample usage:: | ||||
| 
 | ||||
|     python3 -m pip install --upgrade Pillow -C [feature]=enable | ||||
| 
 | ||||
| .. _old-versions: | ||||
| 
 | ||||
| Old Versions | ||||
| ============ | ||||
| 
 | ||||
| You can download old distributions from the `release history at PyPI | ||||
| <https://pypi.org/project/pillow/#history>`_ and by direct URL access | ||||
| eg. https://pypi.org/project/pillow/1.0/. | ||||
							
								
								
									
										10
									
								
								docs/installation/index.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,10 @@ | |||
| Installation | ||||
| ============ | ||||
| 
 | ||||
| .. toctree:: | ||||
|   :maxdepth: 2 | ||||
| 
 | ||||
|   basic-installation | ||||
|   python-support | ||||
|   platform-support | ||||
|   building-from-source | ||||
							
								
								
									
										168
									
								
								docs/installation/platform-support.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,168 @@ | |||
| .. _platform-support: | ||||
| 
 | ||||
| Platform Support | ||||
| ================ | ||||
| 
 | ||||
| Current platform support for Pillow. Binary distributions are | ||||
| contributed for each release on a volunteer basis, but the source | ||||
| should compile and run everywhere platform support is listed. In | ||||
| general, we aim to support all current versions of Linux, macOS, and | ||||
| Windows. | ||||
| 
 | ||||
| Continuous Integration Targets | ||||
| ------------------------------ | ||||
| 
 | ||||
| These platforms are built and tested for every change. | ||||
| 
 | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Operating system                 | Tested Python versions     | Tested architecture | | ||||
| +==================================+============================+=====================+ | ||||
| | Alpine                           | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Amazon Linux 2                   | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Amazon Linux 2023                | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Arch                             | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | CentOS 7                         | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | CentOS Stream 8                  | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | CentOS Stream 9                  | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Debian 11 Bullseye               | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Debian 12 Bookworm               | 3.11                       | x86, x86-64         | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Fedora 38                        | 3.11                       | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Fedora 39                        | 3.12                       | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Gentoo                           | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | macOS 12 Monterey                | 3.8, 3.9, 3.10, 3.11,      | x86-64              | | ||||
| |                                  | 3.12, PyPy3                |                     | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Ubuntu Linux 20.04 LTS (Focal)   | 3.8                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Ubuntu Linux 22.04 LTS (Jammy)   | 3.8, 3.9, 3.10, 3.11,      | x86-64              | | ||||
| |                                  | 3.12, PyPy3                |                     | | ||||
| |                                  +----------------------------+---------------------+ | ||||
| |                                  | 3.10                       | arm64v8, ppc64le,   | | ||||
| |                                  |                            | s390x               | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Windows Server 2016              | 3.8                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Windows Server 2022              | 3.8, 3.9, 3.10, 3.11,      | x86-64              | | ||||
| |                                  | 3.12, PyPy3                |                     | | ||||
| |                                  +----------------------------+---------------------+ | ||||
| |                                  | 3.12                       | x86                 | | ||||
| |                                  +----------------------------+---------------------+ | ||||
| |                                  | 3.9 (MinGW)                | x86-64              | | ||||
| |                                  +----------------------------+---------------------+ | ||||
| |                                  | 3.8, 3.9 (Cygwin)          | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| 
 | ||||
| 
 | ||||
| Other Platforms | ||||
| --------------- | ||||
| 
 | ||||
| These platforms have been reported to work at the versions mentioned. | ||||
| 
 | ||||
| .. note:: | ||||
| 
 | ||||
|     Contributors please test Pillow on your platform then update this | ||||
|     document and send a pull request. | ||||
| 
 | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Operating system                 | | Tested Python            | | Latest tested  | | Tested     | | ||||
| |                                  | | versions                 | | Pillow version | | processors | | ||||
| +==================================+============================+==================+==============+ | ||||
| | macOS 14 Sonoma                  | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.2.0           |arm           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 13 Ventura                 | 3.8, 3.9, 3.10, 3.11       | 10.0.1           |arm           | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 3.7                        | 9.5.0            |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 12 Monterey                | 3.7, 3.8, 3.9, 3.10, 3.11  | 9.3.0            |arm           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 11 Big Sur                 | 3.7, 3.8, 3.9, 3.10        | 8.4.0            |arm           | | ||||
| |                                  +----------------------------+------------------+--------------+ | ||||
| |                                  | 3.7, 3.8, 3.9, 3.10, 3.11  | 9.4.0            |x86-64        | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 3.6                        | 8.4.0            |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 10.15 Catalina             | 3.6, 3.7, 3.8, 3.9         | 8.3.2            |x86-64        | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 3.5                        | 7.2.0            |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 10.14 Mojave               | 3.5, 3.6, 3.7, 3.8         | 7.2.0            |x86-64        | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 2.7                        | 6.0.0            |              | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 3.4                        | 5.4.1            |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 10.13 High Sierra          | 2.7, 3.4, 3.5, 3.6         | 4.2.1            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 10.12 Sierra               | 2.7, 3.4, 3.5, 3.6         | 4.1.1            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Mac OS X 10.11 El Capitan        | 2.7, 3.4, 3.5, 3.6, 3.7    | 5.4.1            |x86-64        | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 3.3                        | 4.1.0            |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Mac OS X 10.9 Mavericks          | 2.7, 3.2, 3.3, 3.4         | 3.0.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Mac OS X 10.8 Mountain Lion      | 2.6, 2.7, 3.2, 3.3         |                  |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Redhat Linux 6                   | 2.6                        |                  |x86           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | CentOS 6.3                       | 2.7, 3.3                   |                  |x86           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | CentOS 8                         | 3.9                        | 9.0.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Fedora 23                        | 2.7, 3.4                   | 3.1.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5  | 3.4.1            |x86,x86-64    | | ||||
| |                                  | | PyPy5.3.1, PyPy3 v2.4.0  |                  |              | | ||||
| |                                  +----------------------------+------------------+--------------+ | ||||
| |                                  | 2.7                        | 4.3.0            |x86-64        | | ||||
| |                                  +----------------------------+------------------+--------------+ | ||||
| |                                  | 2.7, 3.2                   | 3.4.1            |ppc           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Ubuntu Linux 10.04 LTS (Lucid)   | 2.6                        | 2.3.0            |x86,x86-64    | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Debian 8.2 Jessie                | 2.7, 3.4                   | 3.1.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Raspbian Jessie                  | 2.7, 3.4                   | 3.1.0            |arm           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Raspbian Stretch                 | 2.7, 3.5                   | 4.0.0            |arm           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Raspberry Pi OS                  | 3.6, 3.7, 3.8, 3.9         | 8.2.0            |arm           | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 2.7                        | 6.2.2            |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Gentoo Linux                     | 2.7, 3.2                   | 2.1.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | FreeBSD 11.1                     | 2.7, 3.4, 3.5, 3.6         | 4.3.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | FreeBSD 10.3                     | 2.7, 3.4, 3.5              | 4.2.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | FreeBSD 10.2                     | 2.7, 3.4                   | 3.1.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 11                       | 3.9, 3.10, 3.11, 3.12      | 10.2.0           |arm64         | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 11 Pro                   | 3.11, 3.12                 | 10.2.0           |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 10                       | 3.7                        | 7.1.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 10/Cygwin 3.3            | 3.6, 3.7, 3.8, 3.9         | 8.4.0            |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 8.1 Pro                  | 2.6, 2.7, 3.2, 3.3, 3.4    | 2.4.0            |x86,x86-64    | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 8 Pro                    | 2.6, 2.7, 3.2, 3.3, 3.4a3  | 2.2.0            |x86,x86-64    | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 7 Professional           | 3.7                        | 7.0.0            |x86,x86-64    | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows Server 2008 R2 Enterprise| 3.3                        |                  |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
							
								
								
									
										14
									
								
								docs/installation/python-support.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,14 @@ | |||
| .. _python-support: | ||||
| 
 | ||||
| Python Support | ||||
| ============== | ||||
| 
 | ||||
| Pillow supports these Python versions. | ||||
| 
 | ||||
| .. csv-table:: Newer versions | ||||
|    :file: newer-versions.csv | ||||
|    :header-rows: 1 | ||||
| 
 | ||||
| .. csv-table:: Older versions | ||||
|    :file: older-versions.csv | ||||
|    :header-rows: 1 | ||||
|  | @ -14,6 +14,8 @@ only work on L and RGB images. | |||
| .. autofunction:: colorize | ||||
| .. autofunction:: crop | ||||
| .. autofunction:: scale | ||||
| .. autoclass:: SupportsGetMesh | ||||
|     :show-inheritance: | ||||
| .. autofunction:: deform | ||||
| .. autofunction:: equalize | ||||
| .. autofunction:: expand | ||||
|  |  | |||
|  | @ -79,3 +79,9 @@ Portable FloatMap (PFM) images | |||
| 
 | ||||
| Support has been added for reading and writing grayscale (Pf format) | ||||
| Portable FloatMap (PFM) files containing ``F`` data. | ||||
| 
 | ||||
| Release GIL when fetching WebP frames | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Python's Global Interpreter Lock is now released when fetching WebP frames from | ||||
| the libwebp decoder. | ||||
|  |  | |||
|  | @ -33,6 +33,7 @@ classifiers = [ | |||
|   "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", | ||||
|   "Topic :: Multimedia :: Graphics :: Graphics Conversion", | ||||
|   "Topic :: Multimedia :: Graphics :: Viewers", | ||||
|   "Typing :: Typed", | ||||
| ] | ||||
| dynamic = [ | ||||
|   "version", | ||||
|  | @ -79,7 +80,6 @@ Homepage = "https://python-pillow.org" | |||
| Mastodon = "https://fosstodon.org/@pillow" | ||||
| "Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" | ||||
| Source = "https://github.com/python-pillow/Pillow" | ||||
| Twitter = "https://twitter.com/PythonPillow" | ||||
| 
 | ||||
| [tool.setuptools] | ||||
| packages = ["PIL"] | ||||
|  | @ -140,7 +140,3 @@ follow_imports = "silent" | |||
| warn_redundant_casts = true | ||||
| warn_unreachable = true | ||||
| warn_unused_ignores = true | ||||
| exclude = [ | ||||
|   '^src/PIL/FpxImagePlugin.py$', | ||||
|   '^src/PIL/MicImagePlugin.py$', | ||||
| ] | ||||
|  |  | |||
|  | @ -38,7 +38,7 @@ from ._deprecate import deprecate | |||
| split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") | ||||
| field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") | ||||
| 
 | ||||
| gs_binary = None | ||||
| gs_binary: str | bool | None = None | ||||
| gs_windows_binary = None | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||