Merge branch 'main' into build-editable
|  | @ -6,6 +6,7 @@ init: | ||||||
| # Uncomment previous line to get RDP access during the build. | # Uncomment previous line to get RDP access during the build. | ||||||
| 
 | 
 | ||||||
| environment: | environment: | ||||||
|  |   COVERAGE_CORE: sysmon | ||||||
|   EXECUTABLE: python.exe |   EXECUTABLE: python.exe | ||||||
|   TEST_OPTIONS: |   TEST_OPTIONS: | ||||||
|   DEPLOY: YES |   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: |     paths: | ||||||
|       - ".github/workflows/docs.yml" |       - ".github/workflows/docs.yml" | ||||||
|       - "docs/**" |       - "docs/**" | ||||||
|  |       - "src/PIL/**" | ||||||
|   pull_request: |   pull_request: | ||||||
|     paths: |     paths: | ||||||
|       - ".github/workflows/docs.yml" |       - ".github/workflows/docs.yml" | ||||||
|       - "docs/**" |       - "docs/**" | ||||||
|  |       - "src/PIL/**" | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
| 
 | 
 | ||||||
| permissions: | permissions: | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								.github/workflows/test-cygwin.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -26,6 +26,9 @@ concurrency: | ||||||
|   group: ${{ github.workflow }}-${{ github.ref }} |   group: ${{ github.workflow }}-${{ github.ref }} | ||||||
|   cancel-in-progress: true |   cancel-in-progress: true | ||||||
| 
 | 
 | ||||||
|  | env: | ||||||
|  |   COVERAGE_CORE: sysmon | ||||||
|  | 
 | ||||||
| jobs: | jobs: | ||||||
|   build: |   build: | ||||||
|     runs-on: windows-latest |     runs-on: windows-latest | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								.github/workflows/test-mingw.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -26,6 +26,9 @@ concurrency: | ||||||
|   group: ${{ github.workflow }}-${{ github.ref }} |   group: ${{ github.workflow }}-${{ github.ref }} | ||||||
|   cancel-in-progress: true |   cancel-in-progress: true | ||||||
| 
 | 
 | ||||||
|  | env: | ||||||
|  |   COVERAGE_CORE: sysmon | ||||||
|  | 
 | ||||||
| jobs: | jobs: | ||||||
|   build: |   build: | ||||||
|     runs-on: windows-latest |     runs-on: windows-latest | ||||||
|  | @ -64,10 +67,10 @@ jobs: | ||||||
|               mingw-w64-x86_64-python3-cffi \ |               mingw-w64-x86_64-python3-cffi \ | ||||||
|               mingw-w64-x86_64-python3-numpy \ |               mingw-w64-x86_64-python3-numpy \ | ||||||
|               mingw-w64-x86_64-python3-olefile \ |               mingw-w64-x86_64-python3-olefile \ | ||||||
|               mingw-w64-x86_64-python3-pip \ |  | ||||||
|               mingw-w64-x86_64-python3-setuptools \ |               mingw-w64-x86_64-python3-setuptools \ | ||||||
|               mingw-w64-x86_64-python-pyqt6 |               mingw-w64-x86_64-python-pyqt6 | ||||||
| 
 | 
 | ||||||
|  |           python3 -m ensurepip | ||||||
|           python3 -m pip install pyroma pytest pytest-cov pytest-timeout |           python3 -m pip install pyroma pytest pytest-cov pytest-timeout | ||||||
| 
 | 
 | ||||||
|           pushd depends && ./install_extra_test_images.sh && popd |           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 }} |   group: ${{ github.workflow }}-${{ github.ref }} | ||||||
|   cancel-in-progress: true |   cancel-in-progress: true | ||||||
| 
 | 
 | ||||||
|  | env: | ||||||
|  |   COVERAGE_CORE: sysmon | ||||||
|  | 
 | ||||||
| jobs: | jobs: | ||||||
|   build: |   build: | ||||||
|     runs-on: windows-latest |     runs-on: windows-latest | ||||||
|     strategy: |     strategy: | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       matrix: |       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 |     timeout-minutes: 30 | ||||||
| 
 | 
 | ||||||
|  | @ -66,8 +69,16 @@ jobs: | ||||||
|     - name: Print build system information |     - name: Print build system information | ||||||
|       run: python3 .github/workflows/system-info.py |       run: python3 .github/workflows/system-info.py | ||||||
| 
 | 
 | ||||||
|     - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma |     - name: Install Python dependencies | ||||||
|       run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma |       run: > | ||||||
|  |         python3 -m pip install | ||||||
|  |         coverage>=7.4.2 | ||||||
|  |         defusedxml | ||||||
|  |         olefile | ||||||
|  |         pyroma | ||||||
|  |         pytest | ||||||
|  |         pytest-cov | ||||||
|  |         pytest-timeout | ||||||
| 
 | 
 | ||||||
|     - name: Install dependencies |     - name: Install dependencies | ||||||
|       id: install |       id: install | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -27,6 +27,7 @@ concurrency: | ||||||
|   cancel-in-progress: true |   cancel-in-progress: true | ||||||
| 
 | 
 | ||||||
| env: | env: | ||||||
|  |   COVERAGE_CORE: sysmon | ||||||
|   FORCE_COLOR: 1 |   FORCE_COLOR: 1 | ||||||
| 
 | 
 | ||||||
| jobs: | jobs: | ||||||
|  |  | ||||||
							
								
								
									
										31
									
								
								.github/workflows/wheels-dependencies.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -19,7 +19,7 @@ FREETYPE_VERSION=2.13.2 | ||||||
| HARFBUZZ_VERSION=8.3.0 | HARFBUZZ_VERSION=8.3.0 | ||||||
| LIBPNG_VERSION=1.6.40 | LIBPNG_VERSION=1.6.40 | ||||||
| JPEGTURBO_VERSION=3.0.1 | JPEGTURBO_VERSION=3.0.1 | ||||||
| OPENJPEG_VERSION=2.5.0 | OPENJPEG_VERSION=2.5.2 | ||||||
| XZ_VERSION=5.4.5 | XZ_VERSION=5.4.5 | ||||||
| TIFF_VERSION=4.6.0 | TIFF_VERSION=4.6.0 | ||||||
| LCMS2_VERSION=2.16 | LCMS2_VERSION=2.16 | ||||||
|  | @ -40,7 +40,7 @@ BROTLI_VERSION=1.1.0 | ||||||
| 
 | 
 | ||||||
| if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then | if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then | ||||||
|     function build_openjpeg { |     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 \ |         (cd $out_dir \ | ||||||
|             && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ |             && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ | ||||||
|             && make install) |             && make install) | ||||||
|  | @ -62,7 +62,7 @@ function build_brotli { | ||||||
| 
 | 
 | ||||||
| function build { | function build { | ||||||
|     if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then |     if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then | ||||||
|         export BUILD_PREFIX="/usr/local" |         sudo chown -R runner /usr/local | ||||||
|     fi |     fi | ||||||
|     build_xz |     build_xz | ||||||
|     if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then |     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 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 libXau 1.0.11 https://www.x.org/pub/individual/lib | ||||||
|         build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist |         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 |         if [[ "$CIBW_ARCHS" == "arm64" ]]; then | ||||||
|             cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc |             cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig | ||||||
|         fi |         fi | ||||||
|     else |     else | ||||||
|         sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc |         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_tiff | ||||||
|     build_libpng |     build_libpng | ||||||
|     build_lcms2 |     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 |     build_openjpeg | ||||||
|  |     if [ -f /usr/local/lib64/libopenjp2.so ]; then | ||||||
|  |         cp /usr/local/lib64/libopenjp2.so /usr/local/lib | ||||||
|  |     fi | ||||||
| 
 | 
 | ||||||
|     ORIGINAL_CFLAGS=$CFLAGS |     ORIGINAL_CFLAGS=$CFLAGS | ||||||
|     CFLAGS="$CFLAGS -O3 -DNDEBUG" |     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 | untar pillow-depends-main.zip | ||||||
| 
 | 
 | ||||||
| if [[ -n "$IS_MACOS" ]]; then | 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 |   # 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 cairo to fix building harfbuzz on arm64 | ||||||
|   # remove lcms2 and libpng to fix building openjpeg 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 |   # 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 |   brew install pkg-config | ||||||
| fi | fi | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								.github/workflows/wheels-test.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -4,6 +4,9 @@ set -e | ||||||
| if [[ "$OSTYPE" == "darwin"* ]]; then | if [[ "$OSTYPE" == "darwin"* ]]; then | ||||||
|     brew install fribidi |     brew install fribidi | ||||||
|     export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" |     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 | elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then | ||||||
|     apk add curl fribidi |     apk add curl fribidi | ||||||
| else | else | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -101,7 +101,7 @@ jobs: | ||||||
|             cibw_arch: x86_64 |             cibw_arch: x86_64 | ||||||
|             macosx_deployment_target: "10.10" |             macosx_deployment_target: "10.10" | ||||||
|           - name: "macOS arm64" |           - name: "macOS arm64" | ||||||
|             os: macos-latest |             os: macos-14 | ||||||
|             cibw_arch: arm64 |             cibw_arch: arm64 | ||||||
|             macosx_deployment_target: "11.0" |             macosx_deployment_target: "11.0" | ||||||
|           - name: "manylinux2014 and musllinux x86_64" |           - name: "manylinux2014 and musllinux x86_64" | ||||||
|  | @ -134,7 +134,7 @@ jobs: | ||||||
|           CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} |           CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} | ||||||
|           CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} |           CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} | ||||||
|           CIBW_SKIP: pp38-* |           CIBW_SKIP: pp38-* | ||||||
|           CIBW_TEST_SKIP: "*-macosx_arm64" |           CIBW_TEST_SKIP: cp38-macosx_arm64 | ||||||
|           MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} |           MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} | ||||||
| 
 | 
 | ||||||
|       - uses: actions/upload-artifact@v4 |       - uses: actions/upload-artifact@v4 | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								CHANGES.rst
									
									
									
									
									
								
							
							
						
						|  | @ -5,6 +5,21 @@ Changelog (Pillow) | ||||||
| 10.3.0 (unreleased) | 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 | - Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745 | ||||||
|   [nik012003, radarhere] |   [nik012003, radarhere] | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -64,7 +64,7 @@ As of 2019, Pillow development is | ||||||
|                 src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"></a> |                 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 |             <a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img | ||||||
|                 alt="Tidelift" |                 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 |             <a href="https://pypi.org/project/pillow/"><img | ||||||
|                 alt="Newest PyPI version" |                 alt="Newest PyPI version" | ||||||
|                 src="https://img.shields.io/pypi/v/pillow.svg"></a> |                 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 |             <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" |                 alt="Join the chat at https://gitter.im/python-pillow/Pillow" | ||||||
|                 src="https://badges.gitter.im/python-pillow/Pillow.svg"></a> |                 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 |             <a href="https://fosstodon.org/@pillow"><img | ||||||
|                 alt="Follow on https://fosstodon.org/@pillow" |                 alt="Follow on https://fosstodon.org/@pillow" | ||||||
|                 src="https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg" |                 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 | ## 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 | ## Documentation | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -23,7 +23,10 @@ def _get_mem_usage() -> float: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _test_leak( | 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: | ) -> None: | ||||||
|     mem_limit = None |     mem_limit = None | ||||||
|     for i in range(max_iterations): |     for i in range(max_iterations): | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ def test_ignore_dos_text() -> None: | ||||||
|     finally: |     finally: | ||||||
|         ImageFile.LOAD_TRUNCATED_IMAGES = False |         ImageFile.LOAD_TRUNCATED_IMAGES = False | ||||||
| 
 | 
 | ||||||
|  |     assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|     for s in im.text.values(): |     for s in im.text.values(): | ||||||
|         assert len(s) < 1024 * 1024, "Text chunk larger than 1M" |         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" |         assert msg, "Decompressed Data Too Large" | ||||||
|         return |         return | ||||||
| 
 | 
 | ||||||
|  |     assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|     for s in im.text.values(): |     for s in im.text.values(): | ||||||
|         assert len(s) < 1024 * 1024, "Text chunk larger than 1M" |         assert len(s) < 1024 * 1024, "Text chunk larger than 1M" | ||||||
| 
 | 
 | ||||||
|  | @ -57,6 +59,7 @@ def test_dos_total_memory() -> None: | ||||||
|         return |         return | ||||||
| 
 | 
 | ||||||
|     total_len = 0 |     total_len = 0 | ||||||
|  |     assert isinstance(im2, PngImagePlugin.PngImageFile) | ||||||
|     for txt in im2.text.values(): |     for txt in im2.text.values(): | ||||||
|         total_len += len(txt) |         total_len += len(txt) | ||||||
|     assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M" |     assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M" | ||||||
|  |  | ||||||
|  | @ -351,7 +351,7 @@ def is_mingw() -> bool: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class CachedProperty: | class CachedProperty: | ||||||
|     def __init__(self, func: Callable[[Any], None]) -> None: |     def __init__(self, func: Callable[[Any], Any]) -> None: | ||||||
|         self.func = func |         self.func = func | ||||||
| 
 | 
 | ||||||
|     def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any: |     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): |     with pytest.warns(DeprecationWarning, match=expected): | ||||||
|         _deprecate.deprecate("Old thing", version, "new thing") |         _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"" |     expected = r"" | ||||||
|     with pytest.raises(RuntimeError, match=expected): |     with pytest.raises(RuntimeError, match=expected): | ||||||
|         _deprecate.deprecate(deprecated, 1, plural=plural) |         _deprecate.deprecate(deprecated, 1, plural=plural) | ||||||
|  | @ -76,7 +76,7 @@ def test_replacement_and_action() -> None: | ||||||
|         "Upgrade to new thing.", |         "Upgrade to new thing.", | ||||||
|     ], |     ], | ||||||
| ) | ) | ||||||
| def test_action(action) -> None: | def test_action(action: str) -> None: | ||||||
|     expected = ( |     expected = ( | ||||||
|         r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " |         r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " | ||||||
|         r"Upgrade to new thing\." |         r"Upgrade to new thing\." | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ from __future__ import annotations | ||||||
| 
 | 
 | ||||||
| import io | import io | ||||||
| import re | import re | ||||||
|  | from typing import Callable | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
|  | @ -29,7 +30,7 @@ def test_version() -> None: | ||||||
|     # Check the correctness of the convenience function |     # Check the correctness of the convenience function | ||||||
|     # and the format of version numbers |     # and the format of version numbers | ||||||
| 
 | 
 | ||||||
|     def test(name, function) -> None: |     def test(name: str, function: Callable[[str], bool]) -> None: | ||||||
|         version = features.version(name) |         version = features.version(name) | ||||||
|         if not features.check(name): |         if not features.check(name): | ||||||
|             assert version is None |             assert version is None | ||||||
|  | @ -73,12 +74,12 @@ def test_libimagequant_version() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("feature", features.modules) | @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] |     assert features.check_module(feature) in [True, False] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("feature", features.codecs) | @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] |     assert features.check_codec(feature) in [True, False] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -71,7 +71,7 @@ def test_save(tmp_path: Path) -> None: | ||||||
|         "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", |         "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 open(test_file, "rb") as f: | ||||||
|         with Image.open(f) as im: |         with Image.open(f) as im: | ||||||
|             with pytest.raises(OSError): |             with pytest.raises(OSError): | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ from .helper import ( | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_sanity(tmp_path: Path) -> None: | def test_sanity(tmp_path: Path) -> None: | ||||||
|     def roundtrip(im) -> None: |     def roundtrip(im: Image.Image) -> None: | ||||||
|         outfile = str(tmp_path / "temp.bmp") |         outfile = str(tmp_path / "temp.bmp") | ||||||
| 
 | 
 | ||||||
|         im.save(outfile, "BMP") |         im.save(outfile, "BMP") | ||||||
|  | @ -194,7 +194,7 @@ def test_rle4() -> None: | ||||||
|         ("Tests/images/bmp/g/pal8rle.bmp", 1064), |         ("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: |     with open(file_name, "rb") as fp: | ||||||
|         data = fp.read(length) |         data = fp.read(length) | ||||||
|         with Image.open(io.BytesIO(data)) as im: |         with Image.open(io.BytesIO(data)) as im: | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
|  | from typing import Literal | ||||||
|  | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
| from PIL import ContainerIO, Image | from PIL import ContainerIO, Image | ||||||
|  | @ -21,9 +23,16 @@ def test_isatty() -> None: | ||||||
|     assert container.isatty() is False |     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 |     # Arrange | ||||||
|     mode = 0 |  | ||||||
|     with open(TEST_FILE, "rb") as fh: |     with open(TEST_FILE, "rb") as fh: | ||||||
|         container = ContainerIO.ContainerIO(fh, 22, 100) |         container = ContainerIO.ContainerIO(fh, 22, 100) | ||||||
| 
 | 
 | ||||||
|  | @ -32,35 +41,7 @@ def test_seek_mode_0() -> None: | ||||||
|         container.seek(33, mode) |         container.seek(33, mode) | ||||||
| 
 | 
 | ||||||
|         # Assert |         # Assert | ||||||
|         assert container.tell() == 33 |         assert container.tell() == expected_position | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 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 |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("bytesmode", (True, False)) | @pytest.mark.parametrize("bytesmode", (True, False)) | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ import warnings | ||||||
| 
 | 
 | ||||||
| import pytest | 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 | 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. | # save as...-> hopper.fli, default options. | ||||||
| static_test_file = "Tests/images/hopper.fli" | 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" | 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: | def test_sanity() -> None: | ||||||
|     with Image.open(static_test_file) as im: |     with Image.open(static_test_file) as im: | ||||||
|  | @ -32,6 +35,24 @@ def test_sanity() -> None: | ||||||
|         assert im.is_animated |         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") | @pytest.mark.skipif(is_pypy(), reason="Requires CPython") | ||||||
| def test_unclosed_file() -> None: | def test_unclosed_file() -> None: | ||||||
|     def open() -> None: |     def open() -> None: | ||||||
|  |  | ||||||
|  | @ -1113,6 +1113,21 @@ def test_append_images(tmp_path: Path) -> None: | ||||||
|         assert reread.n_frames == 10 |         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: | def test_transparent_optimize(tmp_path: Path) -> None: | ||||||
|     # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses |     # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses | ||||||
|     # transparency. |     # transparency. | ||||||
|  |  | ||||||
|  | @ -135,7 +135,7 @@ def test_different_bit_depths(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) | @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() |     output = io.BytesIO() | ||||||
|     im = hopper(mode) |     im = hopper(mode) | ||||||
|     im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) |     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")) | @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") |     out = str(tmp_path / "temp.im") | ||||||
|     im = hopper(mode) |     im = hopper(mode) | ||||||
|     im.save(out) |     im.save(out) | ||||||
|  |  | ||||||
|  | @ -98,7 +98,7 @@ def test_i() -> None: | ||||||
|     assert ret == 97 |     assert ret == 97 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_dump(monkeypatch) -> None: | def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     c = b"abc" |     c = b"abc" | ||||||
|     # Temporarily redirect stdout |     # Temporarily redirect stdout | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ import warnings | ||||||
| from io import BytesIO | from io import BytesIO | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from types import ModuleType | from types import ModuleType | ||||||
| from typing import Any | from typing import Any, cast | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
|  | @ -45,14 +45,20 @@ TEST_FILE = "Tests/images/hopper.jpg" | ||||||
| 
 | 
 | ||||||
| @skip_unless_feature("jpg") | @skip_unless_feature("jpg") | ||||||
| class TestFileJpeg: | 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() |         out = BytesIO() | ||||||
|         im.save(out, "JPEG", **options) |         im.save(out, "JPEG", **options) | ||||||
|         test_bytes = out.tell() |         test_bytes = out.tell() | ||||||
|         out.seek(0) |         out.seek(0) | ||||||
|         im = Image.open(out) |         reloaded = cast(JpegImagePlugin.JpegImageFile, Image.open(out)) | ||||||
|         im.bytes = test_bytes  # for testing only |         return reloaded, test_bytes | ||||||
|         return im | 
 | ||||||
|  |     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: |     def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image: | ||||||
|         """Generates a very hard to compress file |         """Generates a very hard to compress file | ||||||
|  | @ -246,13 +252,13 @@ class TestFileJpeg: | ||||||
|             im.save(f, progressive=True, quality=94, exif=b" " * 43668) |             im.save(f, progressive=True, quality=94, exif=b" " * 43668) | ||||||
| 
 | 
 | ||||||
|     def test_optimize(self) -> None: |     def test_optimize(self) -> None: | ||||||
|         im1 = self.roundtrip(hopper()) |         im1, im1_bytes = self.roundtrip_with_bytes(hopper()) | ||||||
|         im2 = self.roundtrip(hopper(), optimize=0) |         im2, im2_bytes = self.roundtrip_with_bytes(hopper(), optimize=0) | ||||||
|         im3 = self.roundtrip(hopper(), optimize=1) |         im3, im3_bytes = self.roundtrip_with_bytes(hopper(), optimize=1) | ||||||
|         assert_image_equal(im1, im2) |         assert_image_equal(im1, im2) | ||||||
|         assert_image_equal(im1, im3) |         assert_image_equal(im1, im3) | ||||||
|         assert im1.bytes >= im2.bytes |         assert im1_bytes >= im2_bytes | ||||||
|         assert im1.bytes >= im3.bytes |         assert im1_bytes >= im3_bytes | ||||||
| 
 | 
 | ||||||
|     def test_optimize_large_buffer(self, tmp_path: Path) -> None: |     def test_optimize_large_buffer(self, tmp_path: Path) -> None: | ||||||
|         # https://github.com/python-pillow/Pillow/issues/148 |         # https://github.com/python-pillow/Pillow/issues/148 | ||||||
|  | @ -262,15 +268,15 @@ class TestFileJpeg: | ||||||
|         im.save(f, format="JPEG", optimize=True) |         im.save(f, format="JPEG", optimize=True) | ||||||
| 
 | 
 | ||||||
|     def test_progressive(self) -> None: |     def test_progressive(self) -> None: | ||||||
|         im1 = self.roundtrip(hopper()) |         im1, im1_bytes = self.roundtrip_with_bytes(hopper()) | ||||||
|         im2 = self.roundtrip(hopper(), progressive=False) |         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 im1.info.get("progressive") | ||||||
|         assert not im2.info.get("progressive") |         assert not im2.info.get("progressive") | ||||||
|         assert im3.info.get("progressive") |         assert im3.info.get("progressive") | ||||||
| 
 | 
 | ||||||
|         assert_image_equal(im1, im3) |         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: |     def test_progressive_large_buffer(self, tmp_path: Path) -> None: | ||||||
|         f = str(tmp_path / "temp.jpg") |         f = str(tmp_path / "temp.jpg") | ||||||
|  | @ -341,6 +347,7 @@ class TestFileJpeg: | ||||||
|             assert exif.get_ifd(0x8825) == {} |             assert exif.get_ifd(0x8825) == {} | ||||||
| 
 | 
 | ||||||
|             transposed = ImageOps.exif_transpose(im) |             transposed = ImageOps.exif_transpose(im) | ||||||
|  |         assert transposed is not None | ||||||
|         exif = transposed.getexif() |         exif = transposed.getexif() | ||||||
|         assert exif.get_ifd(0x8825) == {} |         assert exif.get_ifd(0x8825) == {} | ||||||
| 
 | 
 | ||||||
|  | @ -419,14 +426,14 @@ class TestFileJpeg: | ||||||
|         assert im3.info.get("progression") |         assert im3.info.get("progression") | ||||||
| 
 | 
 | ||||||
|     def test_quality(self) -> None: |     def test_quality(self) -> None: | ||||||
|         im1 = self.roundtrip(hopper()) |         im1, im1_bytes = self.roundtrip_with_bytes(hopper()) | ||||||
|         im2 = self.roundtrip(hopper(), quality=50) |         im2, im2_bytes = self.roundtrip_with_bytes(hopper(), quality=50) | ||||||
|         assert_image(im1, im2.mode, im2.size) |         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_image(im1, im3.mode, im3.size) | ||||||
|         assert im2.bytes > im3.bytes |         assert im2_bytes > im3_bytes | ||||||
| 
 | 
 | ||||||
|     def test_smooth(self) -> None: |     def test_smooth(self) -> None: | ||||||
|         im1 = self.roundtrip(hopper()) |         im1 = self.roundtrip(hopper()) | ||||||
|  |  | ||||||
|  | @ -40,10 +40,8 @@ test_card.load() | ||||||
| def roundtrip(im: Image.Image, **options: Any) -> Image.Image: | def roundtrip(im: Image.Image, **options: Any) -> Image.Image: | ||||||
|     out = BytesIO() |     out = BytesIO() | ||||||
|     im.save(out, "JPEG2000", **options) |     im.save(out, "JPEG2000", **options) | ||||||
|     test_bytes = out.tell() |  | ||||||
|     out.seek(0) |     out.seek(0) | ||||||
|     with Image.open(out) as im: |     with Image.open(out) as im: | ||||||
|         im.bytes = test_bytes  # for testing only |  | ||||||
|         im.load() |         im.load() | ||||||
|     return im |     return im | ||||||
| 
 | 
 | ||||||
|  | @ -77,7 +75,9 @@ def test_invalid_file() -> None: | ||||||
| def test_bytesio() -> None: | def test_bytesio() -> None: | ||||||
|     with open("Tests/images/test-card-lossless.jp2", "rb") as f: |     with open("Tests/images/test-card-lossless.jp2", "rb") as f: | ||||||
|         data = BytesIO(f.read()) |         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 | # 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) |     p.feed(data) | ||||||
| 
 | 
 | ||||||
|     # Assert |     # Assert | ||||||
|  |     assert p.image is not None | ||||||
|     assert p.image.size == (640, 480) |     assert p.image.size == (640, 480) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ from .helper import ( | ||||||
| 
 | 
 | ||||||
| @skip_unless_feature("libtiff") | @skip_unless_feature("libtiff") | ||||||
| class LibTiffTestCase: | 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""" |         """Helper tests that assert basic sanity about the g4 tiff reading""" | ||||||
|         # 1 bit |         # 1 bit | ||||||
|         assert im.mode == "1" |         assert im.mode == "1" | ||||||
|  | @ -524,7 +524,8 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|             im.save(out, compression=compression) |             im.save(out, compression=compression) | ||||||
| 
 | 
 | ||||||
|     def test_fp_leak(self) -> None: |     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() |         fn = im.fp.fileno() | ||||||
| 
 | 
 | ||||||
|         os.fstat(fn) |         os.fstat(fn) | ||||||
|  | @ -716,6 +717,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|                 f.write(src.read()) |                 f.write(src.read()) | ||||||
| 
 | 
 | ||||||
|         im = Image.open(tmpfile) |         im = Image.open(tmpfile) | ||||||
|  |         assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|         im.n_frames |         im.n_frames | ||||||
|         im.close() |         im.close() | ||||||
|         # Should not raise PermissionError. |         # Should not raise PermissionError. | ||||||
|  | @ -1097,6 +1099,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
| 
 | 
 | ||||||
|         with Image.open(out) as im: |         with Image.open(out) as im: | ||||||
|             # Assert that there are multiple strips |             # Assert that there are multiple strips | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert len(im.tag_v2[STRIPOFFSETS]) > 1 |             assert len(im.tag_v2[STRIPOFFSETS]) > 1 | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("argument", (True, False)) |     @pytest.mark.parametrize("argument", (True, False)) | ||||||
|  | @ -1113,6 +1116,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|             im.save(out, **arguments) |             im.save(out, **arguments) | ||||||
| 
 | 
 | ||||||
|             with Image.open(out) as im: |             with Image.open(out) as im: | ||||||
|  |                 assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|                 assert len(im.tag_v2[STRIPOFFSETS]) == 1 |                 assert len(im.tag_v2[STRIPOFFSETS]) == 1 | ||||||
|         finally: |         finally: | ||||||
|             TiffImagePlugin.STRIP_SIZE = 65536 |             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/hydro/details/cmx3g8 | ||||||
|     # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ |     # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ | ||||||
|     test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara" |     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 |     # Act | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|  |  | ||||||
|  | @ -2,11 +2,11 @@ from __future__ import annotations | ||||||
| 
 | 
 | ||||||
| import warnings | import warnings | ||||||
| from io import BytesIO | from io import BytesIO | ||||||
| from typing import Any | from typing import Any, cast | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
| from PIL import Image | from PIL import Image, MpoImagePlugin | ||||||
| 
 | 
 | ||||||
| from .helper import ( | from .helper import ( | ||||||
|     assert_image_equal, |     assert_image_equal, | ||||||
|  | @ -20,14 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] | ||||||
| pytestmark = skip_unless_feature("jpg") | pytestmark = skip_unless_feature("jpg") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def roundtrip(im: Image.Image, **options: Any) -> Image.Image: | def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile: | ||||||
|     out = BytesIO() |     out = BytesIO() | ||||||
|     im.save(out, "MPO", **options) |     im.save(out, "MPO", **options) | ||||||
|     test_bytes = out.tell() |  | ||||||
|     out.seek(0) |     out.seek(0) | ||||||
|     im = Image.open(out) |     return cast(MpoImagePlugin.MpoImageFile, Image.open(out)) | ||||||
|     im.bytes = test_bytes  # for testing only |  | ||||||
|     return im |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("test_file", test_files) | @pytest.mark.parametrize("test_file", test_files) | ||||||
|  |  | ||||||
|  | @ -52,7 +52,7 @@ def test_open_windows_v1() -> None: | ||||||
|         assert isinstance(im, MspImagePlugin.MspImageFile) |         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: |     with Image.open(source_path) as im: | ||||||
|         assert_image_equal_tofile(im, target_path) |         assert_image_equal_tofile(im, target_path) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ from PIL import Image, ImageFile, PcxImagePlugin | ||||||
| from .helper import assert_image_equal, hopper | 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") |     f = str(tmp_path / "temp.pcx") | ||||||
|     im.save(f) |     im.save(f) | ||||||
|     with Image.open(f) as im2: |     with Image.open(f) as im2: | ||||||
|  | @ -44,7 +44,7 @@ def test_invalid_file() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) | @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. |     # See issue #523, odd sized images should have a stride that's even. | ||||||
|     # Not that ImageMagick or GIMP write PCX that way. |     # Not that ImageMagick or GIMP write PCX that way. | ||||||
|     # We were not handling properly. |     # We were not handling properly. | ||||||
|  | @ -89,7 +89,7 @@ def test_large_count(tmp_path: Path) -> None: | ||||||
|     _roundtrip(tmp_path, im) |     _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 |     _last = ImageFile.MAXBLOCK | ||||||
|     ImageFile.MAXBLOCK = size |     ImageFile.MAXBLOCK = size | ||||||
|     try: |     try: | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import os.path | ||||||
| import tempfile | import tempfile | ||||||
| import time | import time | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  | from typing import Any, Generator | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
|  | @ -14,7 +15,7 @@ from PIL import Image, PdfParser, features | ||||||
| from .helper import hopper, mark_if_feature_version, skip_unless_feature | 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 |     # Arrange | ||||||
|     im = hopper(mode) |     im = hopper(mode) | ||||||
|     outfile = str(tmp_path / ("temp_" + mode + ".pdf")) |     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")) | @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) |     helper_save_as_pdf(tmp_path, mode) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @skip_unless_feature("jpg_2000") | @skip_unless_feature("jpg_2000") | ||||||
| @pytest.mark.parametrize("mode", ("LA", "RGBA")) | @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) |     helper_save_as_pdf(tmp_path, mode) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -112,7 +113,7 @@ def test_resolution(tmp_path: Path) -> None: | ||||||
|         {"dpi": (75, 150), "resolution": 200}, |         {"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() |     im = hopper() | ||||||
| 
 | 
 | ||||||
|     outfile = str(tmp_path / "temp.pdf") |     outfile = str(tmp_path / "temp.pdf") | ||||||
|  | @ -156,7 +157,7 @@ def test_save_all(tmp_path: Path) -> None: | ||||||
|         assert os.path.getsize(outfile) > 0 |         assert os.path.getsize(outfile) > 0 | ||||||
| 
 | 
 | ||||||
|         # Test appending using a generator |         # Test appending using a generator | ||||||
|         def im_generator(ims): |         def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: | ||||||
|             yield from ims |             yield from ims | ||||||
| 
 | 
 | ||||||
|         im.save(outfile, save_all=True, append_images=im_generator(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) |             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) |     pages_info = pdf.read_indirect(pdf.pages_ref) | ||||||
|     assert b"Parent" not in pages_info |     assert b"Parent" not in pages_info | ||||||
|     assert b"Kids" in pages_info |     assert b"Kids" in pages_info | ||||||
|  | @ -339,7 +340,7 @@ def test_pdf_append_to_bytesio() -> None: | ||||||
| @pytest.mark.timeout(1) | @pytest.mark.timeout(1) | ||||||
| @pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") | @pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") | ||||||
| @pytest.mark.parametrize("newline", (b"\r", b"\n")) | @pytest.mark.parametrize("newline", (b"\r", b"\n")) | ||||||
| def test_redos(newline) -> None: | def test_redos(newline: bytes) -> None: | ||||||
|     malicious = b" trailer<<>>" + newline * 3456 |     malicious = b" trailer<<>>" + newline * 3456 | ||||||
| 
 | 
 | ||||||
|     # This particular exception isn't relevant here. |     # This particular exception isn't relevant here. | ||||||
|  |  | ||||||
|  | @ -6,7 +6,8 @@ import warnings | ||||||
| import zlib | import zlib | ||||||
| from io import BytesIO | from io import BytesIO | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import Any | from types import ModuleType | ||||||
|  | from typing import Any, cast | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
|  | @ -23,6 +24,7 @@ from .helper import ( | ||||||
|     skip_unless_feature, |     skip_unless_feature, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | ElementTree: ModuleType | None | ||||||
| try: | try: | ||||||
|     from defusedxml import ElementTree |     from defusedxml import ElementTree | ||||||
| except ImportError: | except ImportError: | ||||||
|  | @ -57,11 +59,11 @@ def load(data: bytes) -> Image.Image: | ||||||
|     return Image.open(BytesIO(data)) |     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() |     out = BytesIO() | ||||||
|     im.save(out, "PNG", **options) |     im.save(out, "PNG", **options) | ||||||
|     out.seek(0) |     out.seek(0) | ||||||
|     return Image.open(out) |     return cast(PngImagePlugin.PngImageFile, Image.open(out)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @skip_unless_feature("zlib") | @skip_unless_feature("zlib") | ||||||
|  | @ -100,7 +102,7 @@ class TestFilePng: | ||||||
|             im = hopper(mode) |             im = hopper(mode) | ||||||
|             im.save(test_file) |             im.save(test_file) | ||||||
|             with Image.open(test_file) as reloaded: |             with Image.open(test_file) as reloaded: | ||||||
|                 if mode in ("I;16", "I;16B"): |                 if mode in ("I", "I;16B"): | ||||||
|                     reloaded = reloaded.convert(mode) |                     reloaded = reloaded.convert(mode) | ||||||
|                 assert_image_equal(reloaded, im) |                 assert_image_equal(reloaded, im) | ||||||
| 
 | 
 | ||||||
|  | @ -302,8 +304,8 @@ class TestFilePng: | ||||||
|         assert im.getcolors() == [(100, (0, 0, 0, 0))] |         assert im.getcolors() == [(100, (0, 0, 0, 0))] | ||||||
| 
 | 
 | ||||||
|     def test_save_grayscale_transparency(self, tmp_path: Path) -> None: |     def test_save_grayscale_transparency(self, tmp_path: Path) -> None: | ||||||
|         for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items(): |         for mode, num_transparent in {"1": 1994, "L": 559, "I;16": 559}.items(): | ||||||
|             in_file = "Tests/images/" + mode.lower() + "_trns.png" |             in_file = "Tests/images/" + mode.split(";")[0].lower() + "_trns.png" | ||||||
|             with Image.open(in_file) as im: |             with Image.open(in_file) as im: | ||||||
|                 assert im.mode == mode |                 assert im.mode == mode | ||||||
|                 assert im.info["transparency"] == 255 |                 assert im.info["transparency"] == 255 | ||||||
|  | @ -781,6 +783,18 @@ class TestFilePng: | ||||||
|         with Image.open(mystdout) as reloaded: |         with Image.open(mystdout) as reloaded: | ||||||
|             assert_image_equal_tofile(reloaded, TEST_PNG_FILE) |             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") | @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") | ||||||
| @skip_unless_feature("zlib") | @skip_unless_feature("zlib") | ||||||
|  |  | ||||||
|  | @ -88,7 +88,7 @@ def test_16bit_pgm() -> None: | ||||||
|         assert im.size == (20, 100) |         assert im.size == (20, 100) | ||||||
|         assert im.get_format_mimetype() == "image/x-portable-graymap" |         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: | 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), |         ("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 open(test_file, "rb") as f: | ||||||
|         with pytest.raises(raises): |         with pytest.raises(raises): | ||||||
|             with Image.open(f): |             with Image.open(f): | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ import pytest | ||||||
| 
 | 
 | ||||||
| from PIL import Image, ImageSequence, SpiderImagePlugin | 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" | TEST_FILE = "Tests/images/hopper.spider" | ||||||
| 
 | 
 | ||||||
|  | @ -160,4 +160,5 @@ def test_odd_size() -> None: | ||||||
|     im.save(data, format="SPIDER") |     im.save(data, format="SPIDER") | ||||||
| 
 | 
 | ||||||
|     data.seek(0) |     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) | @pytest.mark.parametrize("mode", _MODES) | ||||||
| def test_sanity(mode, tmp_path: Path) -> None: | def test_sanity(mode: str, tmp_path: Path) -> None: | ||||||
|     def roundtrip(original_im) -> None: |     def roundtrip(original_im: Image.Image) -> None: | ||||||
|         out = str(tmp_path / "temp.tga") |         out = str(tmp_path / "temp.tga") | ||||||
| 
 | 
 | ||||||
|         original_im.save(out, rle=rle) |         original_im.save(out, rle=rle) | ||||||
|  |  | ||||||
|  | @ -4,6 +4,8 @@ import os | ||||||
| import warnings | import warnings | ||||||
| from io import BytesIO | from io import BytesIO | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  | from types import ModuleType | ||||||
|  | from typing import Generator | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
|  | @ -20,6 +22,7 @@ from .helper import ( | ||||||
|     is_win32, |     is_win32, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | ElementTree: ModuleType | None | ||||||
| try: | try: | ||||||
|     from defusedxml import ElementTree |     from defusedxml import ElementTree | ||||||
| except ImportError: | except ImportError: | ||||||
|  | @ -156,7 +159,7 @@ class TestFileTiff: | ||||||
|         "resolution_unit, dpi", |         "resolution_unit, dpi", | ||||||
|         [(None, 72.8), (2, 72.8), (3, 184.912)], |         [(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( |         with Image.open( | ||||||
|             "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" |             "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" | ||||||
|         ) as im: |         ) as im: | ||||||
|  | @ -284,7 +287,7 @@ class TestFileTiff: | ||||||
|             ("Tests/images/multipage.tiff", 3), |             ("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: |         with Image.open(path) as im: | ||||||
|             assert im.n_frames == n_frames |             assert im.n_frames == n_frames | ||||||
|             assert im.is_animated == (n_frames != 1) |             assert im.is_animated == (n_frames != 1) | ||||||
|  | @ -402,7 +405,7 @@ class TestFileTiff: | ||||||
|             assert len_before == len_after + 1 |             assert len_before == len_after + 1 | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("legacy_api", (False, True)) |     @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() |         ifd = TiffImagePlugin.ImageFileDirectory_v2() | ||||||
|         data = b"abc" |         data = b"abc" | ||||||
|         ret = ifd.load_byte(data, legacy_api) |         ret = ifd.load_byte(data, legacy_api) | ||||||
|  | @ -431,7 +434,7 @@ class TestFileTiff: | ||||||
|             assert 0x8825 in im.tag_v2 |             assert 0x8825 in im.tag_v2 | ||||||
| 
 | 
 | ||||||
|     def test_exif(self, tmp_path: Path) -> None: |     def test_exif(self, tmp_path: Path) -> None: | ||||||
|         def check_exif(exif) -> None: |         def check_exif(exif: Image.Exif) -> None: | ||||||
|             assert sorted(exif.keys()) == [ |             assert sorted(exif.keys()) == [ | ||||||
|                 256, |                 256, | ||||||
|                 257, |                 257, | ||||||
|  | @ -511,7 +514,7 @@ class TestFileTiff: | ||||||
|             assert im.getexif()[273] == (1408, 1907) |             assert im.getexif()[273] == (1408, 1907) | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("mode", ("1", "L")) |     @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") |         filename = str(tmp_path / "temp.tif") | ||||||
|         im = hopper(mode) |         im = hopper(mode) | ||||||
|         im.save(filename, tiffinfo={262: 0}) |         im.save(filename, tiffinfo={262: 0}) | ||||||
|  | @ -620,6 +623,7 @@ class TestFileTiff: | ||||||
|         im.save(outfile, tiffinfo={278: 256}) |         im.save(outfile, tiffinfo={278: 256}) | ||||||
| 
 | 
 | ||||||
|         with Image.open(outfile) as im: |         with Image.open(outfile) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert im.tag_v2[278] == 256 |             assert im.tag_v2[278] == 256 | ||||||
| 
 | 
 | ||||||
|     def test_strip_raw(self) -> None: |     def test_strip_raw(self) -> None: | ||||||
|  | @ -660,7 +664,7 @@ class TestFileTiff: | ||||||
|                 assert_image_equal_tofile(reloaded, infile) |                 assert_image_equal_tofile(reloaded, infile) | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("mode", ("P", "PA")) |     @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") |         outfile = str(tmp_path / "temp.tif") | ||||||
| 
 | 
 | ||||||
|         im = hopper(mode) |         im = hopper(mode) | ||||||
|  | @ -689,7 +693,7 @@ class TestFileTiff: | ||||||
|             assert reread.n_frames == 3 |             assert reread.n_frames == 3 | ||||||
| 
 | 
 | ||||||
|         # Test appending using a generator |         # Test appending using a generator | ||||||
|         def im_generator(ims): |         def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: | ||||||
|             yield from ims |             yield from ims | ||||||
| 
 | 
 | ||||||
|         mp = BytesIO() |         mp = BytesIO() | ||||||
|  | @ -860,7 +864,7 @@ class TestFileTiff: | ||||||
|         ], |         ], | ||||||
|     ) |     ) | ||||||
|     @pytest.mark.timeout(2) |     @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.raises(UnidentifiedImageError): | ||||||
|             with pytest.warns(UserWarning): |             with pytest.warns(UserWarning): | ||||||
|                 with Image.open(test_file): |                 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"))) | @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() |     info = TiffImagePlugin.ImageFileDirectory_v2() | ||||||
| 
 | 
 | ||||||
|     tag = TiffTags.TAGS_V2[271] |     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))) | @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() |     im = hopper() | ||||||
|     info = TiffImagePlugin.ImageFileDirectory_v2() |     info = TiffImagePlugin.ImageFileDirectory_v2() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,21 +2,18 @@ from __future__ import annotations | ||||||
| 
 | 
 | ||||||
| import colorsys | import colorsys | ||||||
| import itertools | import itertools | ||||||
|  | from typing import Callable | ||||||
| 
 | 
 | ||||||
| from PIL import Image | from PIL import Image | ||||||
| 
 | 
 | ||||||
| from .helper import assert_image_similar, hopper | from .helper import assert_image_similar, hopper | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def int_to_float(i): | def int_to_float(i: int) -> float: | ||||||
|     return i / 255 |     return i / 255 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def str_to_float(i): | def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]: | ||||||
|     return ord(i) / 255 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def tuple_to_ints(tp): |  | ||||||
|     x, y, z = tp |     x, y, z = tp | ||||||
|     return int(x * 255.0), int(y * 255.0), int(z * 255.0) |     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)) |     Image.new("HSV", (100, 100)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def wedge(): | def wedge() -> Image.Image: | ||||||
|     w = Image._wedge() |     w = Image._wedge() | ||||||
|     w90 = w.rotate(90) |     w90 = w.rotate(90) | ||||||
| 
 | 
 | ||||||
|  | @ -49,7 +46,11 @@ def wedge(): | ||||||
|     return img |     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. |     # convert the hard way using the library colorsys routines. | ||||||
| 
 | 
 | ||||||
|     (r, g, b) = im.split() |     (r, g, b) = im.split() | ||||||
|  | @ -70,11 +71,11 @@ def to_xxx_colorsys(im, func, mode): | ||||||
|     return hsv |     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") |     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") |     return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -138,13 +138,13 @@ class TestImage: | ||||||
|         assert im.height == 2 |         assert im.height == 2 | ||||||
| 
 | 
 | ||||||
|         with pytest.raises(AttributeError): |         with pytest.raises(AttributeError): | ||||||
|             im.size = (3, 4) |             im.size = (3, 4)  # type: ignore[misc] | ||||||
| 
 | 
 | ||||||
|     def test_set_mode(self) -> None: |     def test_set_mode(self) -> None: | ||||||
|         im = Image.new("RGB", (1, 1)) |         im = Image.new("RGB", (1, 1)) | ||||||
| 
 | 
 | ||||||
|         with pytest.raises(AttributeError): |         with pytest.raises(AttributeError): | ||||||
|             im.mode = "P" |             im.mode = "P"  # type: ignore[misc] | ||||||
| 
 | 
 | ||||||
|     def test_invalid_image(self) -> None: |     def test_invalid_image(self) -> None: | ||||||
|         im = io.BytesIO(b"") |         im = io.BytesIO(b"") | ||||||
|  | @ -685,12 +685,15 @@ class TestImage: | ||||||
|         _make_new(im, blank_p, ImagePalette.ImagePalette()) |         _make_new(im, blank_p, ImagePalette.ImagePalette()) | ||||||
|         _make_new(im, blank_pa, ImagePalette.ImagePalette()) |         _make_new(im, blank_pa, ImagePalette.ImagePalette()) | ||||||
| 
 | 
 | ||||||
|     def test_p_from_rgb_rgba(self) -> None: |     @pytest.mark.parametrize( | ||||||
|         for mode, color in [ |         "mode, color", | ||||||
|  |         ( | ||||||
|             ("RGB", "#DDEEFF"), |             ("RGB", "#DDEEFF"), | ||||||
|             ("RGB", (221, 238, 255)), |             ("RGB", (221, 238, 255)), | ||||||
|             ("RGBA", (221, 238, 255, 255)), |             ("RGBA", (221, 238, 255, 255)), | ||||||
|         ]: |         ), | ||||||
|  |     ) | ||||||
|  |     def test_p_from_rgb_rgba(self, mode: str, color: str | tuple[int, ...]) -> None: | ||||||
|         im = Image.new("P", (100, 100), color) |         im = Image.new("P", (100, 100), color) | ||||||
|         expected = Image.new(mode, (100, 100), color) |         expected = Image.new(mode, (100, 100), color) | ||||||
|         assert_image_equal(im.convert(mode), expected) |         assert_image_equal(im.convert(mode), expected) | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ from .helper import assert_image_equal, hopper, is_win32 | ||||||
| 
 | 
 | ||||||
| # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 | # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 | ||||||
| # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 | # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 | ||||||
|  | cffi: ModuleType | None | ||||||
| if os.environ.get("PYTHONOPTIMIZE") == "2": | if os.environ.get("PYTHONOPTIMIZE") == "2": | ||||||
|     cffi = None |     cffi = None | ||||||
| else: | else: | ||||||
|  |  | ||||||
|  | @ -148,9 +148,7 @@ def test_kernel_not_enough_coefficients() -> None: | ||||||
| @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) | @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) | ||||||
| def test_consistency_3x3(mode: str) -> None: | def test_consistency_3x3(mode: str) -> None: | ||||||
|     with Image.open("Tests/images/hopper.bmp") as source: |     with Image.open("Tests/images/hopper.bmp") as source: | ||||||
|         reference_name = "hopper_emboss" |         with Image.open("Tests/images/hopper_emboss.bmp") as reference: | ||||||
|         reference_name += "_I.png" if mode == "I" else ".bmp" |  | ||||||
|         with Image.open("Tests/images/" + reference_name) as reference: |  | ||||||
|             kernel = ImageFilter.Kernel( |             kernel = ImageFilter.Kernel( | ||||||
|                 (3, 3), |                 (3, 3), | ||||||
|                 # fmt: off |                 # fmt: off | ||||||
|  | @ -160,23 +158,13 @@ def test_consistency_3x3(mode: str) -> None: | ||||||
|                 # fmt: on |                 # fmt: on | ||||||
|                 0.3, |                 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) |             assert_image_equal(source.filter(kernel), reference) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) | @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) | ||||||
| def test_consistency_5x5(mode: str) -> None: | def test_consistency_5x5(mode: str) -> None: | ||||||
|     with Image.open("Tests/images/hopper.bmp") as source: |     with Image.open("Tests/images/hopper.bmp") as source: | ||||||
|         reference_name = "hopper_emboss_more" |         with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: | ||||||
|         reference_name += "_I.png" if mode == "I" else ".bmp" |  | ||||||
|         with Image.open("Tests/images/" + reference_name) as reference: |  | ||||||
|             kernel = ImageFilter.Kernel( |             kernel = ImageFilter.Kernel( | ||||||
|                 (5, 5), |                 (5, 5), | ||||||
|                 # fmt: off |                 # fmt: off | ||||||
|  | @ -188,14 +176,6 @@ def test_consistency_5x5(mode: str) -> None: | ||||||
|                 # fmt: on |                 # fmt: on | ||||||
|                 0.3, |                 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) |             assert_image_equal(source.filter(kernel), reference) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
| import warnings | import warnings | ||||||
| from typing import Generator |  | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
|  | @ -17,17 +16,14 @@ pytestmark = pytest.mark.skipif( | ||||||
|     not ImageQt.qt_is_installed, reason="Qt bindings are not installed" |     not ImageQt.qt_is_installed, reason="Qt bindings are not installed" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| @pytest.fixture |  | ||||||
| def test_images() -> Generator[Image.Image, None, None]: |  | ||||||
| ims = [ | ims = [ | ||||||
|     hopper(), |     hopper(), | ||||||
|     Image.open("Tests/images/transparent.png"), |     Image.open("Tests/images/transparent.png"), | ||||||
|     Image.open("Tests/images/7x13.png"), |     Image.open("Tests/images/7x13.png"), | ||||||
| ] | ] | ||||||
|     try: | 
 | ||||||
|         yield ims | 
 | ||||||
|     finally: | def teardown_module() -> None: | ||||||
|     for im in ims: |     for im in ims: | ||||||
|         im.close() |         im.close() | ||||||
| 
 | 
 | ||||||
|  | @ -44,26 +40,26 @@ def roundtrip(expected: Image.Image) -> None: | ||||||
|         assert_image_equal(result, expected.convert("RGB")) |         assert_image_equal(result, expected.convert("RGB")) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_sanity_1(test_images: Generator[Image.Image, None, None]) -> None: | def test_sanity_1() -> None: | ||||||
|     for im in test_images: |     for im in ims: | ||||||
|         roundtrip(im.convert("1")) |         roundtrip(im.convert("1")) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_sanity_rgb(test_images: Generator[Image.Image, None, None]) -> None: | def test_sanity_rgb() -> None: | ||||||
|     for im in test_images: |     for im in ims: | ||||||
|         roundtrip(im.convert("RGB")) |         roundtrip(im.convert("RGB")) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_sanity_rgba(test_images: Generator[Image.Image, None, None]) -> None: | def test_sanity_rgba() -> None: | ||||||
|     for im in test_images: |     for im in ims: | ||||||
|         roundtrip(im.convert("RGBA")) |         roundtrip(im.convert("RGBA")) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_sanity_l(test_images: Generator[Image.Image, None, None]) -> None: | def test_sanity_l() -> None: | ||||||
|     for im in test_images: |     for im in ims: | ||||||
|         roundtrip(im.convert("L")) |         roundtrip(im.convert("L")) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_sanity_p(test_images: Generator[Image.Image, None, None]) -> None: | def test_sanity_p() -> None: | ||||||
|     for im in test_images: |     for im in ims: | ||||||
|         roundtrip(im.convert("P")) |         roundtrip(im.convert("P")) | ||||||
|  |  | ||||||
|  | @ -8,7 +8,6 @@ from .helper import CachedProperty, assert_image_equal | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestImagingPaste: | class TestImagingPaste: | ||||||
|     masks = {} |  | ||||||
|     size = 128 |     size = 128 | ||||||
| 
 | 
 | ||||||
|     def assert_9points_image( |     def assert_9points_image( | ||||||
|  | @ -33,7 +32,7 @@ class TestImagingPaste: | ||||||
|     def assert_9points_paste( |     def assert_9points_paste( | ||||||
|         self, |         self, | ||||||
|         im: Image.Image, |         im: Image.Image, | ||||||
|         im2: Image.Image, |         im2: Image.Image | str | tuple[int, ...], | ||||||
|         mask: Image.Image, |         mask: Image.Image, | ||||||
|         expected: list[tuple[int, int, int, int]], |         expected: list[tuple[int, int, int, int]], | ||||||
|     ) -> None: |     ) -> None: | ||||||
|  |  | ||||||
|  | @ -237,7 +237,7 @@ class TestCoreResampleConsistency: | ||||||
|         im = Image.new(mode, (512, 9), fill) |         im = Image.new(mode, (512, 9), fill) | ||||||
|         return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] |         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 |         channel, color = case | ||||||
|         px = channel.load() |         px = channel.load() | ||||||
|         for x in range(channel.size[0]): |         for x in range(channel.size[0]): | ||||||
|  |  | ||||||
|  | @ -154,7 +154,7 @@ class TestImagingCoreResize: | ||||||
| 
 | 
 | ||||||
|     def test_unknown_filter(self) -> None: |     def test_unknown_filter(self) -> None: | ||||||
|         with pytest.raises(ValueError): |         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: |     def test_cross_platform(self, tmp_path: Path) -> None: | ||||||
|         # This test is intended for only check for consistent behaviour across |         # This test is intended for only check for consistent behaviour across | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
|  | from typing import Callable | ||||||
|  | 
 | ||||||
| from PIL import Image, ImageChops | from PIL import Image, ImageChops | ||||||
| 
 | 
 | ||||||
| from .helper import assert_image_equal, hopper | from .helper import assert_image_equal, hopper | ||||||
|  | @ -387,7 +389,9 @@ def test_overlay() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_logical() -> 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 = [] |         out = [] | ||||||
|         for x in (a, b): |         for x in (a, b): | ||||||
|             imx = Image.new("1", (1, 1), x) |             imx = Image.new("1", (1, 1), x) | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import re | ||||||
| import shutil | import shutil | ||||||
| from io import BytesIO | from io import BytesIO | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  | from typing import Any | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
|  | @ -237,7 +238,7 @@ def test_invalid_color_temperature() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("flag", ("my string", -1)) | @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 hopper() as im: | ||||||
|         with pytest.raises( |         with pytest.raises( | ||||||
|             ImageCms.PyCMSError, match="flags must be an integer between 0 and " |             ImageCms.PyCMSError, match="flags must be an integer between 0 and " | ||||||
|  | @ -335,19 +336,21 @@ def test_extended_information() -> None: | ||||||
|     o = ImageCms.getOpenProfile(SRGB) |     o = ImageCms.getOpenProfile(SRGB) | ||||||
|     p = o.profile |     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 |         # Helper function to reduce precision of tuples of floats | ||||||
|         # recursively and then check equality. |         # recursively and then check equality. | ||||||
|         power = 10**digits |         power = 10**digits | ||||||
| 
 | 
 | ||||||
|         def truncate_tuple(tuple_or_float): |         def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]: | ||||||
|             return tuple( |             return tuple( | ||||||
|                 ( |                 ( | ||||||
|                     truncate_tuple(val) |                     truncate_tuple(val) | ||||||
|                     if isinstance(val, tuple) |                     if isinstance(val, tuple) | ||||||
|                     else int(val * power) / power |                     else int(val * power) / power | ||||||
|                 ) |                 ) | ||||||
|                 for val in tuple_or_float |                 for val in tuple_value | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         assert truncate_tuple(tup1) == truncate_tuple(tup2) |         assert truncate_tuple(tup1) == truncate_tuple(tup2) | ||||||
|  | @ -504,8 +507,10 @@ def test_profile_typesafety() -> None: | ||||||
|         ImageCms.ImageCmsProfile(1).tobytes() |         ImageCms.ImageCmsProfile(1).tobytes() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None: | def assert_aux_channel_preserved( | ||||||
|     def create_test_image(): |     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. |         # set up test image with something interesting in the tested aux channel. | ||||||
|         # fmt: off |         # fmt: off | ||||||
|         nine_grid_deltas = [ |         nine_grid_deltas = [ | ||||||
|  | @ -633,7 +638,7 @@ def test_auxiliary_channels_isolated() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) | @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)) |     im = Image.new(mode, (1, 1)) | ||||||
|     converted_im = im.convert("LAB") |     converted_im = im.convert("LAB") | ||||||
|     assert converted_im.getpixel((0, 0)) == (0, 128, 128) |     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) |     draw.rectangle(bbox, outline=0xFFFF) | ||||||
| 
 | 
 | ||||||
|     # Assert |     # 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) | @pytest.mark.parametrize("bbox", BBOX) | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import os.path | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
| from PIL import Image, ImageDraw, ImageDraw2, features | from PIL import Image, ImageDraw, ImageDraw2, features | ||||||
|  | from PIL._typing import Coords | ||||||
| 
 | 
 | ||||||
| from .helper import ( | from .helper import ( | ||||||
|     assert_image_equal, |     assert_image_equal, | ||||||
|  | @ -56,7 +57,7 @@ def test_sanity() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("bbox", BBOX) | @pytest.mark.parametrize("bbox", BBOX) | ||||||
| def test_ellipse(bbox) -> None: | def test_ellipse(bbox: Coords) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     im = Image.new("RGB", (W, H)) |     im = Image.new("RGB", (W, H)) | ||||||
|     draw = ImageDraw2.Draw(im) |     draw = ImageDraw2.Draw(im) | ||||||
|  | @ -84,7 +85,7 @@ def test_ellipse_edge() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("points", POINTS) | @pytest.mark.parametrize("points", POINTS) | ||||||
| def test_line(points) -> None: | def test_line(points: Coords) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     im = Image.new("RGB", (W, H)) |     im = Image.new("RGB", (W, H)) | ||||||
|     draw = ImageDraw2.Draw(im) |     draw = ImageDraw2.Draw(im) | ||||||
|  | @ -98,7 +99,7 @@ def test_line(points) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("points", POINTS) | @pytest.mark.parametrize("points", POINTS) | ||||||
| def test_line_pen_as_brush(points) -> None: | def test_line_pen_as_brush(points: Coords) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     im = Image.new("RGB", (W, H)) |     im = Image.new("RGB", (W, H)) | ||||||
|     draw = ImageDraw2.Draw(im) |     draw = ImageDraw2.Draw(im) | ||||||
|  | @ -114,7 +115,7 @@ def test_line_pen_as_brush(points) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("points", POINTS) | @pytest.mark.parametrize("points", POINTS) | ||||||
| def test_polygon(points) -> None: | def test_polygon(points: Coords) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     im = Image.new("RGB", (W, H)) |     im = Image.new("RGB", (W, H)) | ||||||
|     draw = ImageDraw2.Draw(im) |     draw = ImageDraw2.Draw(im) | ||||||
|  | @ -129,7 +130,7 @@ def test_polygon(points) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("bbox", BBOX) | @pytest.mark.parametrize("bbox", BBOX) | ||||||
| def test_rectangle(bbox) -> None: | def test_rectangle(bbox: Coords) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     im = Image.new("RGB", (W, H)) |     im = Image.new("RGB", (W, H)) | ||||||
|     draw = ImageDraw2.Draw(im) |     draw = ImageDraw2.Draw(im) | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ def test_crash() -> None: | ||||||
|     ImageEnhance.Sharpness(im).enhance(0.5) |     ImageEnhance.Sharpness(im).enhance(0.5) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _half_transparent_image(): | def _half_transparent_image() -> Image.Image: | ||||||
|     # returns an image, half transparent, half solid |     # returns an image, half transparent, half solid | ||||||
|     im = hopper("RGB") |     im = hopper("RGB") | ||||||
| 
 | 
 | ||||||
|  | @ -34,7 +34,9 @@ def _half_transparent_image(): | ||||||
|     return im |     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 im.getbands() == original.getbands() | ||||||
|     assert_image_equal( |     assert_image_equal( | ||||||
|         im.getchannel("A"), |         im.getchannel("A"), | ||||||
|  | @ -44,7 +46,7 @@ def _check_alpha(im, original, op, amount) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) | @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 |     # Issue https://github.com/python-pillow/Pillow/issues/899 | ||||||
|     # Is alpha preserved through image enhancement? |     # Is alpha preserved through image enhancement? | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -31,7 +31,7 @@ SAFEBLOCK = ImageFile.SAFEBLOCK | ||||||
| 
 | 
 | ||||||
| class TestImageFile: | class TestImageFile: | ||||||
|     def test_parser(self) -> None: |     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) |             im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) | ||||||
|             if format in ("MSP", "XBM"): |             if format in ("MSP", "XBM"): | ||||||
|                 im = im.convert("1") |                 im = im.convert("1") | ||||||
|  |  | ||||||
|  | @ -7,11 +7,13 @@ import shutil | ||||||
| import sys | import sys | ||||||
| from io import BytesIO | from io import BytesIO | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  | from typing import Any, BinaryIO | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| from packaging.version import parse as parse_version | from packaging.version import parse as parse_version | ||||||
| 
 | 
 | ||||||
| from PIL import Image, ImageDraw, ImageFont, features | from PIL import Image, ImageDraw, ImageFont, features | ||||||
|  | from PIL._typing import StrOrBytesPath | ||||||
| 
 | 
 | ||||||
| from .helper import ( | from .helper import ( | ||||||
|     assert_image_equal, |     assert_image_equal, | ||||||
|  | @ -42,16 +44,16 @@ def test_sanity() -> None: | ||||||
|         pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), |         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 |     return request.param | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.fixture(scope="module") | @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) |     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.path == FONT_PATH | ||||||
|     assert font.size == FONT_SIZE |     assert font.size == FONT_SIZE | ||||||
| 
 | 
 | ||||||
|  | @ -67,7 +69,9 @@ def test_font_properties(font) -> None: | ||||||
|     assert font_copy.path == second_font_path |     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!" |     txt = "Hello World!" | ||||||
|     ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) |     ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) | ||||||
|     ttf.getbbox(txt) |     ttf.getbbox(txt) | ||||||
|  | @ -80,12 +84,12 @@ def _render(font, layout_engine): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH))) | @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) |     _render(font, layout_engine) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_font_with_filelike(layout_engine) -> None: | def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None: | ||||||
|     def _font_as_bytes(): |     def _font_as_bytes() -> BytesIO: | ||||||
|         with open(FONT_PATH, "rb") as f: |         with open(FONT_PATH, "rb") as f: | ||||||
|             font_bytes = BytesIO(f.read()) |             font_bytes = BytesIO(f.read()) | ||||||
|         return font_bytes |         return font_bytes | ||||||
|  | @ -102,12 +106,12 @@ def test_font_with_filelike(layout_engine) -> None: | ||||||
|     #   _render(shared_bytes) |     #   _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: |     with open(FONT_PATH, "rb") as f: | ||||||
|         _render(f, layout_engine) |         _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) |     img_path = _render(FONT_PATH, layout_engine) | ||||||
|     with open(FONT_PATH, "rb") as f: |     with open(FONT_PATH, "rb") as f: | ||||||
|         font_filelike = BytesIO(f.read()) |         font_filelike = BytesIO(f.read()) | ||||||
|  | @ -116,7 +120,7 @@ def test_render_equal(layout_engine) -> None: | ||||||
|     assert_image_equal(img_path, img_filelike) |     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")) |     tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) | ||||||
|     try: |     try: | ||||||
|         shutil.copy(FONT_PATH, tempfile) |         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) |     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)) |     im = Image.new(mode="RGBA", size=(300, 100)) | ||||||
|     draw = ImageDraw.Draw(im) |     draw = ImageDraw.Draw(im) | ||||||
| 
 | 
 | ||||||
|  | @ -140,7 +144,7 @@ def test_transparent_background(font) -> None: | ||||||
|     assert_image_similar_tofile(im.convert("L"), target, 0.01) |     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)) |     im = Image.new(mode="I;16", size=(300, 100)) | ||||||
|     draw = ImageDraw.Draw(im) |     draw = ImageDraw.Draw(im) | ||||||
| 
 | 
 | ||||||
|  | @ -153,7 +157,7 @@ def test_I16(font) -> None: | ||||||
|     assert_image_similar_tofile(im.convert("L"), target, 0.01) |     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)) |     im = Image.new(mode="RGB", size=(300, 100)) | ||||||
|     draw = ImageDraw.Draw(im) |     draw = ImageDraw.Draw(im) | ||||||
| 
 | 
 | ||||||
|  | @ -181,7 +185,13 @@ def test_textbbox_equal(font) -> None: | ||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
| def test_getlength( | 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: | ) -> None: | ||||||
|     f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) |     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] |     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)) |     im = Image.new(mode="RGB", size=(300, 100)) | ||||||
|     draw = ImageDraw.Draw(im) |     draw = ImageDraw.Draw(im) | ||||||
|     line_spacing = font.getbbox("A")[3] + 4 |     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) |     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() |     # Test that text() correctly connects to multiline_text() | ||||||
|     # and that align defaults to left |     # and that align defaults to left | ||||||
|     im = Image.new(mode="RGB", size=(300, 100)) |     im = Image.new(mode="RGB", size=(300, 100)) | ||||||
|  | @ -243,7 +253,9 @@ def test_render_multiline_text(font) -> None: | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) |     "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)) |     im = Image.new(mode="RGB", size=(300, 100)) | ||||||
|     draw = ImageDraw.Draw(im) |     draw = ImageDraw.Draw(im) | ||||||
|     draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) |     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) |     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)) |     im = Image.new(mode="RGB", size=(300, 100)) | ||||||
|     draw = ImageDraw.Draw(im) |     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") |         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") |     im = Image.new("RGB", (300, 100), "white") | ||||||
|     draw = ImageDraw.Draw(im) |     draw = ImageDraw.Draw(im) | ||||||
|     line = "some text" |     line = "some text" | ||||||
|     draw.text((100, 40), line, (0, 0, 0), font=font, align="left") |     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)) |     im = Image.new(mode="RGB", size=(300, 100)) | ||||||
|     draw = ImageDraw.Draw(im) |     draw = ImageDraw.Draw(im) | ||||||
| 
 | 
 | ||||||
|  | @ -285,7 +297,7 @@ def test_multiline_bbox(font) -> None: | ||||||
|     draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) |     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)) |     im = Image.new(mode="RGB", size=(300, 100)) | ||||||
|     draw = ImageDraw.Draw(im) |     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)) |     im = Image.new(mode="RGB", size=(300, 100)) | ||||||
|     draw = ImageDraw.Draw(im) |     draw = ImageDraw.Draw(im) | ||||||
|     draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) |     draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) | ||||||
|  | @ -306,7 +318,9 @@ def test_multiline_spacing(font) -> None: | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) |     "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)) |     img_gray = Image.new("L", (100, 100)) | ||||||
|     draw = ImageDraw.Draw(img_gray) |     draw = ImageDraw.Draw(img_gray) | ||||||
|     word = "testing" |     word = "testing" | ||||||
|  | @ -347,7 +361,9 @@ def test_rotated_transposed_font(font, orientation) -> None: | ||||||
|         Image.Transpose.FLIP_TOP_BOTTOM, |         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)) |     img_gray = Image.new("L", (100, 100)) | ||||||
|     draw = ImageDraw.Draw(img_gray) |     draw = ImageDraw.Draw(img_gray) | ||||||
|     word = "testing" |     word = "testing" | ||||||
|  | @ -382,7 +398,9 @@ def test_unrotated_transposed_font(font, orientation) -> None: | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) |     "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 |     # Arrange | ||||||
|     text = "mask this" |     text = "mask this" | ||||||
|     transposed_font = ImageFont.TransposedFont(font, orientation=orientation) |     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, |         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 |     # Arrange | ||||||
|     text = "mask this" |     text = "mask this" | ||||||
|     transposed_font = ImageFont.TransposedFont(font, orientation=orientation) |     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) |     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() |     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() |     ascent, descent = font.getmetrics() | ||||||
| 
 | 
 | ||||||
|     assert isinstance(ascent, int) |     assert isinstance(ascent, int) | ||||||
|  | @ -427,7 +447,7 @@ def test_free_type_font_get_metrics(font) -> None: | ||||||
|     assert (ascent, descent) == (16, 4) |     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 |     # Arrange | ||||||
|     text = "mask this" |     text = "mask this" | ||||||
| 
 | 
 | ||||||
|  | @ -473,16 +493,16 @@ def test_default_font() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("mode", (None, "1", "RGBA")) | @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) |     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. |     # issue #2614, should not crash. | ||||||
|     assert (0, 0, 0, 0) == font.getbbox("") |     assert (0, 0, 0, 0) == font.getbbox("") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_render_empty(font) -> None: | def test_render_empty(font: ImageFont.FreeTypeFont) -> None: | ||||||
|     # issue 2666 |     # issue 2666 | ||||||
|     im = Image.new(mode="RGB", size=(300, 100)) |     im = Image.new(mode="RGB", size=(300, 100)) | ||||||
|     target = im.copy() |     target = im.copy() | ||||||
|  | @ -492,7 +512,7 @@ def test_render_empty(font) -> None: | ||||||
|     assert_image_equal(im, target) |     assert_image_equal(im, target) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_unicode_extended(layout_engine) -> None: | def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: | ||||||
|     # issue #3777 |     # issue #3777 | ||||||
|     text = "A\u278A\U0001F12B" |     text = "A\u278A\U0001F12B" | ||||||
|     target = "Tests/images/unicode_extended.png" |     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")), |     (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), | ||||||
| ) | ) | ||||||
| @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") | @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") | ||||||
| def test_find_font(monkeypatch, platform, font_directory) -> None: | def test_find_font( | ||||||
|     def _test_fake_loading_font(path_to_fake, fontname) -> None: |     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 |         # Make a copy of FreeTypeFont so we can patch the original | ||||||
|         free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) |         free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) | ||||||
|         with monkeypatch.context() as m: |         with monkeypatch.context() as m: | ||||||
|             m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) |             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: |                 if filepath == path_to_fake: | ||||||
|                     return ImageFont._FreeTypeFont( |                     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) |             m.setattr(ImageFont, "FreeTypeFont", loadable_font) | ||||||
|             font = ImageFont.truetype(fontname) |             font = ImageFont.truetype(fontname) | ||||||
|  | @ -543,7 +565,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None: | ||||||
|     if platform == "linux": |     if platform == "linux": | ||||||
|         monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") |         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: |         if path == font_directory: | ||||||
|             return [ |             return [ | ||||||
|                 ( |                 ( | ||||||
|  | @ -567,7 +589,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None: | ||||||
|     _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") |     _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.getmetrics() == (16, 4) | ||||||
|     assert font.font.ascent == 16 |     assert font.font.ascent == 16 | ||||||
|     assert font.font.descent == 4 |     assert font.font.descent == 4 | ||||||
|  | @ -588,7 +610,7 @@ def test_imagefont_getters(font) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("stroke_width", (0, 2)) | @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) == ( |     assert font.getbbox("A", stroke_width=stroke_width) == ( | ||||||
|         0 - stroke_width, |         0 - stroke_width, | ||||||
|         4 - stroke_width, |         4 - stroke_width, | ||||||
|  | @ -607,7 +629,7 @@ def test_complex_font_settings() -> None: | ||||||
|         t.getmask("абвг", language="sr") |         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")) |     freetype = parse_version(features.version_module("freetype2")) | ||||||
|     if freetype < parse_version("2.9.1"): |     if freetype < parse_version("2.9.1"): | ||||||
|         with pytest.raises(NotImplementedError): |         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") |     im = Image.new("RGB", (100, 75), "white") | ||||||
|     d = ImageDraw.Draw(im) |     d = ImageDraw.Draw(im) | ||||||
|     d.text((10, 10), "Text", font=font, fill="black") |     d.text((10, 10), "Text", font=font, fill="black") | ||||||
|  | @ -677,7 +699,7 @@ def _check_text(font, path, epsilon): | ||||||
|             raise |             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")) |     freetype = parse_version(features.version_module("freetype2")) | ||||||
|     if freetype < parse_version("2.9.1"): |     if freetype < parse_version("2.9.1"): | ||||||
|         with pytest.raises(NotImplementedError): |         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) |     _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")) |     freetype = parse_version(features.version_module("freetype2")) | ||||||
|     if freetype < parse_version("2.9.1"): |     if freetype < parse_version("2.9.1"): | ||||||
|         with pytest.raises(NotImplementedError): |         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"), |     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" |     name, text = "quick", "Quick" | ||||||
|     path = f"Tests/images/test_anchor_{name}_{anchor}.png" |     path = f"Tests/images/test_anchor_{name}_{anchor}.png" | ||||||
| 
 | 
 | ||||||
|  | @ -782,7 +806,9 @@ def test_anchor(layout_engine, anchor, left, top) -> None: | ||||||
|         ("md", "center"), |         ("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" |     target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" | ||||||
|     text = "a\nlong\ntext sample" |     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) |     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") |     im = Image.new("RGB", (100, 100), "white") | ||||||
|     d = ImageDraw.Draw(im) |     d = ImageDraw.Draw(im) | ||||||
|     d.font = font |     d.font = font | ||||||
|  | @ -826,7 +852,7 @@ def test_anchor_invalid(font) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) | @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" |     text = "Bitmap Font" | ||||||
|     layout_name = ["basic", "raqm"][layout_engine] |     layout_name = ["basic", "raqm"][layout_engine] | ||||||
|     target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" |     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) |     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" |     text = "Bitmap Font" | ||||||
|     layout_name = ["basic", "raqm"][layout_engine] |     layout_name = ["basic", "raqm"][layout_engine] | ||||||
|     target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" |     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)) | @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( |     font = ImageFont.truetype( | ||||||
|         "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine |         "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") |     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!" |     txt = "Hello World!" | ||||||
|     ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) |     ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) | ||||||
|     ttf.getbbox(txt) |     ttf.getbbox(txt) | ||||||
|  | @ -886,7 +912,7 @@ def test_standard_embedded_color(layout_engine) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("fontmode", ("1", "L", "RGBA")) | @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!" |     txt = "Hello World!" | ||||||
|     ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) |     ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) | ||||||
| 
 | 
 | ||||||
|  | @ -908,7 +934,7 @@ def test_float_coord(layout_engine, fontmode): | ||||||
|             raise |             raise | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_cbdt(layout_engine) -> None: | def test_cbdt(layout_engine: ImageFont.Layout) -> None: | ||||||
|     try: |     try: | ||||||
|         font = ImageFont.truetype( |         font = ImageFont.truetype( | ||||||
|             "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine |             "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") |         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: |     try: | ||||||
|         font = ImageFont.truetype( |         font = ImageFont.truetype( | ||||||
|             "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine |             "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") |         pytest.skip("freetype compiled without libpng or CBDT support") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_sbix(layout_engine) -> None: | def test_sbix(layout_engine: ImageFont.Layout) -> None: | ||||||
|     try: |     try: | ||||||
|         font = ImageFont.truetype( |         font = ImageFont.truetype( | ||||||
|             "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine |             "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") |         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: |     try: | ||||||
|         font = ImageFont.truetype( |         font = ImageFont.truetype( | ||||||
|             "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine |             "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") | @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( |     font = ImageFont.truetype( | ||||||
|         "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", |         "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", | ||||||
|         size=64, |         size=64, | ||||||
|  | @ -993,7 +1019,7 @@ def test_colr(layout_engine) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @skip_unless_feature_version("freetype2", "2.10.0") | @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( |     font = ImageFont.truetype( | ||||||
|         "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", |         "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", | ||||||
|         size=64, |         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) |     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: |     try: | ||||||
|         font = ImageFont.truetype( |         font = ImageFont.truetype( | ||||||
|             "Tests/fonts/OpenSans.woff2", |             "Tests/fonts/OpenSans.woff2", | ||||||
|  | @ -1042,7 +1068,7 @@ def test_render_mono_size() -> None: | ||||||
|     assert_image_equal_tofile(im, "Tests/images/text_mono.gif") |     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): |     with pytest.raises(ValueError): | ||||||
|         font.getlength("A" * 1_000_001) |         font.getlength("A" * 1_000_001) | ||||||
|     with pytest.raises(ValueError): |     with pytest.raises(ValueError): | ||||||
|  | @ -1070,14 +1096,14 @@ def test_too_many_characters(font) -> None: | ||||||
|         "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", |         "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: |     with open(test_file, "rb") as f: | ||||||
|         font = ImageFont.truetype(BytesIO(f.read())) |         font = ImageFont.truetype(BytesIO(f.read())) | ||||||
|         with pytest.raises(Image.DecompressionBombError): |         with pytest.raises(Image.DecompressionBombError): | ||||||
|             font.getmask("Test Text") |             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) |     monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) | ||||||
|     with pytest.warns(UserWarning) as record: |     with pytest.warns(UserWarning) as record: | ||||||
|         font = ImageFont.truetype( |         font = ImageFont.truetype( | ||||||
|  | @ -1091,6 +1117,8 @@ def test_raqm_missing_warning(monkeypatch) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("size", [-1, 0]) | @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): |     with pytest.raises(ValueError): | ||||||
|         ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) |         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") |     @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") | ||||||
|     def test_grabclipboard_file(self) -> None: |     def test_grabclipboard_file(self) -> None: | ||||||
|         p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) |         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.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') | ||||||
|         p.communicate() |         p.communicate() | ||||||
| 
 | 
 | ||||||
|  | @ -94,6 +95,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200 | ||||||
|     @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") |     @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") | ||||||
|     def test_grabclipboard_png(self) -> None: |     def test_grabclipboard_png(self) -> None: | ||||||
|         p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) |         p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) | ||||||
|  |         assert p.stdin is not None | ||||||
|         p.stdin.write( |         p.stdin.write( | ||||||
|             rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") |             rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") | ||||||
| $ms = new-object System.IO.MemoryStream(, $bytes) | $ms = new-object System.IO.MemoryStream(, $bytes) | ||||||
|  | @ -113,7 +115,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes) | ||||||
|         reason="Linux with wl-clipboard only", |         reason="Linux with wl-clipboard only", | ||||||
|     ) |     ) | ||||||
|     @pytest.mark.parametrize("ext", ("gif", "png", "ico")) |     @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 |         image_path = "Tests/images/hopper." + ext | ||||||
|         with open(image_path, "rb") as fp: |         with open(image_path, "rb") as fp: | ||||||
|             subprocess.call(["wl-copy"], stdin=fp) |             subprocess.call(["wl-copy"], stdin=fp) | ||||||
|  | @ -128,6 +130,6 @@ $ms = new-object System.IO.MemoryStream(, $bytes) | ||||||
|         reason="Linux with wl-clipboard only", |         reason="Linux with wl-clipboard only", | ||||||
|     ) |     ) | ||||||
|     @pytest.mark.parametrize("arg", ("text", "--clear")) |     @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]) |         subprocess.call(["wl-copy", arg]) | ||||||
|         assert ImageGrab.grabclipboard() is None |         assert ImageGrab.grabclipboard() is None | ||||||
|  |  | ||||||
|  | @ -73,15 +73,16 @@ def test_lut(op: str) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_no_operator_loaded() -> None: | def test_no_operator_loaded() -> None: | ||||||
|  |     im = Image.new("L", (1, 1)) | ||||||
|     mop = ImageMorph.MorphOp() |     mop = ImageMorph.MorphOp() | ||||||
|     with pytest.raises(Exception) as e: |     with pytest.raises(Exception) as e: | ||||||
|         mop.apply(None) |         mop.apply(im) | ||||||
|     assert str(e.value) == "No operator loaded" |     assert str(e.value) == "No operator loaded" | ||||||
|     with pytest.raises(Exception) as e: |     with pytest.raises(Exception) as e: | ||||||
|         mop.match(None) |         mop.match(im) | ||||||
|     assert str(e.value) == "No operator loaded" |     assert str(e.value) == "No operator loaded" | ||||||
|     with pytest.raises(Exception) as e: |     with pytest.raises(Exception) as e: | ||||||
|         mop.save_lut(None) |         mop.save_lut("") | ||||||
|     assert str(e.value) == "No operator loaded" |     assert str(e.value) == "No operator loaded" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,8 +13,12 @@ from .helper import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Deformer: | class Deformer(ImageOps.SupportsGetMesh): | ||||||
|     def getmesh(self, im): |     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 |         x, y = im.size | ||||||
|         return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] |         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))) | @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() |     im = hopper() | ||||||
|     new_im = ImageOps.contain(im, new_size) |     new_im = ImageOps.contain(im, new_size) | ||||||
|     assert new_im.size == (256, 256) |     assert new_im.size == (256, 256) | ||||||
|  | @ -132,7 +136,7 @@ def test_contain_round() -> None: | ||||||
|         ("hopper.png", (256, 256)),  # square |         ("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: |     with Image.open("Tests/images/" + image_name) as im: | ||||||
|         new_im = ImageOps.cover(im, (256, 256)) |         new_im = ImageOps.cover(im, (256, 256)) | ||||||
|         assert new_im.size == expected_size |         assert new_im.size == expected_size | ||||||
|  | @ -168,7 +172,7 @@ def test_pad_round() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("mode", ("P", "PA")) | @pytest.mark.parametrize("mode", ("P", "PA")) | ||||||
| def test_palette(mode) -> None: | def test_palette(mode: str) -> None: | ||||||
|     im = hopper(mode) |     im = hopper(mode) | ||||||
| 
 | 
 | ||||||
|     # Expand |     # Expand | ||||||
|  | @ -210,7 +214,7 @@ def test_scale() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) | @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: |     with Image.open("Tests/images/p_16.tga") as im: | ||||||
|         im_expanded = ImageOps.expand(im, border, (255, 0, 0)) |         im_expanded = ImageOps.expand(im, border, (255, 0, 0)) | ||||||
| 
 | 
 | ||||||
|  | @ -366,7 +370,7 @@ def test_exif_transpose() -> None: | ||||||
|     for ext in exts: |     for ext in exts: | ||||||
|         with Image.open("Tests/images/hopper" + ext) as base_im: |         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 [ |                 for im in [ | ||||||
|                     orientation_im, |                     orientation_im, | ||||||
|                     orientation_im.copy(), |                     orientation_im.copy(), | ||||||
|  | @ -376,6 +380,7 @@ def test_exif_transpose() -> None: | ||||||
|                     else: |                     else: | ||||||
|                         original_exif = im.info["exif"] |                         original_exif = im.info["exif"] | ||||||
|                     transposed_im = ImageOps.exif_transpose(im) |                     transposed_im = ImageOps.exif_transpose(im) | ||||||
|  |                     assert transposed_im is not None | ||||||
|                     assert_image_similar(base_im, transposed_im, 17) |                     assert_image_similar(base_im, transposed_im, 17) | ||||||
|                     if orientation_im is base_im: |                     if orientation_im is base_im: | ||||||
|                         assert "exif" not in im.info |                         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 |                     # Repeat the operation to test that it does not keep transposing | ||||||
|                     transposed_im2 = ImageOps.exif_transpose(transposed_im) |                     transposed_im2 = ImageOps.exif_transpose(transposed_im) | ||||||
|  |                     assert transposed_im2 is not None | ||||||
|                     assert_image_equal(transposed_im2, transposed_im) |                     assert_image_equal(transposed_im2, transposed_im) | ||||||
| 
 | 
 | ||||||
|             check(base_im) |             check(base_im) | ||||||
|  | @ -402,6 +408,7 @@ def test_exif_transpose() -> None: | ||||||
|             assert im.getexif()[0x0112] == 3 |             assert im.getexif()[0x0112] == 3 | ||||||
| 
 | 
 | ||||||
|             transposed_im = ImageOps.exif_transpose(im) |             transposed_im = ImageOps.exif_transpose(im) | ||||||
|  |             assert transposed_im is not None | ||||||
|             assert 0x0112 not in transposed_im.getexif() |             assert 0x0112 not in transposed_im.getexif() | ||||||
| 
 | 
 | ||||||
|             transposed_im._reload_exif() |             transposed_im._reload_exif() | ||||||
|  | @ -414,12 +421,14 @@ def test_exif_transpose() -> None: | ||||||
|         assert im.getexif()[0x0112] == 3 |         assert im.getexif()[0x0112] == 3 | ||||||
| 
 | 
 | ||||||
|         transposed_im = ImageOps.exif_transpose(im) |         transposed_im = ImageOps.exif_transpose(im) | ||||||
|  |         assert transposed_im is not None | ||||||
|         assert 0x0112 not in transposed_im.getexif() |         assert 0x0112 not in transposed_im.getexif() | ||||||
| 
 | 
 | ||||||
|     # Orientation set directly on Image.Exif |     # Orientation set directly on Image.Exif | ||||||
|     im = hopper() |     im = hopper() | ||||||
|     im.getexif()[0x0112] = 3 |     im.getexif()[0x0112] = 3 | ||||||
|     transposed_im = ImageOps.exif_transpose(im) |     transposed_im = ImageOps.exif_transpose(im) | ||||||
|  |     assert transposed_im is not None | ||||||
|     assert 0x0112 not in transposed_im.getexif() |     assert 0x0112 not in transposed_im.getexif() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -445,7 +454,7 @@ def test_autocontrast_cutoff() -> None: | ||||||
|     # Test the cutoff argument of autocontrast |     # Test the cutoff argument of autocontrast | ||||||
|     with Image.open("Tests/images/bw_gradient.png") as img: |     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() |             return ImageOps.autocontrast(img, cutoff).histogram() | ||||||
| 
 | 
 | ||||||
|         assert autocontrast(10) == autocontrast((10, 10)) |         assert autocontrast(10) == autocontrast((10, 10)) | ||||||
|  | @ -486,20 +495,20 @@ def test_autocontrast_mask_real_input() -> None: | ||||||
|         assert result_nomask != result |         assert result_nomask != result | ||||||
|         assert_tuple_approx_equal( |         assert_tuple_approx_equal( | ||||||
|             ImageStat.Stat(result, mask=rect_mask).median, |             ImageStat.Stat(result, mask=rect_mask).median, | ||||||
|             [195, 202, 184], |             (195, 202, 184), | ||||||
|             threshold=2, |             threshold=2, | ||||||
|             msg="autocontrast with mask pixel incorrect", |             msg="autocontrast with mask pixel incorrect", | ||||||
|         ) |         ) | ||||||
|         assert_tuple_approx_equal( |         assert_tuple_approx_equal( | ||||||
|             ImageStat.Stat(result_nomask).median, |             ImageStat.Stat(result_nomask).median, | ||||||
|             [119, 106, 79], |             (119, 106, 79), | ||||||
|             threshold=2, |             threshold=2, | ||||||
|             msg="autocontrast without mask pixel incorrect", |             msg="autocontrast without mask pixel incorrect", | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_autocontrast_preserve_tone() -> None: | def test_autocontrast_preserve_tone() -> None: | ||||||
|     def autocontrast(mode, preserve_tone): |     def autocontrast(mode: str, preserve_tone: bool) -> list[int]: | ||||||
|         im = hopper(mode) |         im = hopper(mode) | ||||||
|         return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() |         return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() | ||||||
| 
 | 
 | ||||||
|  | @ -533,7 +542,7 @@ def test_autocontrast_preserve_gradient() -> None: | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) |     "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) |     img = Image.new("RGB", (10, 10), color) | ||||||
| 
 | 
 | ||||||
|     # single color images shouldn't change |     # single color images shouldn't change | ||||||
|  |  | ||||||
|  | @ -1,12 +1,14 @@ | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
|  | from typing import Generator | ||||||
|  | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
| from PIL import Image, ImageFilter | from PIL import Image, ImageFilter | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.fixture | @pytest.fixture | ||||||
| def test_images(): | def test_images() -> Generator[dict[str, Image.Image], None, None]: | ||||||
|     ims = { |     ims = { | ||||||
|         "im": Image.open("Tests/images/hopper.ppm"), |         "im": Image.open("Tests/images/hopper.ppm"), | ||||||
|         "snakes": Image.open("Tests/images/color_snakes.png"), |         "snakes": Image.open("Tests/images/color_snakes.png"), | ||||||
|  | @ -18,7 +20,7 @@ def test_images(): | ||||||
|             im.close() |             im.close() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_filter_api(test_images) -> None: | def test_filter_api(test_images: dict[str, Image.Image]) -> None: | ||||||
|     im = test_images["im"] |     im = test_images["im"] | ||||||
| 
 | 
 | ||||||
|     test_filter = ImageFilter.GaussianBlur(2.0) |     test_filter = ImageFilter.GaussianBlur(2.0) | ||||||
|  | @ -26,13 +28,13 @@ def test_filter_api(test_images) -> None: | ||||||
|     assert i.mode == "RGB" |     assert i.mode == "RGB" | ||||||
|     assert i.size == (128, 128) |     assert i.size == (128, 128) | ||||||
| 
 | 
 | ||||||
|     test_filter = ImageFilter.UnsharpMask(2.0, 125, 8) |     test_filter2 = ImageFilter.UnsharpMask(2.0, 125, 8) | ||||||
|     i = im.filter(test_filter) |     i = im.filter(test_filter2) | ||||||
|     assert i.mode == "RGB" |     assert i.mode == "RGB" | ||||||
|     assert i.size == (128, 128) |     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"] |     im = test_images["im"] | ||||||
| 
 | 
 | ||||||
|     usm = ImageFilter.UnsharpMask |     usm = ImageFilter.UnsharpMask | ||||||
|  | @ -50,7 +52,7 @@ def test_usm_formats(test_images) -> None: | ||||||
|         im.convert("YCbCr").filter(usm) |         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"] |     im = test_images["im"] | ||||||
| 
 | 
 | ||||||
|     blur = ImageFilter.GaussianBlur |     blur = ImageFilter.GaussianBlur | ||||||
|  | @ -68,7 +70,7 @@ def test_blur_formats(test_images) -> None: | ||||||
|         im.convert("YCbCr").filter(blur) |         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"] |     snakes = test_images["snakes"] | ||||||
| 
 | 
 | ||||||
|     src = snakes.convert("RGB") |     src = snakes.convert("RGB") | ||||||
|  | @ -77,7 +79,7 @@ def test_usm_accuracy(test_images) -> None: | ||||||
|     assert i.tobytes() == src.tobytes() |     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"] |     snakes = test_images["snakes"] | ||||||
| 
 | 
 | ||||||
|     i = snakes.filter(ImageFilter.GaussianBlur(0.4)) |     i = snakes.filter(ImageFilter.GaussianBlur(0.4)) | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ from __future__ import annotations | ||||||
| import array | import array | ||||||
| import math | import math | ||||||
| import struct | import struct | ||||||
|  | from typing import Sequence | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
|  | @ -57,7 +58,9 @@ def test_path() -> None: | ||||||
|         ImagePath.Path((0, 1)), |         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 |     # Arrange / Act | ||||||
|     p = ImagePath.Path(coords) |     p = ImagePath.Path(coords) | ||||||
| 
 | 
 | ||||||
|  | @ -75,7 +78,9 @@ def test_path_constructors(coords) -> None: | ||||||
|         [[0.0, 1.0]], |         [[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 |     # Act | ||||||
|     with pytest.raises(ValueError) as e: |     with pytest.raises(ValueError) as e: | ||||||
|         ImagePath.Path(coords) |         ImagePath.Path(coords) | ||||||
|  | @ -93,7 +98,7 @@ def test_invalid_path_constructors(coords) -> None: | ||||||
|         [0, 1, 2], |         [0, 1, 2], | ||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
| def test_path_odd_number_of_coordinates(coords) -> None: | def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: | ||||||
|     # Act |     # Act | ||||||
|     with pytest.raises(ValueError) as e: |     with pytest.raises(ValueError) as e: | ||||||
|         ImagePath.Path(coords) |         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)), |         (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 |     # Arrange | ||||||
|     p = ImagePath.Path(coords) |     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)]), |         (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 |     # Arrange | ||||||
|     p = ImagePath.Path(coords) |     p = ImagePath.Path(coords) | ||||||
| 
 | 
 | ||||||
|  | @ -201,9 +208,9 @@ class Evil: | ||||||
|     def __init__(self) -> None: |     def __init__(self) -> None: | ||||||
|         self.corrupt = Image.core.path(0x4000000000000000) |         self.corrupt = Image.core.path(0x4000000000000000) | ||||||
| 
 | 
 | ||||||
|     def __getitem__(self, i): |     def __getitem__(self, i: int) -> bytes: | ||||||
|         x = self.corrupt[i] |         x = self.corrupt[i] | ||||||
|         return struct.pack("dd", x[0], x[1]) |         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) |         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) |     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 = ImageQt.rgb(r, g, b) | ||||||
|         val = val % 2**24  # drop the alpha |         val = val % 2**24  # drop the alpha | ||||||
|         assert val >> 16 == r |         assert val >> 16 == r | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ def test_sanity(tmp_path: Path) -> None: | ||||||
|     assert index == 1 |     assert index == 1 | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(AttributeError): |     with pytest.raises(AttributeError): | ||||||
|         ImageSequence.Iterator(0) |         ImageSequence.Iterator(0)  # type: ignore[arg-type] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_iterator() -> None: | def test_iterator() -> None: | ||||||
|  | @ -72,6 +72,7 @@ def test_consecutive() -> None: | ||||||
|         for frame in ImageSequence.Iterator(im): |         for frame in ImageSequence.Iterator(im): | ||||||
|             if first_frame is None: |             if first_frame is None: | ||||||
|                 first_frame = frame.copy() |                 first_frame = frame.copy() | ||||||
|  |         assert first_frame is not None | ||||||
|         for frame in ImageSequence.Iterator(im): |         for frame in ImageSequence.Iterator(im): | ||||||
|             assert_image_equal(frame, first_frame) |             assert_image_equal(frame, first_frame) | ||||||
|             break |             break | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
|  | from typing import Any | ||||||
|  | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
| from PIL import Image, ImageShow | from PIL import Image, ImageShow | ||||||
|  | @ -24,9 +26,9 @@ def test_register() -> None: | ||||||
|     "order", |     "order", | ||||||
|     [-1, 0], |     [-1, 0], | ||||||
| ) | ) | ||||||
| def test_viewer_show(order) -> None: | def test_viewer_show(order: int) -> None: | ||||||
|     class TestViewer(ImageShow.Viewer): |     class TestViewer(ImageShow.Viewer): | ||||||
|         def show_image(self, image, **options) -> bool: |         def show_image(self, image: Image.Image, **options: Any) -> bool: | ||||||
|             self.methodCalled = True |             self.methodCalled = True | ||||||
|             return True |             return True | ||||||
| 
 | 
 | ||||||
|  | @ -48,7 +50,7 @@ def test_viewer_show(order) -> None: | ||||||
|     reason="Only run on CIs; hangs on Windows CIs", |     reason="Only run on CIs; hangs on Windows CIs", | ||||||
| ) | ) | ||||||
| @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) | @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) | ||||||
| def test_show(mode) -> None: | def test_show(mode: str) -> None: | ||||||
|     im = hopper(mode) |     im = hopper(mode) | ||||||
|     assert ImageShow.show(im) |     assert ImageShow.show(im) | ||||||
| 
 | 
 | ||||||
|  | @ -66,14 +68,15 @@ def test_show_without_viewers() -> None: | ||||||
| def test_viewer() -> None: | def test_viewer() -> None: | ||||||
|     viewer = ImageShow.Viewer() |     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): |     with pytest.raises(NotImplementedError): | ||||||
|         viewer.get_command(None) |         viewer.get_command("") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("viewer", ImageShow._viewers) | @pytest.mark.parametrize("viewer", ImageShow._viewers) | ||||||
| def test_viewers(viewer) -> None: | def test_viewers(viewer: ImageShow.Viewer) -> None: | ||||||
|     try: |     try: | ||||||
|         viewer.get_command("test.jpg") |         viewer.get_command("test.jpg") | ||||||
|     except NotImplementedError: |     except NotImplementedError: | ||||||
|  |  | ||||||
|  | @ -70,7 +70,7 @@ if is_win32(): | ||||||
|     ] |     ] | ||||||
|     CreateDIBSection.restype = ctypes.wintypes.HBITMAP |     CreateDIBSection.restype = ctypes.wintypes.HBITMAP | ||||||
| 
 | 
 | ||||||
|     def serialize_dib(bi, pixels): |     def serialize_dib(bi, pixels) -> bytearray: | ||||||
|         bf = BITMAPFILEHEADER() |         bf = BITMAPFILEHEADER() | ||||||
|         bf.bfType = 0x4D42 |         bf.bfType = 0x4D42 | ||||||
|         bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize |         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 test_tobytes() -> None: | ||||||
|     def tobytes(mode: str) -> Image.Image: |     def tobytes(mode: str) -> bytes: | ||||||
|         return Image.new(mode, (1, 1), 1).tobytes() |         return Image.new(mode, (1, 1), 1).tobytes() | ||||||
| 
 | 
 | ||||||
|     order = 1 if Image._ENDIAN == "<" else -1 |     order = 1 if Image._ENDIAN == "<" else -1 | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ TEST_IMAGE_SIZE = (10, 10) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_numpy_to_image() -> None: | 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 bands == 1: | ||||||
|             if boolean: |             if boolean: | ||||||
|                 data = [0, 255] * 50 |                 data = [0, 255] * 50 | ||||||
|  | @ -99,7 +99,7 @@ def test_1d_array() -> None: | ||||||
|     assert_image(Image.fromarray(a), "L", (1, 5)) |     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 |     assert len(np.shape) >= 2 | ||||||
|     np_size = np.shape[1], np.shape[0] |     np_size = np.shape[1], np.shape[0] | ||||||
|     assert img.size == np_size |     assert img.size == np_size | ||||||
|  | @ -157,7 +157,7 @@ def test_save_tiff_uint16() -> None: | ||||||
|         ("HSV", numpy.uint8), |         ("HSV", numpy.uint8), | ||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
| def test_to_array(mode, dtype) -> None: | def test_to_array(mode: str, dtype) -> None: | ||||||
|     img = hopper(mode) |     img = hopper(mode) | ||||||
| 
 | 
 | ||||||
|     # Resize to non-square |     # Resize to non-square | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ from pathlib import Path | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
| from PIL import ImageQt | from PIL import Image, ImageQt | ||||||
| 
 | 
 | ||||||
| from .helper import assert_image_equal_tofile, assert_image_similar, hopper | from .helper import assert_image_equal_tofile, assert_image_similar, hopper | ||||||
| 
 | 
 | ||||||
|  | @ -37,7 +37,7 @@ if ImageQt.qt_is_installed: | ||||||
|             lbl.setPixmap(pixmap1.copy()) |             lbl.setPixmap(pixmap1.copy()) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def roundtrip(expected) -> None: | def roundtrip(expected: Image.Image) -> None: | ||||||
|     result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) |     result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) | ||||||
|     # Qt saves all pixmaps as rgb |     # Qt saves all pixmaps as rgb | ||||||
|     assert_image_similar(result, expected.convert("RGB"), 1) |     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")) | @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) |     src = hopper(mode) | ||||||
|     data = ImageQt.toqimage(src) |     data = ImageQt.toqimage(src) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -47,9 +47,8 @@ def test_tiff_crashes(test_file: str) -> None: | ||||||
|         with Image.open(test_file) as im: |         with Image.open(test_file) as im: | ||||||
|             im.load() |             im.load() | ||||||
|     except FileNotFoundError: |     except FileNotFoundError: | ||||||
|         if not on_ci(): |         if on_ci(): | ||||||
|             pytest.skip("test image not found") |  | ||||||
|             return |  | ||||||
|             raise |             raise | ||||||
|  |         pytest.skip("test image not found") | ||||||
|     except OSError: |     except OSError: | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ from PIL import _util | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] |     "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 |     # Act | ||||||
|     it_is = _util.is_path(test_path) |     it_is = _util.is_path(test_path) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| #!/bin/bash | #!/bin/bash | ||||||
| # install openjpeg | # 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 | ./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://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/?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://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/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]+", |     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 | 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 | (2018-01). The deprecated file has now been removed from Pillow. If needed, install from | ||||||
| PyPI (eg. ``python3 -m pip install olefile``). | 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 |    :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow | ||||||
|    :alt: Zenodo |    :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 |    :target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge | ||||||
|    :alt: Tidelift |    :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 |    :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 |    :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 | .. image:: https://img.shields.io/badge/publish-on%20Mastodon-595aff.svg | ||||||
|    :target: https://fosstodon.org/@pillow |    :target: https://fosstodon.org/@pillow | ||||||
|    :alt: Follow on 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:: | .. toctree:: | ||||||
|    :maxdepth: 2 |    :maxdepth: 2 | ||||||
| 
 | 
 | ||||||
|    installation.rst |    installation/index.rst | ||||||
|    handbook/index.rst |    handbook/index.rst | ||||||
|    reference/index.rst |    reference/index.rst | ||||||
|    porting.rst |    porting.rst | ||||||
|  |  | ||||||
|  | @ -1,606 +1,29 @@ | ||||||
|  | :orphan: | ||||||
|  | 
 | ||||||
| Installation | 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 | 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 | Python Support | ||||||
|     most common image formats. See :ref:`external-libraries` for a | -------------- | ||||||
|     full list of external libraries supported. |  | ||||||
| 
 | 
 | ||||||
| Install Pillow with :command:`pip`:: | .. Note:: This section has moved to :ref:`python-support`. Please update references accordingly. | ||||||
| 
 |  | ||||||
|     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 |  | ||||||
| 
 | 
 | ||||||
| Platform Support | Platform Support | ||||||
| ---------------- | ---------------- | ||||||
| 
 | 
 | ||||||
| Current platform support for Pillow. Binary distributions are | .. Note:: This section has moved to :ref:`platform-support`. Please update references accordingly. | ||||||
| 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 | Building From Source | ||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | -------------------- | ||||||
| 
 | 
 | ||||||
| These platforms are built and tested for every change. | .. Note:: This section has moved to :ref:`building-from-source`. Please update references accordingly. | ||||||
| 
 |  | ||||||
| +----------------------------------+----------------------------+---------------------+ |  | ||||||
| | 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        | |  | ||||||
| +----------------------------------+----------------------------+------------------+--------------+ |  | ||||||
| 
 | 
 | ||||||
| Old Versions | Old Versions | ||||||
| ------------ | ------------ | ||||||
| 
 | 
 | ||||||
| You can download old distributions from the `release history at PyPI | .. Note:: This section has moved to :ref:`old-versions`. Please update references accordingly. | ||||||
| <https://pypi.org/project/pillow/#history>`_ and by direct URL access |  | ||||||
| eg. https://pypi.org/project/pillow/1.0/. |  | ||||||
|  |  | ||||||
							
								
								
									
										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:: colorize | ||||||
| .. autofunction:: crop | .. autofunction:: crop | ||||||
| .. autofunction:: scale | .. autofunction:: scale | ||||||
|  | .. autoclass:: SupportsGetMesh | ||||||
|  |     :show-inheritance: | ||||||
| .. autofunction:: deform | .. autofunction:: deform | ||||||
| .. autofunction:: equalize | .. autofunction:: equalize | ||||||
| .. autofunction:: expand | .. autofunction:: expand | ||||||
|  |  | ||||||
|  | @ -79,3 +79,9 @@ Portable FloatMap (PFM) images | ||||||
| 
 | 
 | ||||||
| Support has been added for reading and writing grayscale (Pf format) | Support has been added for reading and writing grayscale (Pf format) | ||||||
| Portable FloatMap (PFM) files containing ``F`` data. | 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 :: Capture :: Screen Capture", | ||||||
|   "Topic :: Multimedia :: Graphics :: Graphics Conversion", |   "Topic :: Multimedia :: Graphics :: Graphics Conversion", | ||||||
|   "Topic :: Multimedia :: Graphics :: Viewers", |   "Topic :: Multimedia :: Graphics :: Viewers", | ||||||
|  |   "Typing :: Typed", | ||||||
| ] | ] | ||||||
| dynamic = [ | dynamic = [ | ||||||
|   "version", |   "version", | ||||||
|  | @ -79,7 +80,6 @@ Homepage = "https://python-pillow.org" | ||||||
| Mastodon = "https://fosstodon.org/@pillow" | Mastodon = "https://fosstodon.org/@pillow" | ||||||
| "Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" | "Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" | ||||||
| Source = "https://github.com/python-pillow/Pillow" | Source = "https://github.com/python-pillow/Pillow" | ||||||
| Twitter = "https://twitter.com/PythonPillow" |  | ||||||
| 
 | 
 | ||||||
| [tool.setuptools] | [tool.setuptools] | ||||||
| packages = ["PIL"] | packages = ["PIL"] | ||||||
|  | @ -140,7 +140,3 @@ follow_imports = "silent" | ||||||
| warn_redundant_casts = true | warn_redundant_casts = true | ||||||
| warn_unreachable = true | warn_unreachable = true | ||||||
| warn_unused_ignores = true | warn_unused_ignores = true | ||||||
| exclude = [ |  | ||||||
|   '^src/PIL/FpxImagePlugin.py$', |  | ||||||
|   '^src/PIL/MicImagePlugin.py$', |  | ||||||
| ] |  | ||||||
|  |  | ||||||
|  | @ -38,7 +38,7 @@ from ._deprecate import deprecate | ||||||
| split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") | split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") | ||||||
| field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") | field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") | ||||||
| 
 | 
 | ||||||
| gs_binary = None | gs_binary: str | bool | None = None | ||||||
| gs_windows_binary = None | gs_windows_binary = None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||