mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-10-31 07:57:27 +03:00 
			
		
		
		
	Merge branch 'main' into imaging-cleanup
This commit is contained in:
		
						commit
						b627fb97c7
					
				|  | @ -21,7 +21,7 @@ set -e | |||
| 
 | ||||
| if [[ $(uname) != CYGWIN* ]]; then | ||||
|     sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ | ||||
|                              ghostscript libjpeg-turbo-progs libopenjp2-7-dev\ | ||||
|                              ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ | ||||
|                              cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ | ||||
|                              sway wl-clipboard libopenblas-dev | ||||
| fi | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| cibuildwheel==2.21.2 | ||||
| cibuildwheel==2.22.0 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| mypy==1.11.2 | ||||
| mypy==1.14.0 | ||||
| IceSpringPySideStubs-PyQt6 | ||||
| IceSpringPySideStubs-PySide6 | ||||
| ipython | ||||
|  |  | |||
							
								
								
									
										1
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							|  | @ -19,7 +19,6 @@ Please send a pull request to the `main` branch. Please include [documentation]( | |||
| - Follow PEP 8. | ||||
| - When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. | ||||
| - Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. | ||||
| - Do not add to the [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged. | ||||
| 
 | ||||
| ## Reporting Issues | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										11
									
								
								.github/release-drafter.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/release-drafter.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -3,18 +3,19 @@ tag-template: "$NEXT_MINOR_VERSION" | |||
| change-template: '- $TITLE #$NUMBER [@$AUTHOR]' | ||||
| 
 | ||||
| categories: | ||||
|   - title: "Dependencies" | ||||
|     label: "Dependency" | ||||
|   - title: "Removals" | ||||
|     label: "Removal" | ||||
|   - title: "Deprecations" | ||||
|     label: "Deprecation" | ||||
|   - title: "Documentation" | ||||
|     label: "Documentation" | ||||
|   - title: "Removals" | ||||
|     label: "Removal" | ||||
|   - title: "Dependencies" | ||||
|     label: "Dependency" | ||||
|   - title: "Testing" | ||||
|     label: "Testing" | ||||
|   - title: "Type hints" | ||||
|     label: "Type hints" | ||||
|   - title: "Other changes" | ||||
| 
 | ||||
| exclude-labels: | ||||
|   - "changelog: skip" | ||||
|  | @ -23,6 +24,4 @@ template: | | |||
| 
 | ||||
|   https://pillow.readthedocs.io/en/stable/releasenotes/$NEXT_MINOR_VERSION.html | ||||
| 
 | ||||
|   ## Changes | ||||
| 
 | ||||
|   $CHANGES | ||||
|  |  | |||
							
								
								
									
										12
									
								
								.github/renovate.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/renovate.json
									
									
									
									
										vendored
									
									
								
							|  | @ -1,7 +1,7 @@ | |||
| { | ||||
|     "$schema": "https://docs.renovatebot.com/renovate-schema.json", | ||||
|     "extends": [ | ||||
|         "config:base" | ||||
|         "config:recommended" | ||||
|     ], | ||||
|     "labels": [ | ||||
|         "Dependency" | ||||
|  | @ -9,9 +9,13 @@ | |||
|     "packageRules": [ | ||||
|         { | ||||
|             "groupName": "github-actions", | ||||
|             "matchManagers": ["github-actions"], | ||||
|             "separateMajorMinor": "false" | ||||
|             "matchManagers": [ | ||||
|                 "github-actions" | ||||
|             ], | ||||
|             "separateMajorMinor": false | ||||
|         } | ||||
|     ], | ||||
|     "schedule": ["on the 3rd day of the month"] | ||||
|     "schedule": [ | ||||
|         "on the 3rd day of the month" | ||||
|     ] | ||||
| } | ||||
|  |  | |||
							
								
								
									
										2
									
								
								.github/workflows/docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/docs.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -33,6 +33,8 @@ jobs: | |||
| 
 | ||||
|     steps: | ||||
|     - uses: actions/checkout@v4 | ||||
|       with: | ||||
|         persist-credentials: false | ||||
| 
 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v5 | ||||
|  |  | |||
							
								
								
									
										2
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -21,6 +21,8 @@ jobs: | |||
| 
 | ||||
|     steps: | ||||
|     - uses: actions/checkout@v4 | ||||
|       with: | ||||
|         persist-credentials: false | ||||
| 
 | ||||
|     - name: pre-commit cache | ||||
|       uses: actions/cache@v4 | ||||
|  |  | |||
							
								
								
									
										2
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							|  | @ -8,8 +8,8 @@ fi | |||
| brew install \ | ||||
|     freetype \ | ||||
|     ghostscript \ | ||||
|     jpeg-turbo \ | ||||
|     libimagequant \ | ||||
|     libjpeg \ | ||||
|     libtiff \ | ||||
|     little-cms2 \ | ||||
|     openjpeg \ | ||||
|  |  | |||
							
								
								
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -6,7 +6,7 @@ on: | |||
|   workflow_dispatch: | ||||
| 
 | ||||
| permissions: | ||||
|   issues: write | ||||
|   contents: read | ||||
| 
 | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|  | @ -15,6 +15,8 @@ concurrency: | |||
| jobs: | ||||
|   stale: | ||||
|     if: github.repository_owner == 'python-pillow' | ||||
|     permissions: | ||||
|       issues: write | ||||
| 
 | ||||
|     runs-on: ubuntu-latest | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										7
									
								
								.github/workflows/test-cygwin.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/test-cygwin.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -48,6 +48,8 @@ jobs: | |||
| 
 | ||||
|       - name: Checkout Pillow | ||||
|         uses: actions/checkout@v4 | ||||
|         with: | ||||
|           persist-credentials: false | ||||
| 
 | ||||
|       - name: Install Cygwin | ||||
|         uses: cygwin/cygwin-install-action@v4 | ||||
|  | @ -131,11 +133,12 @@ jobs: | |||
|       - name: After success | ||||
|         run: | | ||||
|           bash.exe .ci/after_success.sh | ||||
|           rm C:\cygwin\bin\bash.EXE | ||||
| 
 | ||||
|       - name: Upload coverage | ||||
|         uses: codecov/codecov-action@v4 | ||||
|         uses: codecov/codecov-action@v5 | ||||
|         with: | ||||
|           file: ./coverage.xml | ||||
|           files: ./coverage.xml | ||||
|           flags: GHA_Cygwin | ||||
|           name: Cygwin Python 3.${{ matrix.python-minor-version }} | ||||
|           token: ${{ secrets.CODECOV_ORG_TOKEN }} | ||||
|  |  | |||
							
								
								
									
										7
									
								
								.github/workflows/test-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/test-docker.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -46,8 +46,8 @@ jobs: | |||
|           centos-stream-9-amd64, | ||||
|           debian-12-bookworm-x86, | ||||
|           debian-12-bookworm-amd64, | ||||
|           fedora-39-amd64, | ||||
|           fedora-40-amd64, | ||||
|           fedora-41-amd64, | ||||
|           gentoo, | ||||
|           ubuntu-22.04-jammy-amd64, | ||||
|           ubuntu-24.04-noble-amd64, | ||||
|  | @ -65,6 +65,8 @@ jobs: | |||
| 
 | ||||
|     steps: | ||||
|     - uses: actions/checkout@v4 | ||||
|       with: | ||||
|         persist-credentials: false | ||||
| 
 | ||||
|     - name: Build system information | ||||
|       run: python3 .github/workflows/system-info.py | ||||
|  | @ -98,11 +100,10 @@ jobs: | |||
|         MATRIX_DOCKER: ${{ matrix.docker }} | ||||
| 
 | ||||
|     - name: Upload coverage | ||||
|       uses: codecov/codecov-action@v4 | ||||
|       uses: codecov/codecov-action@v5 | ||||
|       with: | ||||
|         flags: GHA_Docker | ||||
|         name: ${{ matrix.docker }} | ||||
|         gcov: true | ||||
|         token: ${{ secrets.CODECOV_ORG_TOKEN }} | ||||
| 
 | ||||
|   success: | ||||
|  |  | |||
							
								
								
									
										16
									
								
								.github/workflows/test-mingw.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								.github/workflows/test-mingw.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -46,6 +46,8 @@ jobs: | |||
|     steps: | ||||
|       - name: Checkout Pillow | ||||
|         uses: actions/checkout@v4 | ||||
|         with: | ||||
|           persist-credentials: false | ||||
| 
 | ||||
|       - name: Set up shell | ||||
|         run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH | ||||
|  | @ -66,16 +68,16 @@ jobs: | |||
|               mingw-w64-x86_64-openjpeg2 \ | ||||
|               mingw-w64-x86_64-python3-numpy \ | ||||
|               mingw-w64-x86_64-python3-olefile \ | ||||
|               mingw-w64-x86_64-python3-setuptools \ | ||||
|               mingw-w64-x86_64-python3-pip \ | ||||
|               mingw-w64-x86_64-python-pytest \ | ||||
|               mingw-w64-x86_64-python-pytest-cov \ | ||||
|               mingw-w64-x86_64-python-pytest-timeout \ | ||||
|               mingw-w64-x86_64-python-pyqt6 | ||||
| 
 | ||||
|           python3 -m ensurepip | ||||
|           python3 -m pip install pyroma pytest pytest-cov pytest-timeout | ||||
| 
 | ||||
|           pushd depends && ./install_extra_test_images.sh && popd | ||||
| 
 | ||||
|       - name: Build Pillow | ||||
|         run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install . | ||||
|         run: CFLAGS="-coverage" python3 -m pip install . | ||||
| 
 | ||||
|       - name: Test Pillow | ||||
|         run: | | ||||
|  | @ -83,9 +85,9 @@ jobs: | |||
|           .ci/test.sh | ||||
| 
 | ||||
|       - name: Upload coverage | ||||
|         uses: codecov/codecov-action@v4 | ||||
|         uses: codecov/codecov-action@v5 | ||||
|         with: | ||||
|           file: ./coverage.xml | ||||
|           files: ./coverage.xml | ||||
|           flags: GHA_Windows | ||||
|           name: "MSYS2 MinGW" | ||||
|           token: ${{ secrets.CODECOV_ORG_TOKEN }} | ||||
|  |  | |||
							
								
								
									
										2
									
								
								.github/workflows/test-valgrind.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/test-valgrind.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -40,6 +40,8 @@ jobs: | |||
| 
 | ||||
|     steps: | ||||
|     - uses: actions/checkout@v4 | ||||
|       with: | ||||
|         persist-credentials: false | ||||
| 
 | ||||
|     - name: Build system information | ||||
|       run: python3 .github/workflows/system-info.py | ||||
|  |  | |||
							
								
								
									
										28
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -44,16 +44,20 @@ jobs: | |||
|     steps: | ||||
|     - name: Checkout Pillow | ||||
|       uses: actions/checkout@v4 | ||||
|       with: | ||||
|         persist-credentials: false | ||||
| 
 | ||||
|     - name: Checkout cached dependencies | ||||
|       uses: actions/checkout@v4 | ||||
|       with: | ||||
|         persist-credentials: false | ||||
|         repository: python-pillow/pillow-depends | ||||
|         path: winbuild\depends | ||||
| 
 | ||||
|     - name: Checkout extra test images | ||||
|       uses: actions/checkout@v4 | ||||
|       with: | ||||
|         persist-credentials: false | ||||
|         repository: python-pillow/test-images | ||||
|         path: Tests\test-images | ||||
| 
 | ||||
|  | @ -69,16 +73,14 @@ jobs: | |||
|     - name: Print build system information | ||||
|       run: python3 .github/workflows/system-info.py | ||||
| 
 | ||||
|     - name: Install Python dependencies | ||||
|       run: > | ||||
|         python3 -m pip install | ||||
|         coverage>=7.4.2 | ||||
|         defusedxml | ||||
|         olefile | ||||
|         pyroma | ||||
|         pytest | ||||
|         pytest-cov | ||||
|         pytest-timeout | ||||
|     - name: Upgrade pip | ||||
|       run: | | ||||
|         python3 -m pip install --upgrade pip | ||||
| 
 | ||||
|     - name: Install CPython dependencies | ||||
|       if: "!contains(matrix.python-version, 'pypy')" | ||||
|       run: | | ||||
|         python3 -m pip install PyQt6 | ||||
| 
 | ||||
|     - name: Install dependencies | ||||
|       id: install | ||||
|  | @ -178,7 +180,7 @@ jobs: | |||
|     - name: Build Pillow | ||||
|       run: | | ||||
|         $FLAGS="-C raqm=vendor -C fribidi=vendor" | ||||
|         cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ." | ||||
|         cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS .[tests]" | ||||
|         & $env:pythonLocation\python.exe selftest.py --installed | ||||
|       shell: pwsh | ||||
| 
 | ||||
|  | @ -213,9 +215,9 @@ jobs: | |||
|       shell: pwsh | ||||
| 
 | ||||
|     - name: Upload coverage | ||||
|       uses: codecov/codecov-action@v4 | ||||
|       uses: codecov/codecov-action@v5 | ||||
|       with: | ||||
|         file: ./coverage.xml | ||||
|         files: ./coverage.xml | ||||
|         flags: GHA_Windows | ||||
|         name: ${{ runner.os }} Python ${{ matrix.python-version }} | ||||
|         token: ${{ secrets.CODECOV_ORG_TOKEN }} | ||||
|  |  | |||
							
								
								
									
										22
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -42,6 +42,7 @@ jobs: | |||
|         ] | ||||
|         python-version: [ | ||||
|           "pypy3.10", | ||||
|           "3.13t", | ||||
|           "3.13", | ||||
|           "3.12", | ||||
|           "3.11", | ||||
|  | @ -52,21 +53,22 @@ jobs: | |||
|         - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } | ||||
|         - { python-version: "3.10", PYTHONOPTIMIZE: 2 } | ||||
|         # Free-threaded | ||||
|         - { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true } | ||||
|         - { python-version: "3.13t", disable-gil: true } | ||||
|         # M1 only available for 3.10+ | ||||
|         - { os: "macos-13", python-version: "3.9" } | ||||
|         exclude: | ||||
|         - { os: "macos-latest", python-version: "3.9" } | ||||
| 
 | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }} | ||||
|     name: ${{ matrix.os }} Python ${{ matrix.python-version }} | ||||
| 
 | ||||
|     steps: | ||||
|     - uses: actions/checkout@v4 | ||||
|       with: | ||||
|         persist-credentials: false | ||||
| 
 | ||||
|     - name: Set up Python ${{ matrix.python-version }} | ||||
|       uses: actions/setup-python@v5 | ||||
|       if: "${{ !matrix.disable-gil }}" | ||||
|       uses: Quansight-Labs/setup-python@v5 | ||||
|       with: | ||||
|         python-version: ${{ matrix.python-version }} | ||||
|         allow-prereleases: true | ||||
|  | @ -75,13 +77,6 @@ jobs: | |||
|           ".ci/*.sh" | ||||
|           "pyproject.toml" | ||||
| 
 | ||||
|     - name: Set up Python ${{ matrix.python-version }} (free-threaded) | ||||
|       uses: deadsnakes/action@v3.2.0 | ||||
|       if: "${{ matrix.disable-gil }}" | ||||
|       with: | ||||
|         python-version: ${{ matrix.python-version }} | ||||
|         nogil: ${{ matrix.disable-gil }} | ||||
| 
 | ||||
|     - name: Set PYTHON_GIL | ||||
|       if: "${{ matrix.disable-gil }}" | ||||
|       run: | | ||||
|  | @ -114,7 +109,7 @@ jobs: | |||
|         GHA_PYTHON_VERSION: ${{ matrix.python-version }} | ||||
| 
 | ||||
|     - name: Register gcc problem matcher | ||||
|       if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'" | ||||
|       if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'" | ||||
|       run: echo "::add-matcher::.github/problem-matchers/gcc.json" | ||||
| 
 | ||||
|     - name: Build | ||||
|  | @ -154,11 +149,10 @@ jobs: | |||
|         .ci/after_success.sh | ||||
| 
 | ||||
|     - name: Upload coverage | ||||
|       uses: codecov/codecov-action@v4 | ||||
|       uses: codecov/codecov-action@v5 | ||||
|       with: | ||||
|         flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} | ||||
|         name: ${{ matrix.os }} Python ${{ matrix.python-version }} | ||||
|         gcov: true | ||||
|         token: ${{ secrets.CODECOV_ORG_TOKEN }} | ||||
| 
 | ||||
|   success: | ||||
|  |  | |||
							
								
								
									
										170
									
								
								.github/workflows/wheels-dependencies.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										170
									
								
								.github/workflows/wheels-dependencies.sh
									
									
									
									
										vendored
									
									
								
							|  | @ -1,11 +1,33 @@ | |||
| #!/bin/bash | ||||
| # Define custom utilities | ||||
| # Test for macOS with [ -n "$IS_MACOS" ] | ||||
| if [ -z "$IS_MACOS" ]; then | ||||
|     export MB_ML_LIBC=${AUDITWHEEL_POLICY::9} | ||||
|     export MB_ML_VER=${AUDITWHEEL_POLICY:9} | ||||
| 
 | ||||
| # Setup that needs to be done before multibuild utils are invoked | ||||
| PROJECTDIR=$(pwd) | ||||
| if [[ "$(uname -s)" == "Darwin" ]]; then | ||||
|     # Safety check - macOS builds require that CIBW_ARCHS is set, and that it | ||||
|     # only contains a single value (even though cibuildwheel allows multiple | ||||
|     # values in CIBW_ARCHS). | ||||
|     if [[ -z "$CIBW_ARCHS" ]]; then | ||||
|         echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined." | ||||
|         exit 1 | ||||
|     fi | ||||
|     if [[ "$CIBW_ARCHS" == *" "* ]]; then | ||||
|         echo "ERROR: Pillow macOS builds only support a single architecture in CIBW_ARCHS." | ||||
|         exit 1 | ||||
|     fi | ||||
| 
 | ||||
|     # Build macOS dependencies in `build/darwin` | ||||
|     # Install them into `build/deps/darwin` | ||||
|     WORKDIR=$(pwd)/build/darwin | ||||
|     BUILD_PREFIX=$(pwd)/build/deps/darwin | ||||
| else | ||||
|     # Build prefix will default to /usr/local | ||||
|     WORKDIR=$(pwd)/build | ||||
|     MB_ML_LIBC=${AUDITWHEEL_POLICY::9} | ||||
|     MB_ML_VER=${AUDITWHEEL_POLICY:9} | ||||
| fi | ||||
| export PLAT=$CIBW_ARCHS | ||||
| PLAT=$CIBW_ARCHS | ||||
| 
 | ||||
| # Define custom utilities | ||||
| source wheels/multibuild/common_utils.sh | ||||
| source wheels/multibuild/library_builders.sh | ||||
| if [ -z "$IS_MACOS" ]; then | ||||
|  | @ -16,10 +38,10 @@ ARCHIVE_SDIR=pillow-depends-main | |||
| 
 | ||||
| # Package versions for fresh source builds | ||||
| FREETYPE_VERSION=2.13.2 | ||||
| HARFBUZZ_VERSION=10.0.1 | ||||
| HARFBUZZ_VERSION=10.1.0 | ||||
| LIBPNG_VERSION=1.6.44 | ||||
| JPEGTURBO_VERSION=3.0.4 | ||||
| OPENJPEG_VERSION=2.5.2 | ||||
| JPEGTURBO_VERSION=3.1.0 | ||||
| OPENJPEG_VERSION=2.5.3 | ||||
| XZ_VERSION=5.6.3 | ||||
| TIFF_VERSION=4.6.0 | ||||
| LCMS2_VERSION=2.16 | ||||
|  | @ -28,82 +50,90 @@ if [[ -n "$IS_MACOS" ]]; then | |||
| else | ||||
|     GIFLIB_VERSION=5.2.1 | ||||
| fi | ||||
| if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then | ||||
|     ZLIB_VERSION=1.3.1 | ||||
| else | ||||
|     ZLIB_VERSION=1.2.8 | ||||
| fi | ||||
| LIBWEBP_VERSION=1.4.0 | ||||
| ZLIB_NG_VERSION=2.2.2 | ||||
| LIBWEBP_VERSION=1.5.0 | ||||
| BZIP2_VERSION=1.0.8 | ||||
| LIBXCB_VERSION=1.17.0 | ||||
| BROTLI_VERSION=1.1.0 | ||||
| 
 | ||||
| if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then | ||||
|     function build_openjpeg { | ||||
|         local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v$OPENJPEG_VERSION.tar.gz openjpeg-$OPENJPEG_VERSION.tar.gz) | ||||
|         (cd $out_dir \ | ||||
|             && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ | ||||
| function build_pkg_config { | ||||
|     if [ -e pkg-config-stamp ]; then return; fi | ||||
|     # This essentially duplicates the Homebrew recipe | ||||
|     ORIGINAL_CFLAGS=$CFLAGS | ||||
|     CFLAGS="$CFLAGS -Wno-int-conversion" | ||||
|     build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \ | ||||
|         --disable-debug --disable-host-tool --with-internal-glib \ | ||||
|         --with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \ | ||||
|         --with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include | ||||
|     CFLAGS=$ORIGINAL_CFLAGS | ||||
|     export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config | ||||
|     touch pkg-config-stamp | ||||
| } | ||||
| 
 | ||||
| function build_zlib_ng { | ||||
|     if [ -e zlib-stamp ]; then return; fi | ||||
|     fetch_unpack https://github.com/zlib-ng/zlib-ng/archive/$ZLIB_NG_VERSION.tar.gz zlib-ng-$ZLIB_NG_VERSION.tar.gz | ||||
|     (cd zlib-ng-$ZLIB_NG_VERSION \ | ||||
|         && ./configure --prefix=$BUILD_PREFIX --zlib-compat \ | ||||
|         && make -j4 \ | ||||
|         && make install) | ||||
|         touch openjpeg-stamp | ||||
|     } | ||||
| fi | ||||
|     touch zlib-stamp | ||||
| } | ||||
| 
 | ||||
| function build_brotli { | ||||
|     local cmake=$(get_modern_cmake) | ||||
|     if [ -e brotli-stamp ]; then return; fi | ||||
|     local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz) | ||||
|     (cd $out_dir \ | ||||
|         && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ | ||||
|         && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \ | ||||
|         && make install) | ||||
|     if [[ "$MB_ML_LIBC" == "manylinux" ]]; then | ||||
|         cp /usr/local/lib64/libbrotli* /usr/local/lib | ||||
|         cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig | ||||
|     fi | ||||
|     touch brotli-stamp | ||||
| } | ||||
| 
 | ||||
| function build_harfbuzz { | ||||
|     if [ -e harfbuzz-stamp ]; then return; fi | ||||
|     python3 -m pip install meson ninja | ||||
| 
 | ||||
|     local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) | ||||
|     local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) | ||||
|     (cd $out_dir \ | ||||
|         && meson setup build --buildtype=release -Dfreetype=enabled -Dglib=disabled) | ||||
|         && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled) | ||||
|     (cd $out_dir/build \ | ||||
|         && meson install) | ||||
|     if [[ "$MB_ML_LIBC" == "manylinux" ]]; then | ||||
|         cp /usr/local/lib64/libharfbuzz* /usr/local/lib | ||||
|     fi | ||||
|     touch harfbuzz-stamp | ||||
| } | ||||
| 
 | ||||
| function build { | ||||
|     if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then | ||||
|         sudo chown -R runner /usr/local | ||||
|     fi | ||||
|     build_xz | ||||
|     if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then | ||||
|         yum remove -y zlib-devel | ||||
|     fi | ||||
|     build_new_zlib | ||||
|     build_zlib_ng | ||||
| 
 | ||||
|     build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto | ||||
|     if [ -n "$IS_MACOS" ]; then | ||||
|         build_simple xorgproto 2024.1 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.12 https://www.x.org/pub/individual/lib | ||||
|         build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist | ||||
|         if [[ "$CIBW_ARCHS" == "arm64" ]]; then | ||||
|             cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig | ||||
|         fi | ||||
|     else | ||||
|         sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc | ||||
|         sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc | ||||
|     fi | ||||
|     build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib | ||||
| 
 | ||||
|     build_libjpeg_turbo | ||||
|     if [ -n "$IS_MACOS" ]; then | ||||
|         # Custom tiff build to include jpeg; by default, configure won't include | ||||
|         # headers/libs in the custom macOS prefix. Explicitly disable webp, | ||||
|         # libdeflate and zstd, because on x86_64 macs, it will pick up the | ||||
|         # Homebrew versions of those libraries from /usr/local. | ||||
|         build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \ | ||||
|             --with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \ | ||||
|             --disable-webp --disable-libdeflate --disable-zstd | ||||
|     else | ||||
|         build_tiff | ||||
|     fi | ||||
| 
 | ||||
|     build_libpng | ||||
|     build_lcms2 | ||||
|     build_openjpeg | ||||
|     if [ -f /usr/local/lib64/libopenjp2.so ]; then | ||||
|         cp /usr/local/lib64/libopenjp2.so /usr/local/lib | ||||
|     fi | ||||
| 
 | ||||
|     ORIGINAL_CFLAGS=$CFLAGS | ||||
|     CFLAGS="$CFLAGS -O3 -DNDEBUG" | ||||
|  | @ -125,31 +155,47 @@ function build { | |||
|     build_harfbuzz | ||||
| } | ||||
| 
 | ||||
| # Perform all dependency builds in the build subfolder. | ||||
| mkdir -p $WORKDIR | ||||
| pushd $WORKDIR > /dev/null | ||||
| 
 | ||||
| # Any stuff that you need to do before you start building the wheels | ||||
| # Runs in the root directory of this repository. | ||||
| curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip | ||||
| untar pillow-depends-main.zip | ||||
| if [[ ! -d $WORKDIR/pillow-depends-main ]]; then | ||||
|   if [[ ! -f $PROJECTDIR/pillow-depends-main.zip ]]; then | ||||
|     echo "Download pillow dependency sources..." | ||||
|     curl -fSL -o $PROJECTDIR/pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip | ||||
|   fi | ||||
|   echo "Unpacking pillow dependency sources..." | ||||
|   untar $PROJECTDIR/pillow-depends-main.zip | ||||
| fi | ||||
| 
 | ||||
| if [[ -n "$IS_MACOS" ]]; then | ||||
|   # libtiff and libxcb cause a conflict with building libtiff and libxcb | ||||
|   # libxau and libxdmcp cause an issue on macOS < 11 | ||||
|   # remove cairo to fix building harfbuzz on arm64 | ||||
|   # remove lcms2 and libpng to fix building openjpeg on arm64 | ||||
|   # remove jpeg-turbo to avoid inclusion on arm64 | ||||
|   # remove webp and zstd to avoid inclusion on x86_64 | ||||
|   # curl from brew requires zstd, use system curl | ||||
|   brew remove --ignore-dependencies 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 | ||||
|     # Homebrew (or similar packaging environments) install can contain some of | ||||
|     # the libraries that we're going to build. However, they may be compiled | ||||
|     # with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use, | ||||
|     # and they may bring in other dependencies that we don't want. The same will | ||||
|     # be true of any other locations on the path. To avoid conflicts, strip the | ||||
|     # path down to the bare minimum (which, on macOS, won't include any | ||||
|     # development dependencies). | ||||
|     export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" | ||||
|     export CMAKE_PREFIX_PATH=$BUILD_PREFIX | ||||
| 
 | ||||
|   brew install pkg-config | ||||
|     # Ensure the basic structure of the build prefix directory exists. | ||||
|     mkdir -p "$BUILD_PREFIX/bin" | ||||
|     mkdir -p "$BUILD_PREFIX/lib" | ||||
| 
 | ||||
|     # Ensure pkg-config is available | ||||
|     build_pkg_config | ||||
|     # Ensure cmake is available | ||||
|     python3 -m pip install cmake | ||||
| fi | ||||
| 
 | ||||
| wrap_wheel_builder build | ||||
| 
 | ||||
| # Return to the project root to finish the build | ||||
| popd > /dev/null | ||||
| 
 | ||||
| # Append licenses | ||||
| for filename in wheels/dependency_licenses/*; do | ||||
|   echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE | ||||
|  |  | |||
							
								
								
									
										20
									
								
								.github/workflows/wheels-test.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/workflows/wheels-test.sh
									
									
									
									
										vendored
									
									
								
							|  | @ -1,12 +1,24 @@ | |||
| #!/bin/bash | ||||
| set -e | ||||
| 
 | ||||
| # Ensure fribidi is installed by the system. | ||||
| if [[ "$OSTYPE" == "darwin"* ]]; then | ||||
|     brew install fribidi | ||||
|     export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" | ||||
|     if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then | ||||
|         sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib | ||||
|     # If Homebrew is on the path during the build, it may leak into the wheels. | ||||
|     # However, we *do* need Homebrew to provide a copy of fribidi for | ||||
|     # testing purposes so that we can verify the fribidi shim works as expected. | ||||
|     if [[ "$(uname -m)" == "x86_64" ]]; then | ||||
|         HOMEBREW_PREFIX=/usr/local | ||||
|     else | ||||
|         HOMEBREW_PREFIX=/opt/homebrew | ||||
|     fi | ||||
|     $HOMEBREW_PREFIX/bin/brew install fribidi | ||||
| 
 | ||||
|     # Add the lib folder for fribidi so that the vendored library can be found. | ||||
|     # Don't use $HOMEWBREW_PREFIX/lib directly - use the lib folder where the | ||||
|     # installed copy of fribidi is cellared. This ensures we don't pick up the | ||||
|     # Homebrew version of any other library that we're dependent on (most notably, | ||||
|     # freetype). | ||||
|     export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_PREFIX/lib/libfribidi.dylib)) | ||||
| elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then | ||||
|     apk add curl fribidi | ||||
| else | ||||
|  |  | |||
							
								
								
									
										19
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -41,7 +41,7 @@ env: | |||
| 
 | ||||
| jobs: | ||||
|   build-1-QEMU-emulated-wheels: | ||||
|     if: github.event_name != 'schedule' && github.event_name != 'workflow_dispatch' | ||||
|     if: github.event_name != 'schedule' | ||||
|     name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|  | @ -61,6 +61,7 @@ jobs: | |||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|         with: | ||||
|           persist-credentials: false | ||||
|           submodules: true | ||||
| 
 | ||||
|       - uses: actions/setup-python@v5 | ||||
|  | @ -84,7 +85,7 @@ jobs: | |||
|           CIBW_ARCHS: "aarch64" | ||||
|           # Likewise, select only one Python version per job to speed this up. | ||||
|           CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" | ||||
|           CIBW_PRERELEASE_PYTHONS: True | ||||
|           CIBW_ENABLE: cpython-prerelease | ||||
|           # Extra options for manylinux. | ||||
|           CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} | ||||
|           CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} | ||||
|  | @ -132,6 +133,7 @@ jobs: | |||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|         with: | ||||
|           persist-credentials: false | ||||
|           submodules: true | ||||
| 
 | ||||
|       - uses: actions/setup-python@v5 | ||||
|  | @ -148,10 +150,10 @@ jobs: | |||
|         env: | ||||
|           CIBW_ARCHS: ${{ matrix.cibw_arch }} | ||||
|           CIBW_BUILD: ${{ matrix.build }} | ||||
|           CIBW_FREE_THREADED_SUPPORT: True | ||||
|           CIBW_ENABLE: cpython-prerelease cpython-freethreading | ||||
|           CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} | ||||
|           CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} | ||||
|           CIBW_PRERELEASE_PYTHONS: True | ||||
|           CIBW_SKIP: pp39-* | ||||
|           MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} | ||||
| 
 | ||||
|       - uses: actions/upload-artifact@v4 | ||||
|  | @ -172,10 +174,13 @@ jobs: | |||
|           - cibw_arch: ARM64 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|         with: | ||||
|           persist-credentials: false | ||||
| 
 | ||||
|       - name: Checkout extra test images | ||||
|         uses: actions/checkout@v4 | ||||
|         with: | ||||
|           persist-credentials: false | ||||
|           repository: python-pillow/test-images | ||||
|           path: Tests\test-images | ||||
| 
 | ||||
|  | @ -222,8 +227,8 @@ jobs: | |||
|           CIBW_ARCHS: ${{ matrix.cibw_arch }} | ||||
|           CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" | ||||
|           CIBW_CACHE_PATH: "C:\\cibw" | ||||
|           CIBW_FREE_THREADED_SUPPORT: True | ||||
|           CIBW_PRERELEASE_PYTHONS: True | ||||
|           CIBW_ENABLE: cpython-prerelease cpython-freethreading | ||||
|           CIBW_SKIP: pp39-* | ||||
|           CIBW_TEST_SKIP: "*-win_arm64" | ||||
|           CIBW_TEST_COMMAND: 'docker run --rm | ||||
|             -v {project}:C:\pillow | ||||
|  | @ -251,6 +256,8 @@ jobs: | |||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|     - uses: actions/checkout@v4 | ||||
|       with: | ||||
|         persist-credentials: false | ||||
| 
 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v5 | ||||
|  |  | |||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							|  | @ -19,6 +19,7 @@ lib64/ | |||
| parts/ | ||||
| sdist/ | ||||
| var/ | ||||
| wheelhouse/ | ||||
| *.egg-info/ | ||||
| .installed.cfg | ||||
| *.egg | ||||
|  | @ -90,5 +91,9 @@ Tests/images/msp | |||
| Tests/images/picins | ||||
| Tests/images/sunraster | ||||
| 
 | ||||
| # Test and dependency downloads | ||||
| pillow-depends-main.zip | ||||
| pillow-test-images.zip | ||||
| 
 | ||||
| # pyinstaller | ||||
| *.spec | ||||
|  |  | |||
|  | @ -1,17 +1,17 @@ | |||
| repos: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     rev: v0.6.3 | ||||
|     rev: v0.8.1 | ||||
|     hooks: | ||||
|       - id: ruff | ||||
|         args: [--exit-non-zero-on-fix] | ||||
| 
 | ||||
|   - repo: https://github.com/psf/black-pre-commit-mirror | ||||
|     rev: 24.8.0 | ||||
|     rev: 24.10.0 | ||||
|     hooks: | ||||
|       - id: black | ||||
| 
 | ||||
|   - repo: https://github.com/PyCQA/bandit | ||||
|     rev: 1.7.9 | ||||
|     rev: 1.8.0 | ||||
|     hooks: | ||||
|     - id: bandit | ||||
|       args: [--severity-level=high] | ||||
|  | @ -24,7 +24,7 @@ repos: | |||
|         exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) | ||||
| 
 | ||||
|   - repo: https://github.com/pre-commit/mirrors-clang-format | ||||
|     rev: v18.1.8 | ||||
|     rev: v19.1.4 | ||||
|     hooks: | ||||
|       - id: clang-format | ||||
|         types: [c] | ||||
|  | @ -36,7 +36,7 @@ repos: | |||
|       - id: rst-backticks | ||||
| 
 | ||||
|   - repo: https://github.com/pre-commit/pre-commit-hooks | ||||
|     rev: v4.6.0 | ||||
|     rev: v5.0.0 | ||||
|     hooks: | ||||
|       - id: check-executables-have-shebangs | ||||
|       - id: check-shebang-scripts-are-executable | ||||
|  | @ -50,29 +50,30 @@ repos: | |||
|         exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ | ||||
| 
 | ||||
|   - repo: https://github.com/python-jsonschema/check-jsonschema | ||||
|     rev: 0.29.2 | ||||
|     rev: 0.30.0 | ||||
|     hooks: | ||||
|       - id: check-github-workflows | ||||
|       - id: check-readthedocs | ||||
|       - id: check-renovate | ||||
| 
 | ||||
|   - repo: https://github.com/sphinx-contrib/sphinx-lint | ||||
|     rev: v0.9.1 | ||||
|     rev: v1.0.0 | ||||
|     hooks: | ||||
|       - id: sphinx-lint | ||||
| 
 | ||||
|   - repo: https://github.com/tox-dev/pyproject-fmt | ||||
|     rev: 2.2.1 | ||||
|     rev: v2.5.0 | ||||
|     hooks: | ||||
|       - id: pyproject-fmt | ||||
| 
 | ||||
|   - repo: https://github.com/abravalheri/validate-pyproject | ||||
|     rev: v0.19 | ||||
|     rev: v0.23 | ||||
|     hooks: | ||||
|       - id: validate-pyproject | ||||
|         additional_dependencies: [trove-classifiers>=2024.10.12] | ||||
| 
 | ||||
|   - repo: https://github.com/tox-dev/tox-ini-fmt | ||||
|     rev: 1.3.1 | ||||
|     rev: 1.4.1 | ||||
|     hooks: | ||||
|       - id: tox-ini-fmt | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										27
									
								
								CHANGES.rst
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								CHANGES.rst
									
									
									
									
									
								
							|  | @ -2,9 +2,34 @@ | |||
| Changelog (Pillow) | ||||
| ================== | ||||
| 
 | ||||
| 11.0.0 (unreleased) | ||||
| 11.1.0 and newer | ||||
| ---------------- | ||||
| 
 | ||||
| See GitHub Releases: | ||||
| 
 | ||||
| - https://github.com/python-pillow/Pillow/releases | ||||
| 
 | ||||
| 11.0.0 (2024-10-15) | ||||
| ------------------- | ||||
| 
 | ||||
| - Update licence to MIT-CMU #8460 | ||||
|   [hugovk] | ||||
| 
 | ||||
| - Conditionally define ImageCms type hint to avoid requiring core #8197 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Support writing LONG8 offsets in AppendingTiffWriter #8417 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Use ImageFile.MAXBLOCK when saving TIFF images #8461 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Do not close provided file handles with libtiff when saving #8458 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Support ImageFilter.BuiltinFilter for I;16* images #8438 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Use ImagingCore.ptr instead of ImagingCore.id #8341 | ||||
|   [homm, radarhere, hugovk] | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										4
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								LICENSE
									
									
									
									
									
								
							|  | @ -5,9 +5,9 @@ The Python Imaging Library (PIL) is | |||
| 
 | ||||
| Pillow is the friendly PIL fork. It is | ||||
| 
 | ||||
|     Copyright © 2010-2024 by Jeffrey A. Clark and contributors | ||||
|     Copyright © 2010 by Jeffrey A. Clark and contributors | ||||
| 
 | ||||
| Like PIL, Pillow is licensed under the open source HPND License: | ||||
| Like PIL, Pillow is licensed under the open source MIT-CMU License: | ||||
| 
 | ||||
| By obtaining, using, and/or copying this software and/or its associated | ||||
| documentation, you agree that you have read, understood, and will comply | ||||
|  |  | |||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							|  | @ -17,12 +17,10 @@ coverage: | |||
| .PHONY: doc | ||||
| .PHONY: html | ||||
| doc html: | ||||
| 	python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . | ||||
| 	$(MAKE) -C docs html | ||||
| 
 | ||||
| .PHONY: htmlview | ||||
| htmlview: | ||||
| 	python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . | ||||
| 	$(MAKE) -C docs htmlview | ||||
| 
 | ||||
| .PHONY: doccheck | ||||
|  |  | |||
|  | @ -107,7 +107,7 @@ The core image library is designed for fast access to data stored in a few basic | |||
|   - [Issues](https://github.com/python-pillow/Pillow/issues) | ||||
|   - [Pull requests](https://github.com/python-pillow/Pillow/pulls) | ||||
| - [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html) | ||||
| - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) | ||||
| - [Changelog](https://github.com/python-pillow/Pillow/releases) | ||||
|   - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) | ||||
| 
 | ||||
| ## Report a Vulnerability | ||||
|  |  | |||
|  | @ -12,7 +12,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. | |||
| * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. | ||||
| * [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them. | ||||
| * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` | ||||
| * [ ] Update `CHANGES.rst`. | ||||
| * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. | ||||
| * [ ] Create branch and tag for release e.g.: | ||||
|   ```bash | ||||
|  | @ -34,7 +33,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. | |||
| Released as needed for security, installation or critical bug fixes. | ||||
| 
 | ||||
| * [ ] Make necessary changes in `main` branch. | ||||
| * [ ] Update `CHANGES.rst`. | ||||
| * [ ] Check out release branch e.g.: | ||||
|   ```bash | ||||
|   git checkout -t remotes/origin/5.2.x | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ def test_wheel_features() -> None: | |||
|         "fribidi", | ||||
|         "harfbuzz", | ||||
|         "libjpeg_turbo", | ||||
|         "zlib_ng", | ||||
|         "xcb", | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 486 B After Width: | Height: | Size: 533 B | 
|  | @ -22,6 +22,8 @@ def test_bad() -> None: | |||
|     for f in get_files("b"): | ||||
|         # Assert that there is no unclosed file warning | ||||
|         with warnings.catch_warnings(): | ||||
|             warnings.simplefilter("error") | ||||
| 
 | ||||
|             try: | ||||
|                 with Image.open(f) as im: | ||||
|                     im.load() | ||||
|  |  | |||
|  | @ -388,10 +388,12 @@ class TestColorLut3DFilter: | |||
| 
 | ||||
|         table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16) | ||||
|         lut = ImageFilter.Color3DLUT((5, 6, 7), table) | ||||
|         assert isinstance(lut.table, numpy.ndarray) | ||||
|         assert lut.table.shape == (table.size,) | ||||
| 
 | ||||
|         table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) | ||||
|         lut = ImageFilter.Color3DLUT((5, 6, 7), table) | ||||
|         assert isinstance(lut.table, numpy.ndarray) | ||||
|         assert lut.table.shape == (table.size,) | ||||
| 
 | ||||
|         # Check application | ||||
|  |  | |||
|  | @ -36,9 +36,10 @@ def test_version() -> None: | |||
|         else: | ||||
|             assert function(name) == version | ||||
|             if name != "PIL": | ||||
|                 if name == "zlib" and version is not None: | ||||
|                 if version is not None: | ||||
|                     if name == "zlib" and features.check_feature("zlib_ng"): | ||||
|                         version = re.sub(".zlib-ng$", "", version) | ||||
|                 elif name == "libtiff" and version is not None: | ||||
|                     elif name == "libtiff": | ||||
|                         version = re.sub("t$", "", version) | ||||
|                 assert version is None or re.search(r"\d+(\.\d+)*$", version) | ||||
| 
 | ||||
|  | @ -56,17 +57,17 @@ def test_version() -> None: | |||
| 
 | ||||
| def test_webp_transparency() -> None: | ||||
|     with pytest.warns(DeprecationWarning): | ||||
|         assert features.check("transp_webp") == features.check_module("webp") | ||||
|         assert (features.check("transp_webp") or False) == features.check_module("webp") | ||||
| 
 | ||||
| 
 | ||||
| def test_webp_mux() -> None: | ||||
|     with pytest.warns(DeprecationWarning): | ||||
|         assert features.check("webp_mux") == features.check_module("webp") | ||||
|         assert (features.check("webp_mux") or False) == features.check_module("webp") | ||||
| 
 | ||||
| 
 | ||||
| def test_webp_anim() -> None: | ||||
|     with pytest.warns(DeprecationWarning): | ||||
|         assert features.check("webp_anim") == features.check_module("webp") | ||||
|         assert (features.check("webp_anim") or False) == features.check_module("webp") | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("libjpeg_turbo") | ||||
|  |  | |||
|  | @ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None: | |||
|         im.save(temp_file) | ||||
|         assert handler.saved | ||||
| 
 | ||||
|     BufrStubImagePlugin._handler = None | ||||
|     BufrStubImagePlugin.register_handler(None) | ||||
|  |  | |||
|  | @ -4,8 +4,6 @@ import pytest | |||
| 
 | ||||
| from PIL import ContainerIO, Image | ||||
| 
 | ||||
| from .helper import hopper | ||||
| 
 | ||||
| TEST_FILE = "Tests/images/dummy.container" | ||||
| 
 | ||||
| 
 | ||||
|  | @ -15,15 +13,15 @@ def test_sanity() -> None: | |||
| 
 | ||||
| 
 | ||||
| def test_isatty() -> None: | ||||
|     with hopper() as im: | ||||
|         container = ContainerIO.ContainerIO(im, 0, 0) | ||||
|     with open(TEST_FILE, "rb") as fh: | ||||
|         container = ContainerIO.ContainerIO(fh, 0, 0) | ||||
| 
 | ||||
|     assert container.isatty() is False | ||||
| 
 | ||||
| 
 | ||||
| def test_seekable() -> None: | ||||
|     with hopper() as im: | ||||
|         container = ContainerIO.ContainerIO(im, 0, 0) | ||||
|     with open(TEST_FILE, "rb") as fh: | ||||
|         container = ContainerIO.ContainerIO(fh, 0, 0) | ||||
| 
 | ||||
|     assert container.seekable() is True | ||||
| 
 | ||||
|  |  | |||
|  | @ -36,6 +36,8 @@ def test_unclosed_file() -> None: | |||
| 
 | ||||
| def test_closed_file() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         im = Image.open(TEST_FILE) | ||||
|         im.load() | ||||
|         im.close() | ||||
|  | @ -43,6 +45,8 @@ def test_closed_file() -> None: | |||
| 
 | ||||
| def test_context_manager() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         with Image.open(TEST_FILE) as im: | ||||
|             im.load() | ||||
| 
 | ||||
|  |  | |||
|  | @ -65,6 +65,8 @@ def test_unclosed_file() -> None: | |||
| 
 | ||||
| def test_closed_file() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         im = Image.open(static_test_file) | ||||
|         im.load() | ||||
|         im.close() | ||||
|  | @ -81,6 +83,8 @@ def test_seek_after_close() -> None: | |||
| 
 | ||||
| def test_context_manager() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         with Image.open(static_test_file) as im: | ||||
|             im.load() | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import warnings | |||
| from collections.abc import Generator | ||||
| from io import BytesIO | ||||
| from pathlib import Path | ||||
| from typing import Any | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
|  | @ -46,6 +47,8 @@ def test_unclosed_file() -> None: | |||
| 
 | ||||
| def test_closed_file() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         im = Image.open(TEST_GIF) | ||||
|         im.load() | ||||
|         im.close() | ||||
|  | @ -67,6 +70,8 @@ def test_seek_after_close() -> None: | |||
| 
 | ||||
| def test_context_manager() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         with Image.open(TEST_GIF) as im: | ||||
|             im.load() | ||||
| 
 | ||||
|  | @ -1431,7 +1436,8 @@ def test_saving_rgba(tmp_path: Path) -> None: | |||
|         assert reloaded_rgba.load()[0, 0][3] == 0 | ||||
| 
 | ||||
| 
 | ||||
| def test_optimizing_p_rgba(tmp_path: Path) -> None: | ||||
| @pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) | ||||
| def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: | ||||
|     out = str(tmp_path / "temp.gif") | ||||
| 
 | ||||
|     im1 = Image.new("P", (100, 100)) | ||||
|  | @ -1443,7 +1449,7 @@ def test_optimizing_p_rgba(tmp_path: Path) -> None: | |||
|     im2 = Image.new("P", (100, 100)) | ||||
|     im2.putpalette(data, "RGBA") | ||||
| 
 | ||||
|     im1.save(out, save_all=True, append_images=[im2]) | ||||
|     im1.save(out, save_all=True, append_images=[im2], **params) | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert reloaded.n_frames == 2 | ||||
|  |  | |||
|  | @ -83,4 +83,4 @@ def test_handler(tmp_path: Path) -> None: | |||
|         im.save(temp_file) | ||||
|         assert handler.saved | ||||
| 
 | ||||
|     GribStubImagePlugin._handler = None | ||||
|     GribStubImagePlugin.register_handler(None) | ||||
|  |  | |||
|  | @ -85,4 +85,4 @@ def test_handler(tmp_path: Path) -> None: | |||
|         im.save(temp_file) | ||||
|         assert handler.saved | ||||
| 
 | ||||
|     Hdf5StubImagePlugin._handler = None | ||||
|     Hdf5StubImagePlugin.register_handler(None) | ||||
|  |  | |||
|  | @ -21,6 +21,8 @@ def test_sanity() -> None: | |||
|     with Image.open(TEST_FILE) as im: | ||||
|         # Assert that there is no unclosed file warning | ||||
|         with warnings.catch_warnings(): | ||||
|             warnings.simplefilter("error") | ||||
| 
 | ||||
|             im.load() | ||||
| 
 | ||||
|         assert im.mode == "RGBA" | ||||
|  |  | |||
|  | @ -41,6 +41,8 @@ def test_unclosed_file() -> None: | |||
| 
 | ||||
| def test_closed_file() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         im = Image.open(TEST_IM) | ||||
|         im.load() | ||||
|         im.close() | ||||
|  | @ -48,6 +50,8 @@ def test_closed_file() -> None: | |||
| 
 | ||||
| def test_context_manager() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         with Image.open(TEST_IM) as im: | ||||
|             im.load() | ||||
| 
 | ||||
|  |  | |||
|  | @ -541,12 +541,12 @@ class TestFileJpeg: | |||
|     @mark_if_feature_version( | ||||
|         pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" | ||||
|     ) | ||||
|     def test_qtables(self, tmp_path: Path) -> None: | ||||
|     def test_qtables(self) -> None: | ||||
|         def _n_qtables_helper(n: int, test_file: str) -> None: | ||||
|             b = BytesIO() | ||||
|             with Image.open(test_file) as im: | ||||
|                 f = str(tmp_path / "temp.jpg") | ||||
|                 im.save(f, qtables=[[n] * 64] * n) | ||||
|             with Image.open(f) as im: | ||||
|                 im.save(b, "JPEG", qtables=[[n] * 64] * n) | ||||
|             with Image.open(b) as im: | ||||
|                 assert len(im.quantization) == n | ||||
|                 reloaded = self.roundtrip(im, qtables="keep") | ||||
|                 assert im.quantization == reloaded.quantization | ||||
|  | @ -850,6 +850,8 @@ class TestFileJpeg: | |||
| 
 | ||||
|             out = str(tmp_path / "out.jpg") | ||||
|             with warnings.catch_warnings(): | ||||
|                 warnings.simplefilter("error") | ||||
| 
 | ||||
|                 im.save(out, exif=exif) | ||||
| 
 | ||||
|         with Image.open(out) as reloaded: | ||||
|  | @ -998,8 +1000,13 @@ class TestFileJpeg: | |||
|         with Image.open(f) as reloaded: | ||||
|             assert reloaded.info["xmp"] == b"XMP test" | ||||
| 
 | ||||
|         im.info["xmp"] = b"1" * 65504 | ||||
|         im.save(f) | ||||
|             # Check that XMP is not saved from image info | ||||
|             reloaded.save(f) | ||||
| 
 | ||||
|         with Image.open(f) as reloaded: | ||||
|             assert "xmp" not in reloaded.info | ||||
| 
 | ||||
|         im.save(f, xmp=b"1" * 65504) | ||||
|         with Image.open(f) as reloaded: | ||||
|             assert reloaded.info["xmp"] == b"1" * 65504 | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ from __future__ import annotations | |||
| 
 | ||||
| import os | ||||
| import re | ||||
| from collections.abc import Generator | ||||
| from io import BytesIO | ||||
| from pathlib import Path | ||||
| from typing import Any | ||||
|  | @ -29,8 +30,16 @@ EXTRA_DIR = "Tests/images/jpeg2000" | |||
| 
 | ||||
| pytestmark = skip_unless_feature("jpg_2000") | ||||
| 
 | ||||
| test_card = Image.open("Tests/images/test-card.png") | ||||
| test_card.load() | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def card() -> Generator[ImageFile.ImageFile, None, None]: | ||||
|     with Image.open("Tests/images/test-card.png") as im: | ||||
|         im.load() | ||||
|     try: | ||||
|         yield im | ||||
|     finally: | ||||
|         im.close() | ||||
| 
 | ||||
| 
 | ||||
| # OpenJPEG 2.0.0 outputs this debugging message sometimes; we should | ||||
| # ignore it---it doesn't represent a test failure. | ||||
|  | @ -74,76 +83,76 @@ def test_invalid_file() -> None: | |||
|         Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) | ||||
| 
 | ||||
| 
 | ||||
| def test_bytesio() -> None: | ||||
| def test_bytesio(card: ImageFile.ImageFile) -> None: | ||||
|     with open("Tests/images/test-card-lossless.jp2", "rb") as f: | ||||
|         data = BytesIO(f.read()) | ||||
|     with Image.open(data) as im: | ||||
|         im.load() | ||||
|         assert_image_similar(im, test_card, 1.0e-3) | ||||
|         assert_image_similar(im, card, 1.0e-3) | ||||
| 
 | ||||
| 
 | ||||
| # These two test pre-written JPEG 2000 files that were not written with | ||||
| # PIL (they were made using Adobe Photoshop) | ||||
| 
 | ||||
| 
 | ||||
| def test_lossless(tmp_path: Path) -> None: | ||||
| def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None: | ||||
|     with Image.open("Tests/images/test-card-lossless.jp2") as im: | ||||
|         im.load() | ||||
|         outfile = str(tmp_path / "temp_test-card.png") | ||||
|         im.save(outfile) | ||||
|     assert_image_similar(im, test_card, 1.0e-3) | ||||
|     assert_image_similar(im, card, 1.0e-3) | ||||
| 
 | ||||
| 
 | ||||
| def test_lossy_tiled() -> None: | ||||
|     assert_image_similar_tofile( | ||||
|         test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 | ||||
|     ) | ||||
| def test_lossy_tiled(card: ImageFile.ImageFile) -> None: | ||||
|     assert_image_similar_tofile(card, "Tests/images/test-card-lossy-tiled.jp2", 2.0) | ||||
| 
 | ||||
| 
 | ||||
| def test_lossless_rt() -> None: | ||||
|     im = roundtrip(test_card) | ||||
|     assert_image_equal(im, test_card) | ||||
| def test_lossless_rt(card: ImageFile.ImageFile) -> None: | ||||
|     im = roundtrip(card) | ||||
|     assert_image_equal(im, card) | ||||
| 
 | ||||
| 
 | ||||
| def test_lossy_rt() -> None: | ||||
|     im = roundtrip(test_card, quality_layers=[20]) | ||||
|     assert_image_similar(im, test_card, 2.0) | ||||
| def test_lossy_rt(card: ImageFile.ImageFile) -> None: | ||||
|     im = roundtrip(card, quality_layers=[20]) | ||||
|     assert_image_similar(im, card, 2.0) | ||||
| 
 | ||||
| 
 | ||||
| def test_tiled_rt() -> None: | ||||
|     im = roundtrip(test_card, tile_size=(128, 128)) | ||||
|     assert_image_equal(im, test_card) | ||||
| def test_tiled_rt(card: ImageFile.ImageFile) -> None: | ||||
|     im = roundtrip(card, tile_size=(128, 128)) | ||||
|     assert_image_equal(im, card) | ||||
| 
 | ||||
| 
 | ||||
| def test_tiled_offset_rt() -> None: | ||||
|     im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) | ||||
|     assert_image_equal(im, test_card) | ||||
| def test_tiled_offset_rt(card: ImageFile.ImageFile) -> None: | ||||
|     im = roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) | ||||
|     assert_image_equal(im, card) | ||||
| 
 | ||||
| 
 | ||||
| def test_tiled_offset_too_small() -> None: | ||||
| def test_tiled_offset_too_small(card: ImageFile.ImageFile) -> None: | ||||
|     with pytest.raises(ValueError): | ||||
|         roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) | ||||
|         roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) | ||||
| 
 | ||||
| 
 | ||||
| def test_irreversible_rt() -> None: | ||||
|     im = roundtrip(test_card, irreversible=True, quality_layers=[20]) | ||||
|     assert_image_similar(im, test_card, 2.0) | ||||
| def test_irreversible_rt(card: ImageFile.ImageFile) -> None: | ||||
|     im = roundtrip(card, irreversible=True, quality_layers=[20]) | ||||
|     assert_image_similar(im, card, 2.0) | ||||
| 
 | ||||
| 
 | ||||
| def test_prog_qual_rt() -> None: | ||||
|     im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") | ||||
|     assert_image_similar(im, test_card, 2.0) | ||||
| def test_prog_qual_rt(card: ImageFile.ImageFile) -> None: | ||||
|     im = roundtrip(card, quality_layers=[60, 40, 20], progression="LRCP") | ||||
|     assert_image_similar(im, card, 2.0) | ||||
| 
 | ||||
| 
 | ||||
| def test_prog_res_rt() -> None: | ||||
|     im = roundtrip(test_card, num_resolutions=8, progression="RLCP") | ||||
|     assert_image_equal(im, test_card) | ||||
| def test_prog_res_rt(card: ImageFile.ImageFile) -> None: | ||||
|     im = roundtrip(card, num_resolutions=8, progression="RLCP") | ||||
|     assert_image_equal(im, card) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("num_resolutions", range(2, 6)) | ||||
| def test_default_num_resolutions(num_resolutions: int) -> None: | ||||
| def test_default_num_resolutions( | ||||
|     card: ImageFile.ImageFile, num_resolutions: int | ||||
| ) -> None: | ||||
|     d = 1 << (num_resolutions - 1) | ||||
|     im = test_card.resize((d - 1, d - 1)) | ||||
|     im = card.resize((d - 1, d - 1)) | ||||
|     with pytest.raises(OSError): | ||||
|         roundtrip(im, num_resolutions=num_resolutions) | ||||
|     reloaded = roundtrip(im) | ||||
|  | @ -205,31 +214,31 @@ def test_header_errors() -> None: | |||
|             pass | ||||
| 
 | ||||
| 
 | ||||
| def test_layers_type(tmp_path: Path) -> None: | ||||
| def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None: | ||||
|     outfile = str(tmp_path / "temp_layers.jp2") | ||||
|     for quality_layers in [[100, 50, 10], (100, 50, 10), None]: | ||||
|         test_card.save(outfile, quality_layers=quality_layers) | ||||
|         card.save(outfile, quality_layers=quality_layers) | ||||
| 
 | ||||
|     for quality_layers_str in ["quality_layers", ("100", "50", "10")]: | ||||
|         with pytest.raises(ValueError): | ||||
|             test_card.save(outfile, quality_layers=quality_layers_str) | ||||
|             card.save(outfile, quality_layers=quality_layers_str) | ||||
| 
 | ||||
| 
 | ||||
| def test_layers() -> None: | ||||
| def test_layers(card: ImageFile.ImageFile) -> None: | ||||
|     out = BytesIO() | ||||
|     test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") | ||||
|     card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") | ||||
|     out.seek(0) | ||||
| 
 | ||||
|     with Image.open(out) as im: | ||||
|         im.layers = 1 | ||||
|         im.load() | ||||
|         assert_image_similar(im, test_card, 13) | ||||
|         assert_image_similar(im, card, 13) | ||||
| 
 | ||||
|     out.seek(0) | ||||
|     with Image.open(out) as im: | ||||
|         im.layers = 3 | ||||
|         im.load() | ||||
|         assert_image_similar(im, test_card, 0.4) | ||||
|         assert_image_similar(im, card, 0.4) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|  | @ -245,24 +254,30 @@ def test_layers() -> None: | |||
|         (None, {"no_jp2": False}, 4, b"jP"), | ||||
|     ), | ||||
| ) | ||||
| def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: | ||||
| def test_no_jp2( | ||||
|     card: ImageFile.ImageFile, | ||||
|     name: str, | ||||
|     args: dict[str, bool], | ||||
|     offset: int, | ||||
|     data: bytes, | ||||
| ) -> None: | ||||
|     out = BytesIO() | ||||
|     if name: | ||||
|         out.name = name | ||||
|     test_card.save(out, "JPEG2000", **args) | ||||
|     card.save(out, "JPEG2000", **args) | ||||
|     out.seek(offset) | ||||
|     assert out.read(2) == data | ||||
| 
 | ||||
| 
 | ||||
| def test_mct() -> None: | ||||
| def test_mct(card: ImageFile.ImageFile) -> None: | ||||
|     # Three component | ||||
|     for val in (0, 1): | ||||
|         out = BytesIO() | ||||
|         test_card.save(out, "JPEG2000", mct=val, no_jp2=True) | ||||
|         card.save(out, "JPEG2000", mct=val, no_jp2=True) | ||||
| 
 | ||||
|         assert out.getvalue()[59] == val | ||||
|         with Image.open(out) as im: | ||||
|             assert_image_similar(im, test_card, 1.0e-3) | ||||
|             assert_image_similar(im, card, 1.0e-3) | ||||
| 
 | ||||
|     # Single component should have MCT disabled | ||||
|     for val in (0, 1): | ||||
|  | @ -310,6 +325,18 @@ def test_cmyk() -> None: | |||
|         assert im.getpixel((0, 0)) == (185, 134, 0, 0) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif( | ||||
|     not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" | ||||
| ) | ||||
| @skip_unless_feature_version("jpg_2000", "2.5.3") | ||||
| def test_cmyk_save() -> None: | ||||
|     with Image.open(f"{EXTRA_DIR}/issue205.jp2") as jp2: | ||||
|         assert jp2.mode == "CMYK" | ||||
| 
 | ||||
|         im = roundtrip(jp2) | ||||
|         assert_image_equal(im, jp2) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("ext", (".j2k", ".jp2")) | ||||
| def test_16bit_monochrome_has_correct_mode(ext: str) -> None: | ||||
|     with Image.open("Tests/images/16bit.cropped" + ext) as im: | ||||
|  | @ -409,7 +436,8 @@ def test_pclr() -> None: | |||
| 
 | ||||
| 
 | ||||
| def test_comment() -> None: | ||||
|     with Image.open("Tests/images/comment.jp2") as im: | ||||
|     for path in ("Tests/images/9bit.j2k", "Tests/images/comment.jp2"): | ||||
|         with Image.open(path) as im: | ||||
|             assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" | ||||
| 
 | ||||
|     # Test an image that is truncated partway through a codestream | ||||
|  | @ -419,22 +447,22 @@ def test_comment() -> None: | |||
|             pass | ||||
| 
 | ||||
| 
 | ||||
| def test_save_comment() -> None: | ||||
| def test_save_comment(card: ImageFile.ImageFile) -> None: | ||||
|     for comment in ("Created by Pillow", b"Created by Pillow"): | ||||
|         out = BytesIO() | ||||
|         test_card.save(out, "JPEG2000", comment=comment) | ||||
|         card.save(out, "JPEG2000", comment=comment) | ||||
| 
 | ||||
|         with Image.open(out) as im: | ||||
|             assert im.info["comment"] == b"Created by Pillow" | ||||
| 
 | ||||
|     out = BytesIO() | ||||
|     long_comment = b" " * 65531 | ||||
|     test_card.save(out, "JPEG2000", comment=long_comment) | ||||
|     card.save(out, "JPEG2000", comment=long_comment) | ||||
|     with Image.open(out) as im: | ||||
|         assert im.info["comment"] == long_comment | ||||
| 
 | ||||
|     with pytest.raises(ValueError): | ||||
|         test_card.save(out, "JPEG2000", comment=long_comment + b" ") | ||||
|         card.save(out, "JPEG2000", comment=long_comment + b" ") | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|  | @ -457,10 +485,10 @@ def test_crashes(test_file: str) -> None: | |||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature_version("jpg_2000", "2.4.0") | ||||
| def test_plt_marker() -> None: | ||||
| def test_plt_marker(card: ImageFile.ImageFile) -> None: | ||||
|     # Search the start of the codesteam for PLT | ||||
|     out = BytesIO() | ||||
|     test_card.save(out, "JPEG2000", no_jp2=True, plt=True) | ||||
|     card.save(out, "JPEG2000", no_jp2=True, plt=True) | ||||
|     out.seek(0) | ||||
|     while True: | ||||
|         marker = out.read(2) | ||||
|  |  | |||
|  | @ -1098,6 +1098,25 @@ class TestFileLibTiff(LibTiffTestCase): | |||
| 
 | ||||
|                     assert_image_similar(base_im, im, 0.7) | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         "test_file", | ||||
|         [ | ||||
|             "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif", | ||||
|             "Tests/images/old-style-jpeg-compression.tif", | ||||
|         ], | ||||
|     ) | ||||
|     def test_buffering(self, test_file: str) -> None: | ||||
|         # load exif first | ||||
|         with Image.open(open(test_file, "rb", buffering=1048576)) as im: | ||||
|             exif = dict(im.getexif()) | ||||
| 
 | ||||
|         # load image before exif | ||||
|         with Image.open(open(test_file, "rb", buffering=1048576)) as im2: | ||||
|             im2.load() | ||||
|             exif_after_load = dict(im2.getexif()) | ||||
| 
 | ||||
|         assert exif == exif_after_load | ||||
| 
 | ||||
|     @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") | ||||
|     def test_sampleformat_not_corrupted(self) -> None: | ||||
|         # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted | ||||
|  |  | |||
|  | @ -48,6 +48,8 @@ def test_unclosed_file() -> None: | |||
| 
 | ||||
| def test_closed_file() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         im = Image.open(test_files[0]) | ||||
|         im.load() | ||||
|         im.close() | ||||
|  | @ -63,6 +65,8 @@ def test_seek_after_close() -> None: | |||
| 
 | ||||
| def test_context_manager() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         with Image.open(test_files[0]) as im: | ||||
|             im.load() | ||||
| 
 | ||||
|  | @ -293,3 +297,15 @@ def test_save_all() -> None: | |||
|     # Test that a single frame image will not be saved as an MPO | ||||
|     jpg = roundtrip(im, save_all=True) | ||||
|     assert "mp" not in jpg.info | ||||
| 
 | ||||
| 
 | ||||
| def test_save_xmp() -> None: | ||||
|     im = Image.new("RGB", (1, 1)) | ||||
|     im2 = Image.new("RGB", (1, 1), "#f00") | ||||
|     im2.encoderinfo = {"xmp": b"Second frame"} | ||||
|     im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2]) | ||||
| 
 | ||||
|     assert im_reloaded.info["xmp"] == b"First frame" | ||||
| 
 | ||||
|     im_reloaded.seek(1) | ||||
|     assert im_reloaded.info["xmp"] == b"Second frame" | ||||
|  |  | |||
|  | @ -338,6 +338,8 @@ class TestFilePng: | |||
|         with Image.open(TEST_PNG_FILE) as im: | ||||
|             # Assert that there is no unclosed file warning | ||||
|             with warnings.catch_warnings(): | ||||
|                 warnings.simplefilter("error") | ||||
| 
 | ||||
|                 im.verify() | ||||
| 
 | ||||
|         with Image.open(TEST_PNG_FILE) as im: | ||||
|  | @ -770,22 +772,18 @@ class TestFilePng: | |||
|                 im.seek(1) | ||||
| 
 | ||||
|     @pytest.mark.parametrize("buffer", (True, False)) | ||||
|     def test_save_stdout(self, buffer: bool) -> None: | ||||
|         old_stdout = sys.stdout | ||||
|     def test_save_stdout(self, buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None: | ||||
| 
 | ||||
|         class MyStdOut: | ||||
|             buffer = BytesIO() | ||||
| 
 | ||||
|         mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() | ||||
| 
 | ||||
|         sys.stdout = mystdout | ||||
|         monkeypatch.setattr(sys, "stdout", mystdout) | ||||
| 
 | ||||
|         with Image.open(TEST_PNG_FILE) as im: | ||||
|             im.save(sys.stdout, "PNG") | ||||
| 
 | ||||
|         # Reset stdout | ||||
|         sys.stdout = old_stdout | ||||
| 
 | ||||
|         if isinstance(mystdout, MyStdOut): | ||||
|             mystdout = mystdout.buffer | ||||
|         with Image.open(mystdout) as reloaded: | ||||
|  |  | |||
|  | @ -367,22 +367,18 @@ def test_mimetypes(tmp_path: Path) -> None: | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("buffer", (True, False)) | ||||
| def test_save_stdout(buffer: bool) -> None: | ||||
|     old_stdout = sys.stdout | ||||
| def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None: | ||||
| 
 | ||||
|     class MyStdOut: | ||||
|         buffer = BytesIO() | ||||
| 
 | ||||
|     mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() | ||||
| 
 | ||||
|     sys.stdout = mystdout | ||||
|     monkeypatch.setattr(sys, "stdout", mystdout) | ||||
| 
 | ||||
|     with Image.open(TEST_FILE) as im: | ||||
|         im.save(sys.stdout, "PPM") | ||||
| 
 | ||||
|     # Reset stdout | ||||
|     sys.stdout = old_stdout | ||||
| 
 | ||||
|     if isinstance(mystdout, MyStdOut): | ||||
|         mystdout = mystdout.buffer | ||||
|     with Image.open(mystdout) as reloaded: | ||||
|  |  | |||
|  | @ -35,6 +35,8 @@ def test_unclosed_file() -> None: | |||
| 
 | ||||
| def test_closed_file() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         im = Image.open(test_file) | ||||
|         im.load() | ||||
|         im.close() | ||||
|  | @ -42,6 +44,8 @@ def test_closed_file() -> None: | |||
| 
 | ||||
| def test_context_manager() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         with Image.open(test_file) as im: | ||||
|             im.load() | ||||
| 
 | ||||
|  |  | |||
|  | @ -34,6 +34,8 @@ def test_unclosed_file() -> None: | |||
| 
 | ||||
| def test_closed_file() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         im = Image.open(TEST_FILE) | ||||
|         im.load() | ||||
|         im.close() | ||||
|  | @ -41,6 +43,8 @@ def test_closed_file() -> None: | |||
| 
 | ||||
| def test_context_manager() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         with Image.open(TEST_FILE) as im: | ||||
|             im.load() | ||||
| 
 | ||||
|  |  | |||
|  | @ -37,11 +37,15 @@ def test_unclosed_file() -> None: | |||
| 
 | ||||
| def test_close() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") | ||||
|         tar.close() | ||||
| 
 | ||||
| 
 | ||||
| def test_contextmanager() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): | ||||
|             pass | ||||
|  |  | |||
|  | @ -72,6 +72,8 @@ class TestFileTiff: | |||
| 
 | ||||
|     def test_closed_file(self) -> None: | ||||
|         with warnings.catch_warnings(): | ||||
|             warnings.simplefilter("error") | ||||
| 
 | ||||
|             im = Image.open("Tests/images/multipage.tiff") | ||||
|             im.load() | ||||
|             im.close() | ||||
|  | @ -88,6 +90,8 @@ class TestFileTiff: | |||
| 
 | ||||
|     def test_context_manager(self) -> None: | ||||
|         with warnings.catch_warnings(): | ||||
|             warnings.simplefilter("error") | ||||
| 
 | ||||
|             with Image.open("Tests/images/multipage.tiff") as im: | ||||
|                 im.load() | ||||
| 
 | ||||
|  | @ -108,10 +112,6 @@ class TestFileTiff: | |||
|             assert_image_equal_tofile(im, "Tests/images/hopper.tif") | ||||
| 
 | ||||
|         with Image.open("Tests/images/hopper_bigtiff.tif") as im: | ||||
|             # The data type of this file's StripOffsets tag is LONG8, | ||||
|             # which is not yet supported for offset data when saving multiple frames. | ||||
|             del im.tag_v2[273] | ||||
| 
 | ||||
|             outfile = str(tmp_path / "temp.tif") | ||||
|             im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) | ||||
| 
 | ||||
|  | @ -732,6 +732,20 @@ class TestFileTiff: | |||
|         with Image.open(mp) as reread: | ||||
|             assert reread.n_frames == 3 | ||||
| 
 | ||||
|     def test_fixoffsets(self) -> None: | ||||
|         b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") | ||||
|         with TiffImagePlugin.AppendingTiffWriter(b) as a: | ||||
|             b.seek(0) | ||||
|             a.fixOffsets(1, isShort=True) | ||||
| 
 | ||||
|             b.seek(0) | ||||
|             a.fixOffsets(1, isLong=True) | ||||
| 
 | ||||
|             # Neither short nor long | ||||
|             b.seek(0) | ||||
|             with pytest.raises(RuntimeError): | ||||
|                 a.fixOffsets(1) | ||||
| 
 | ||||
|     def test_saving_icc_profile(self, tmp_path: Path) -> None: | ||||
|         # Tests saving TIFF with icc_profile set. | ||||
|         # At the time of writing this will only work for non-compressed tiffs | ||||
|  |  | |||
|  | @ -191,6 +191,8 @@ class TestFileWebp: | |||
|         file_path = "Tests/images/hopper.webp" | ||||
|         with Image.open(file_path) as image: | ||||
|             with warnings.catch_warnings(): | ||||
|                 warnings.simplefilter("error") | ||||
| 
 | ||||
|                 image.save(tmp_path / "temp.webp") | ||||
| 
 | ||||
|     def test_file_pointer_could_be_reused(self) -> None: | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| from io import BytesIO | ||||
| from pathlib import Path | ||||
| from typing import IO | ||||
| 
 | ||||
|  | @ -34,6 +35,13 @@ def test_load() -> None: | |||
|             assert im.load()[0, 0] == (255, 255, 255) | ||||
| 
 | ||||
| 
 | ||||
| def test_load_zero_inch() -> None: | ||||
|     b = BytesIO(b"\xd7\xcd\xc6\x9a\x00\x00" + b"\x00" * 10) | ||||
|     with pytest.raises(ValueError): | ||||
|         with Image.open(b): | ||||
|             pass | ||||
| 
 | ||||
| 
 | ||||
| def test_register_handler(tmp_path: Path) -> None: | ||||
|     class TestHandler(ImageFile.StubHandler): | ||||
|         methodCalled = False | ||||
|  | @ -61,6 +69,12 @@ def test_load_float_dpi() -> None: | |||
|     with Image.open("Tests/images/drawing.emf") as im: | ||||
|         assert im.info["dpi"] == 1423.7668161434979 | ||||
| 
 | ||||
|     with open("Tests/images/drawing.emf", "rb") as fp: | ||||
|         data = fp.read() | ||||
|     b = BytesIO(data[:8] + b"\x06\xFA" + data[10:]) | ||||
|     with Image.open(b) as im: | ||||
|         assert im.info["dpi"][0] == 2540 | ||||
| 
 | ||||
| 
 | ||||
| def test_load_set_dpi() -> None: | ||||
|     with Image.open("Tests/images/drawing.wmf") as im: | ||||
|  |  | |||
|  | @ -737,6 +737,8 @@ class TestImage: | |||
|         # Act/Assert | ||||
|         with Image.open(test_file) as im: | ||||
|             with warnings.catch_warnings(): | ||||
|                 warnings.simplefilter("error") | ||||
| 
 | ||||
|                 im.save(temp_file) | ||||
| 
 | ||||
|     def test_no_new_file_on_error(self, tmp_path: Path) -> None: | ||||
|  |  | |||
|  | @ -35,16 +35,25 @@ from .helper import assert_image_equal, hopper | |||
|         ImageFilter.UnsharpMask(10), | ||||
|     ), | ||||
| ) | ||||
| @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) | ||||
| def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: | ||||
| @pytest.mark.parametrize( | ||||
|     "mode", ("L", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK") | ||||
| ) | ||||
| def test_sanity( | ||||
|     filter_to_apply: ImageFilter.Filter | type[ImageFilter.Filter], mode: str | ||||
| ) -> None: | ||||
|     im = hopper(mode) | ||||
|     if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): | ||||
|     if mode[0] != "I" or ( | ||||
|         callable(filter_to_apply) | ||||
|         and issubclass(filter_to_apply, ImageFilter.BuiltinFilter) | ||||
|     ): | ||||
|         out = im.filter(filter_to_apply) | ||||
|         assert out.mode == im.mode | ||||
|         assert out.size == im.size | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) | ||||
| @pytest.mark.parametrize( | ||||
|     "mode", ("L", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK") | ||||
| ) | ||||
| def test_sanity_error(mode: str) -> None: | ||||
|     im = hopper(mode) | ||||
|     with pytest.raises(TypeError): | ||||
|  | @ -145,7 +154,9 @@ def test_kernel_not_enough_coefficients() -> None: | |||
|         ImageFilter.Kernel((3, 3), (0, 0)) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) | ||||
| @pytest.mark.parametrize( | ||||
|     "mode", ("L", "LA", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK") | ||||
| ) | ||||
| def test_consistency_3x3(mode: str) -> None: | ||||
|     with Image.open("Tests/images/hopper.bmp") as source: | ||||
|         with Image.open("Tests/images/hopper_emboss.bmp") as reference: | ||||
|  | @ -161,7 +172,9 @@ def test_consistency_3x3(mode: str) -> None: | |||
|             assert_image_equal(source.filter(kernel), reference) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) | ||||
| @pytest.mark.parametrize( | ||||
|     "mode", ("L", "LA", "I", "I;16", "I;16L", "I;16B", "I;16N", "RGB", "CMYK") | ||||
| ) | ||||
| def test_consistency_5x5(mode: str) -> None: | ||||
|     with Image.open("Tests/images/hopper.bmp") as source: | ||||
|         with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ from pathlib import Path | |||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image | ||||
| from PIL import Image, ImageFile | ||||
| 
 | ||||
| from .helper import ( | ||||
|     assert_image_equal, | ||||
|  | @ -179,7 +179,7 @@ class TestImagingCoreResize: | |||
| 
 | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def gradients_image() -> Generator[Image.Image, None, None]: | ||||
| def gradients_image() -> Generator[ImageFile.ImageFile, None, None]: | ||||
|     with Image.open("Tests/images/radial_gradients.png") as im: | ||||
|         im.load() | ||||
|     try: | ||||
|  | @ -189,7 +189,7 @@ def gradients_image() -> Generator[Image.Image, None, None]: | |||
| 
 | ||||
| 
 | ||||
| class TestReducingGapResize: | ||||
|     def test_reducing_gap_values(self, gradients_image: Image.Image) -> None: | ||||
|     def test_reducing_gap_values(self, gradients_image: ImageFile.ImageFile) -> None: | ||||
|         ref = gradients_image.resize( | ||||
|             (52, 34), Image.Resampling.BICUBIC, reducing_gap=None | ||||
|         ) | ||||
|  | @ -210,7 +210,7 @@ class TestReducingGapResize: | |||
|     ) | ||||
|     def test_reducing_gap_1( | ||||
|         self, | ||||
|         gradients_image: Image.Image, | ||||
|         gradients_image: ImageFile.ImageFile, | ||||
|         box: tuple[float, float, float, float], | ||||
|         epsilon: float, | ||||
|     ) -> None: | ||||
|  | @ -230,7 +230,7 @@ class TestReducingGapResize: | |||
|     ) | ||||
|     def test_reducing_gap_2( | ||||
|         self, | ||||
|         gradients_image: Image.Image, | ||||
|         gradients_image: ImageFile.ImageFile, | ||||
|         box: tuple[float, float, float, float], | ||||
|         epsilon: float, | ||||
|     ) -> None: | ||||
|  | @ -250,7 +250,7 @@ class TestReducingGapResize: | |||
|     ) | ||||
|     def test_reducing_gap_3( | ||||
|         self, | ||||
|         gradients_image: Image.Image, | ||||
|         gradients_image: ImageFile.ImageFile, | ||||
|         box: tuple[float, float, float, float], | ||||
|         epsilon: float, | ||||
|     ) -> None: | ||||
|  | @ -266,7 +266,9 @@ class TestReducingGapResize: | |||
| 
 | ||||
|     @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) | ||||
|     def test_reducing_gap_8( | ||||
|         self, gradients_image: Image.Image, box: tuple[float, float, float, float] | ||||
|         self, | ||||
|         gradients_image: ImageFile.ImageFile, | ||||
|         box: tuple[float, float, float, float], | ||||
|     ) -> None: | ||||
|         ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) | ||||
|         im = gradients_image.resize( | ||||
|  | @ -281,7 +283,7 @@ class TestReducingGapResize: | |||
|     ) | ||||
|     def test_box_filter( | ||||
|         self, | ||||
|         gradients_image: Image.Image, | ||||
|         gradients_image: ImageFile.ImageFile, | ||||
|         box: tuple[float, float, float, float], | ||||
|         epsilon: float, | ||||
|     ) -> None: | ||||
|  |  | |||
|  | @ -104,20 +104,20 @@ def test_transposed() -> None: | |||
|         assert im.size == (590, 88) | ||||
| 
 | ||||
| 
 | ||||
| def test_load_first_unless_jpeg() -> None: | ||||
| def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|     # Test that thumbnail() still uses draft() for JPEG | ||||
|     with Image.open("Tests/images/hopper.jpg") as im: | ||||
|         draft = im.draft | ||||
|         original_draft = im.draft | ||||
| 
 | ||||
|         def im_draft( | ||||
|             mode: str, size: tuple[int, int] | ||||
|             mode: str | None, size: tuple[int, int] | None | ||||
|         ) -> tuple[str, tuple[int, int, float, float]] | None: | ||||
|             result = draft(mode, size) | ||||
|             result = original_draft(mode, size) | ||||
|             assert result is not None | ||||
| 
 | ||||
|             return result | ||||
| 
 | ||||
|         im.draft = im_draft | ||||
|         monkeypatch.setattr(im, "draft", im_draft) | ||||
| 
 | ||||
|         im.thumbnail((64, 64)) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1674,6 +1674,9 @@ def test_continuous_horizontal_edges_polygon() -> None: | |||
| def test_discontiguous_corners_polygon() -> None: | ||||
|     img, draw = create_base_image_draw((84, 68)) | ||||
|     draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) | ||||
|     draw.polygon( | ||||
|         ((82, 29), (82, 26), (82, 24), (67, 22), (52, 29), (52, 15), (67, 22)), BLACK | ||||
|     ) | ||||
|     draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK) | ||||
|     draw.polygon( | ||||
|         ((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)), | ||||
|  |  | |||
|  | @ -93,6 +93,19 @@ class TestImageFile: | |||
|             assert p.image is not None | ||||
|             assert (48, 48) == p.image.size | ||||
| 
 | ||||
|     @pytest.mark.filterwarnings("ignore:Corrupt EXIF data") | ||||
|     def test_incremental_tiff(self) -> None: | ||||
|         with ImageFile.Parser() as p: | ||||
|             with open("Tests/images/hopper.tif", "rb") as f: | ||||
|                 p.feed(f.read(1024)) | ||||
| 
 | ||||
|                 # Check that insufficient data was given in the first feed | ||||
|                 assert not p.image | ||||
| 
 | ||||
|                 p.feed(f.read()) | ||||
|             assert p.image is not None | ||||
|             assert (128, 128) == p.image.size | ||||
| 
 | ||||
|     @skip_unless_feature("webp") | ||||
|     def test_incremental_webp(self) -> None: | ||||
|         with ImageFile.Parser() as p: | ||||
|  |  | |||
|  | @ -52,4 +52,6 @@ def test_image(mode: str) -> None: | |||
| 
 | ||||
| def test_closed_file() -> None: | ||||
|     with warnings.catch_warnings(): | ||||
|         warnings.simplefilter("error") | ||||
| 
 | ||||
|         ImageQt.ImageQt("Tests/images/hopper.gif") | ||||
|  |  | |||
|  | @ -264,4 +264,6 @@ def test_no_resource_warning_for_numpy_array() -> None: | |||
|     with Image.open(test_file) as im: | ||||
|         # Act/Assert | ||||
|         with warnings.catch_warnings(): | ||||
|             warnings.simplefilter("error") | ||||
| 
 | ||||
|             array(im) | ||||
|  |  | |||
|  | @ -74,6 +74,17 @@ def test_pickle_image( | |||
|     helper_pickle_file(tmp_path, protocol, test_file, test_mode) | ||||
| 
 | ||||
| 
 | ||||
| def test_pickle_jpeg() -> None: | ||||
|     # Arrange | ||||
|     with Image.open("Tests/images/hopper.jpg") as image: | ||||
|         # Act: roundtrip | ||||
|         unpickled_image = pickle.loads(pickle.dumps(image)) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert len(unpickled_image.layer) == 3 | ||||
|     assert unpickled_image.layers == 3 | ||||
| 
 | ||||
| 
 | ||||
| def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: | ||||
|     # Arrange | ||||
|     filename = str(tmp_path / "temp.pkl") | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| # Documentation: https://docs.codecov.com/docs/codecov-yaml | ||||
| 
 | ||||
| codecov: | ||||
|   # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" | ||||
|   # Avoid "Missing base report" due to committing with "[CI skip]" | ||||
|   # https://github.com/codecov/support/issues/363 | ||||
|   # https://docs.codecov.com/docs/comparing-commits | ||||
|   allow_coverage_offsets: true | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| #!/bin/bash | ||||
| # install openjpeg | ||||
| 
 | ||||
| archive=openjpeg-2.5.2 | ||||
| archive=openjpeg-2.5.3 | ||||
| 
 | ||||
| ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| #!/bin/bash | ||||
| # install webp | ||||
| 
 | ||||
| archive=libwebp-1.4.0 | ||||
| archive=libwebp-1.5.0 | ||||
| 
 | ||||
| ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ The Python Imaging Library (PIL) is | |||
| 
 | ||||
| Pillow is the friendly PIL fork. It is | ||||
| 
 | ||||
|     Copyright © 2010-2024 by Jeffrey A. Clark and contributors | ||||
|     Copyright © 2010 by Jeffrey A. Clark and contributors | ||||
| 
 | ||||
| Like PIL, Pillow is licensed under the open source PIL | ||||
| Software License: | ||||
|  |  | |||
|  | @ -46,7 +46,7 @@ clean: | |||
| 	-rm -rf $(BUILDDIR)/* | ||||
| 
 | ||||
| install-sphinx: | ||||
| 	$(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinxext-opengraph | ||||
| 	$(PYTHON) -m pip install -e ..[docs] | ||||
| 
 | ||||
| .PHONY: html | ||||
| html: | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ The fork author's goal is to foster and support active development of PIL throug | |||
| License | ||||
| ------- | ||||
| 
 | ||||
| Like PIL, Pillow is `licensed under the open source HPND License <https://raw.githubusercontent.com/python-pillow/Pillow/main/LICENSE>`_ | ||||
| Like PIL, Pillow is `licensed under the open source MIT-CMU License <https://raw.githubusercontent.com/python-pillow/Pillow/main/LICENSE>`_ | ||||
| 
 | ||||
| Why a fork? | ||||
| ----------- | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ import PIL | |||
| # -- General configuration ------------------------------------------------ | ||||
| 
 | ||||
| # If your documentation needs a minimal Sphinx version, state it here. | ||||
| needs_sphinx = "7.3" | ||||
| needs_sphinx = "8.1" | ||||
| 
 | ||||
| # Add any Sphinx extension module names here, as strings. They can be | ||||
| # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom | ||||
|  | @ -55,7 +55,7 @@ master_doc = "index" | |||
| project = "Pillow (PIL Fork)" | ||||
| copyright = ( | ||||
|     "1995-2011 Fredrik Lundh and contributors, " | ||||
|     "2010-2024 Jeffrey A. Clark and contributors." | ||||
|     "2010 Jeffrey A. Clark and contributors." | ||||
| ) | ||||
| author = "Fredrik Lundh (PIL), Jeffrey A. Clark (Pillow)" | ||||
| 
 | ||||
|  | @ -121,7 +121,7 @@ nitpicky = True | |||
| # generating warnings in “nitpicky mode”. Note that type should include the domain name | ||||
| # if present. Example entries would be ('py:func', 'int') or | ||||
| # ('envvar', 'LD_LIBRARY_PATH'). | ||||
| nitpick_ignore = [("py:class", "_io.BytesIO")] | ||||
| nitpick_ignore = [("py:class", "_io.BytesIO"), ("py:class", "_CmsProfileCompatible")] | ||||
| 
 | ||||
| 
 | ||||
| # -- Options for HTML output ---------------------------------------------- | ||||
|  | @ -338,8 +338,6 @@ linkcheck_allowed_redirects = { | |||
| # https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html | ||||
| _repo = "https://github.com/python-pillow/Pillow/" | ||||
| extlinks = { | ||||
|     "cve": ("https://www.cve.org/CVERecord?id=CVE-%s", "CVE-%s"), | ||||
|     "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"), | ||||
|     "issue": (_repo + "issues/%s", "#%s"), | ||||
|     "pr": (_repo + "pull/%s", "#%s"), | ||||
|     "pypi": ("https://pypi.org/project/%s/", "%s"), | ||||
|  |  | |||
|  | @ -572,10 +572,19 @@ JPEG 2000 | |||
| Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``, | ||||
| ``RGBA``, or ``YCbCr`` data.  When reading, ``YCbCr`` data is converted to | ||||
| ``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel. | ||||
| Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``, | ||||
| ``RGBA``, and ``YCbCr`` images with subsampled components.  Pillow supports | ||||
| JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files | ||||
| (``.jp2`` or ``.jpx`` files). | ||||
| 
 | ||||
| .. versionadded:: 8.3.0 | ||||
|    Pillow can read (but not write) ``RGB``, ``RGBA``, and ``YCbCr`` images with | ||||
|    subsampled components. | ||||
| 
 | ||||
| .. versionadded:: 10.4.0 | ||||
|    Pillow can read ``CMYK`` images with OpenJPEG 2.5.1 and later. | ||||
| 
 | ||||
| .. versionadded:: 11.1.0 | ||||
|    Pillow can write ``CMYK`` images with OpenJPEG 2.5.3 and later. | ||||
| 
 | ||||
| Pillow supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed | ||||
| JPEG 2000 files (``.jp2`` or ``.jpx`` files). | ||||
| 
 | ||||
| When loading, if you set the ``mode`` on the image prior to the | ||||
| :py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to | ||||
|  | @ -692,6 +701,30 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: | |||
|    you fail to do this, you will get errors about not being able to load the | ||||
|    ``_imaging`` DLL). | ||||
| 
 | ||||
| MPO | ||||
| ^^^ | ||||
| 
 | ||||
| Pillow reads and writes Multi Picture Object (MPO) files. When first opened, it loads | ||||
| the primary image. The :py:meth:`~PIL.Image.Image.seek` and | ||||
| :py:meth:`~PIL.Image.Image.tell` methods may be used to read other pictures from the | ||||
| file. The pictures are zero-indexed and random access is supported. | ||||
| 
 | ||||
| .. _mpo-saving: | ||||
| 
 | ||||
| Saving | ||||
| ~~~~~~ | ||||
| 
 | ||||
| When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default | ||||
| only the first frame of a multiframe image will be saved. If the ``save_all`` | ||||
| argument is present and true, then all frames will be saved, and the following | ||||
| option will also be available. | ||||
| 
 | ||||
| **append_images** | ||||
|     A list of images to append as additional pictures. Each of the | ||||
|     images in the list can be single or multiframe images. | ||||
| 
 | ||||
|     .. versionadded:: 9.3.0 | ||||
| 
 | ||||
| MSP | ||||
| ^^^ | ||||
| 
 | ||||
|  | @ -1435,30 +1468,6 @@ Note that there may be an embedded gamma of 2.2 in MIC files. | |||
| 
 | ||||
| To enable MIC support, you must install :pypi:`olefile`. | ||||
| 
 | ||||
| MPO | ||||
| ^^^ | ||||
| 
 | ||||
| Pillow identifies and reads Multi Picture Object (MPO) files, loading the primary | ||||
| image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` | ||||
| methods may be used to read other pictures from the file. The pictures are | ||||
| zero-indexed and random access is supported. | ||||
| 
 | ||||
| .. _mpo-saving: | ||||
| 
 | ||||
| Saving | ||||
| ~~~~~~ | ||||
| 
 | ||||
| When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default | ||||
| only the first frame of a multiframe image will be saved. If the ``save_all`` | ||||
| argument is present and true, then all frames will be saved, and the following | ||||
| option will also be available. | ||||
| 
 | ||||
| **append_images** | ||||
|     A list of images to append as additional pictures. Each of the | ||||
|     images in the list can be single or multiframe images. | ||||
| 
 | ||||
|     .. versionadded:: 9.3.0 | ||||
| 
 | ||||
| PCD | ||||
| ^^^ | ||||
| 
 | ||||
|  |  | |||
|  | @ -678,7 +678,7 @@ Reading from URL | |||
| 
 | ||||
|     from PIL import Image | ||||
|     from urllib.request import urlopen | ||||
|     url = "https://python-pillow.org/assets/images/pillow-logo.png" | ||||
|     url = "https://python-pillow.github.io/assets/images/pillow-logo.png" | ||||
|     img = Image.open(urlopen(url)) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -58,7 +58,7 @@ Many of Pillow's features require external libraries: | |||
| * **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**. | ||||
|     **2.4.0**, **2.5.0**, **2.5.2** and **2.5.3**. | ||||
|   * Pillow does **not** support the earlier **1.5** series which ships | ||||
|     with Debian Jessie. | ||||
| 
 | ||||
|  | @ -148,13 +148,7 @@ Many of Pillow's features require external libraries: | |||
|     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. | ||||
|         brew install libjpeg libraqm libtiff little-cms2 openjpeg webp | ||||
| 
 | ||||
| .. tab:: Windows | ||||
| 
 | ||||
|  | @ -195,11 +189,6 @@ Many of Pillow's features require external libraries: | |||
|             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 | ||||
|  |  | |||
|  | @ -29,10 +29,10 @@ These platforms are built and tested for every change. | |||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Debian 12 Bookworm               | 3.11                       | x86, x86-64         | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Fedora 39                        | 3.12                       | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Fedora 40                        | 3.12                       | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Fedora 41                        | 3.13                       | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Gentoo                           | 3.12                       | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | macOS 13 Ventura                 | 3.9                        | x86-64              | | ||||
|  | @ -55,7 +55,7 @@ These platforms are built and tested for every change. | |||
| |                                  +----------------------------+---------------------+ | ||||
| |                                  | 3.13                       | x86                 | | ||||
| |                                  +----------------------------+---------------------+ | ||||
| |                                  | 3.9 (MinGW)                | x86-64              | | ||||
| |                                  | 3.12 (MinGW)               | x86-64              | | ||||
| |                                  +----------------------------+---------------------+ | ||||
| |                                  | 3.9 (Cygwin)               | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
|  | @ -75,7 +75,9 @@ These platforms have been reported to work at the versions mentioned. | |||
| | Operating system                 | | Tested Python            | | Latest tested  | | Tested     | | ||||
| |                                  | | versions                 | | Pillow version | | processors | | ||||
| +==================================+============================+==================+==============+ | ||||
| | macOS 15 Sequoia                 | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0           |arm           | | ||||
| | macOS 15 Sequoia                 | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0           |arm           | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 3.8                        | 10.4.0           |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | macOS 14 Sonoma                  | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.4.0           |arm           | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
|  | @ -148,7 +150,7 @@ These platforms have been reported to work at the versions mentioned. | |||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | 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 23H2                  | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0           |arm64         | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
| | Windows 11 Pro                   | 3.11, 3.12                 | 10.2.0           |x86-64        | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ Example: Parse an image | |||
| 
 | ||||
|     from PIL import ImageFile | ||||
| 
 | ||||
|     fp = open("hopper.pgm", "rb") | ||||
|     fp = open("hopper.ppm", "rb") | ||||
| 
 | ||||
|     p = ImageFile.Parser() | ||||
| 
 | ||||
|  |  | |||
|  | @ -54,6 +54,7 @@ Feature version numbers are available only where stated. | |||
| Support for the following features can be checked: | ||||
| 
 | ||||
| * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. | ||||
| * ``zlib_ng``: (compile time) Whether Pillow was compiled against the zlib-ng version of zlib. Compile-time version number is available. | ||||
| * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. | ||||
| * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. | ||||
| * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. | ||||
|  |  | |||
|  | @ -1,19 +1,6 @@ | |||
| 11.0.0 | ||||
| ------ | ||||
| 
 | ||||
| Security | ||||
| ======== | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| :cve:`YYYY-XXXXX`: TODO | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| Backwards Incompatible Changes | ||||
| ============================== | ||||
| 
 | ||||
|  | @ -159,7 +146,7 @@ Python 3.13 | |||
| 
 | ||||
| Pillow 10.4.0 had wheels built against Python 3.13 beta, available as a preview to help | ||||
| others prepare for 3.13, and to ensure Pillow could be used immediately at the release | ||||
| of 3.13.0 final (2024-10-01, :pep:`719`). | ||||
| of 3.13.0 final (2024-10-07, :pep:`719`). | ||||
| 
 | ||||
| Pillow 11.0.0 now officially supports Python 3.13. | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										86
									
								
								docs/releasenotes/11.1.0.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								docs/releasenotes/11.1.0.rst
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | |||
| 11.1.0 | ||||
| ------ | ||||
| 
 | ||||
| Security | ||||
| ======== | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| :cve:`YYYY-XXXXX`: TODO | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| Backwards Incompatible Changes | ||||
| ============================== | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| 
 | ||||
| Deprecations | ||||
| ============ | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| API Changes | ||||
| =========== | ||||
| 
 | ||||
| Writing XMP bytes to JPEG and MPO | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Pillow 11.0.0 added writing XMP data to JPEG and MPO images:: | ||||
| 
 | ||||
|     im.info["xmp"] = b"test" | ||||
|     im.save("out.jpg") | ||||
| 
 | ||||
| However, this meant that XMP data was automatically kept from an opened image, | ||||
| which is inconsistent with the rest of Pillow's behaviour. This functionality | ||||
| has been removed. To write XMP data, the ``xmp`` argument can still be used for | ||||
| JPEG files:: | ||||
| 
 | ||||
|     im.save("out.jpg", xmp=b"test") | ||||
| 
 | ||||
| To save XMP data to the second frame of an MPO image, ``encoderinfo`` can now | ||||
| be used:: | ||||
| 
 | ||||
|     second_im.encoderinfo = {"xmp": b"test"} | ||||
|     im.save("out.mpo", save_all=True, append_images=[second_im]) | ||||
| 
 | ||||
| API Additions | ||||
| ============= | ||||
| 
 | ||||
| Check for zlib-ng | ||||
| ^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| You can check if Pillow has been built against the zlib-ng version of the | ||||
| zlib library, and what version of zlib-ng is being used:: | ||||
| 
 | ||||
|     from PIL import features | ||||
|     features.check_feature("zlib_ng")  # True or False | ||||
|     features.version_feature("zlib_ng")  # "2.2.2" for example, or None | ||||
| 
 | ||||
| Other Changes | ||||
| ============= | ||||
| 
 | ||||
| Reading JPEG 2000 comments | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| When opening a JPEG 2000 image, the comment may now be read into | ||||
| :py:attr:`~PIL.Image.Image.info` for J2K images, not just JP2 images. | ||||
| 
 | ||||
| Saving JPEG 2000 CMYK images | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files. | ||||
| 
 | ||||
| zlib-ng in wheels | ||||
| ^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Wheels are now built against zlib-ng for improved speed. In tests, saving a PNG | ||||
| was found to be more than twice as fast at higher compression levels. | ||||
|  | @ -14,6 +14,7 @@ expected to be backported to earlier versions. | |||
| .. toctree:: | ||||
|   :maxdepth: 2 | ||||
| 
 | ||||
|   11.1.0 | ||||
|   11.0.0 | ||||
|   10.4.0 | ||||
|   10.3.0 | ||||
|  |  | |||
|  | @ -14,14 +14,14 @@ readme = "README.md" | |||
| keywords = [ | ||||
|   "Imaging", | ||||
| ] | ||||
| license = { text = "HPND" } | ||||
| license = { text = "MIT-CMU" } | ||||
| authors = [ | ||||
|   { name = "Jeffrey A. Clark", email = "aclark@aclark.net" }, | ||||
| ] | ||||
| requires-python = ">=3.9" | ||||
| classifiers = [ | ||||
|   "Development Status :: 6 - Mature", | ||||
|   "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", | ||||
|   "License :: OSI Approved :: CMU License (MIT-CMU)", | ||||
|   "Programming Language :: Python :: 3 :: Only", | ||||
|   "Programming Language :: Python :: 3.9", | ||||
|   "Programming Language :: Python :: 3.10", | ||||
|  | @ -43,7 +43,7 @@ dynamic = [ | |||
| optional-dependencies.docs = [ | ||||
|   "furo", | ||||
|   "olefile", | ||||
|   "sphinx>=7.3", | ||||
|   "sphinx>=8.1", | ||||
|   "sphinx-copybutton", | ||||
|   "sphinx-inline-tabs", | ||||
|   "sphinxext-opengraph", | ||||
|  | @ -56,7 +56,7 @@ optional-dependencies.mic = [ | |||
| ] | ||||
| optional-dependencies.tests = [ | ||||
|   "check-manifest", | ||||
|   "coverage", | ||||
|   "coverage>=7.4.2", | ||||
|   "defusedxml", | ||||
|   "markdown2", | ||||
|   "olefile", | ||||
|  | @ -65,6 +65,7 @@ optional-dependencies.tests = [ | |||
|   "pytest", | ||||
|   "pytest-cov", | ||||
|   "pytest-timeout", | ||||
|   "trove-classifiers>=2024.10.12", | ||||
| ] | ||||
| optional-dependencies.typing = [ | ||||
|   "typing-extensions; python_version<'3.10'", | ||||
|  | @ -72,10 +73,10 @@ optional-dependencies.typing = [ | |||
| optional-dependencies.xmp = [ | ||||
|   "defusedxml", | ||||
| ] | ||||
| urls.Changelog = "https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst" | ||||
| urls.Changelog = "https://github.com/python-pillow/Pillow/releases" | ||||
| urls.Documentation = "https://pillow.readthedocs.io" | ||||
| urls.Funding = "https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=pypi" | ||||
| urls.Homepage = "https://python-pillow.org" | ||||
| urls.Homepage = "https://python-pillow.github.io" | ||||
| urls.Mastodon = "https://fosstodon.org/@pillow" | ||||
| urls."Release notes" = "https://pillow.readthedocs.io/en/stable/releasenotes/index.html" | ||||
| urls.Source = "https://github.com/python-pillow/Pillow" | ||||
|  | @ -93,10 +94,18 @@ version = { attr = "PIL.__version__" } | |||
| [tool.cibuildwheel] | ||||
| before-all = ".github/workflows/wheels-dependencies.sh" | ||||
| build-verbosity = 1 | ||||
| 
 | ||||
| config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" | ||||
| # Disable platform guessing on macOS | ||||
| macos.config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable platform-guessing=disable" | ||||
| 
 | ||||
| test-command = "cd {project} && .github/workflows/wheels-test.sh" | ||||
| test-extras = "tests" | ||||
| 
 | ||||
| [tool.cibuildwheel.macos.environment] | ||||
| PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" | ||||
| DYLD_LIBRARY_PATH = "$(pwd)/build/deps/darwin/lib" | ||||
| 
 | ||||
| [tool.black] | ||||
| exclude = "wheels/multibuild" | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										28
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								setup.py
									
									
									
									
									
								
							|  | @ -344,7 +344,7 @@ class pil_build_ext(build_ext): | |||
|             for x in ("raqm", "fribidi") | ||||
|         ] | ||||
|         + [ | ||||
|             ("disable-platform-guessing", None, "Disable platform guessing on Linux"), | ||||
|             ("disable-platform-guessing", None, "Disable platform guessing"), | ||||
|             ("debug", None, "Debug logging"), | ||||
|         ] | ||||
|     ) | ||||
|  | @ -387,17 +387,18 @@ class pil_build_ext(build_ext): | |||
|                     pass | ||||
|         for x in self.feature: | ||||
|             if getattr(self, f"disable_{x}"): | ||||
|                 setattr(self.feature, x, False) | ||||
|                 self.feature.set(x, False) | ||||
|                 self.feature.required.discard(x) | ||||
|                 _dbg("Disabling %s", x) | ||||
|                 if getattr(self, f"enable_{x}"): | ||||
|                     msg = f"Conflicting options: --enable-{x} and --disable-{x}" | ||||
|                     msg = f"Conflicting options: '-C {x}=enable' and '-C {x}=disable'" | ||||
|                     raise ValueError(msg) | ||||
|                 if x == "freetype": | ||||
|                     _dbg("--disable-freetype implies --disable-raqm") | ||||
|                     _dbg("'-C freetype=disable' implies '-C raqm=disable'") | ||||
|                     if getattr(self, "enable_raqm"): | ||||
|                         msg = ( | ||||
|                             "Conflicting options: --enable-raqm and --disable-freetype" | ||||
|                             "Conflicting options: " | ||||
|                             "'-C raqm=enable' and '-C freetype=disable'" | ||||
|                         ) | ||||
|                         raise ValueError(msg) | ||||
|                     setattr(self, "disable_raqm", True) | ||||
|  | @ -405,15 +406,17 @@ class pil_build_ext(build_ext): | |||
|                 _dbg("Requiring %s", x) | ||||
|                 self.feature.required.add(x) | ||||
|                 if x == "raqm": | ||||
|                     _dbg("--enable-raqm implies --enable-freetype") | ||||
|                     _dbg("'-C raqm=enable' implies '-C freetype=enable'") | ||||
|                     self.feature.required.add("freetype") | ||||
|         for x in ("raqm", "fribidi"): | ||||
|             if getattr(self, f"vendor_{x}"): | ||||
|                 if getattr(self, "disable_raqm"): | ||||
|                     msg = f"Conflicting options: --vendor-{x} and --disable-raqm" | ||||
|                     msg = f"Conflicting options: '-C {x}=vendor' and '-C raqm=disable'" | ||||
|                     raise ValueError(msg) | ||||
|                 if x == "fribidi" and not getattr(self, "vendor_raqm"): | ||||
|                     msg = f"Conflicting options: --vendor-{x} and not --vendor-raqm" | ||||
|                     msg = ( | ||||
|                         f"Conflicting options: '-C {x}=vendor' and not '-C raqm=vendor'" | ||||
|                     ) | ||||
|                     raise ValueError(msg) | ||||
|                 _dbg("Using vendored version of %s", x) | ||||
|                 self.feature.vendor.add(x) | ||||
|  | @ -446,7 +449,7 @@ class pil_build_ext(build_ext): | |||
|     def get_macos_sdk_path(self) -> str | None: | ||||
|         try: | ||||
|             sdk_path = ( | ||||
|                 subprocess.check_output(["xcrun", "--show-sdk-path"]) | ||||
|                 subprocess.check_output(["xcrun", "--show-sdk-path", "--sdk", "macosx"]) | ||||
|                 .strip() | ||||
|                 .decode("latin1") | ||||
|             ) | ||||
|  | @ -604,6 +607,7 @@ class pil_build_ext(build_ext): | |||
|                 _add_directory(library_dirs, "/usr/X11/lib") | ||||
|                 _add_directory(include_dirs, "/usr/X11/include") | ||||
| 
 | ||||
|             # Add the macOS SDK path. | ||||
|             sdk_path = self.get_macos_sdk_path() | ||||
|             if sdk_path: | ||||
|                 _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) | ||||
|  | @ -688,6 +692,8 @@ class pil_build_ext(build_ext): | |||
|                     feature.set("zlib", "z") | ||||
|                 elif sys.platform == "win32" and _find_library_file(self, "zlib"): | ||||
|                     feature.set("zlib", "zlib")  # alternative name | ||||
|                 elif sys.platform == "win32" and _find_library_file(self, "zdll"): | ||||
|                     feature.set("zlib", "zdll")  # dll import library | ||||
| 
 | ||||
|         if feature.want("jpeg"): | ||||
|             _dbg("Looking for jpeg") | ||||
|  | @ -998,7 +1004,7 @@ def debug_build() -> bool: | |||
|     return hasattr(sys, "gettotalrefcount") or FUZZING_BUILD | ||||
| 
 | ||||
| 
 | ||||
| files = ["src/_imaging.c"] | ||||
| files: list[str | os.PathLike[str]] = ["src/_imaging.c"] | ||||
| for src_file in _IMAGING: | ||||
|     files.append("src/" + src_file + ".c") | ||||
| for src_file in _LIB_IMAGING: | ||||
|  | @ -1041,7 +1047,7 @@ except DependencyException as err: | |||
|     msg = f""" | ||||
| 
 | ||||
| The headers or library files could not be found for {str(err)}, | ||||
| which was requested by the option flag --enable-{str(err)} | ||||
| which was requested by the option flag '-C {str(err)}=enable' | ||||
| 
 | ||||
| """ | ||||
|     sys.stderr.write(msg) | ||||
|  |  | |||
|  | @ -273,7 +273,7 @@ class BlpImageFile(ImageFile.ImageFile): | |||
|             raise BLPFormatError(msg) | ||||
| 
 | ||||
|         self._mode = "RGBA" if self._blp_alpha_depth else "RGB" | ||||
|         self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] | ||||
|         self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, self.mode)] | ||||
| 
 | ||||
| 
 | ||||
| class _BLPBaseDecoder(ImageFile.PyDecoder): | ||||
|  |  | |||
|  | @ -560,9 +560,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | |||
|         + struct.pack("<4I", *rgba_mask)  # dwRGBABitMask | ||||
|         + struct.pack("<5I", DDSCAPS.TEXTURE, 0, 0, 0, 0) | ||||
|     ) | ||||
|     ImageFile._save( | ||||
|         im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] | ||||
|     ) | ||||
|     ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)]) | ||||
| 
 | ||||
| 
 | ||||
| def _accept(prefix: bytes) -> bool: | ||||
|  |  | |||
|  | @ -454,7 +454,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) - | |||
|     if hasattr(fp, "flush"): | ||||
|         fp.flush() | ||||
| 
 | ||||
|     ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0, None)]) | ||||
|     ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size)]) | ||||
| 
 | ||||
|     fp.write(b"\n%%%%EndBinary\n") | ||||
|     fp.write(b"grestore end\n") | ||||
|  |  | |||
|  | @ -303,38 +303,38 @@ TAGS = { | |||
| 
 | ||||
| 
 | ||||
| class GPS(IntEnum): | ||||
|     GPSVersionID = 0 | ||||
|     GPSLatitudeRef = 1 | ||||
|     GPSLatitude = 2 | ||||
|     GPSLongitudeRef = 3 | ||||
|     GPSLongitude = 4 | ||||
|     GPSAltitudeRef = 5 | ||||
|     GPSAltitude = 6 | ||||
|     GPSTimeStamp = 7 | ||||
|     GPSSatellites = 8 | ||||
|     GPSStatus = 9 | ||||
|     GPSMeasureMode = 10 | ||||
|     GPSDOP = 11 | ||||
|     GPSSpeedRef = 12 | ||||
|     GPSSpeed = 13 | ||||
|     GPSTrackRef = 14 | ||||
|     GPSTrack = 15 | ||||
|     GPSImgDirectionRef = 16 | ||||
|     GPSImgDirection = 17 | ||||
|     GPSMapDatum = 18 | ||||
|     GPSDestLatitudeRef = 19 | ||||
|     GPSDestLatitude = 20 | ||||
|     GPSDestLongitudeRef = 21 | ||||
|     GPSDestLongitude = 22 | ||||
|     GPSDestBearingRef = 23 | ||||
|     GPSDestBearing = 24 | ||||
|     GPSDestDistanceRef = 25 | ||||
|     GPSDestDistance = 26 | ||||
|     GPSProcessingMethod = 27 | ||||
|     GPSAreaInformation = 28 | ||||
|     GPSDateStamp = 29 | ||||
|     GPSDifferential = 30 | ||||
|     GPSHPositioningError = 31 | ||||
|     GPSVersionID = 0x00 | ||||
|     GPSLatitudeRef = 0x01 | ||||
|     GPSLatitude = 0x02 | ||||
|     GPSLongitudeRef = 0x03 | ||||
|     GPSLongitude = 0x04 | ||||
|     GPSAltitudeRef = 0x05 | ||||
|     GPSAltitude = 0x06 | ||||
|     GPSTimeStamp = 0x07 | ||||
|     GPSSatellites = 0x08 | ||||
|     GPSStatus = 0x09 | ||||
|     GPSMeasureMode = 0x0A | ||||
|     GPSDOP = 0x0B | ||||
|     GPSSpeedRef = 0x0C | ||||
|     GPSSpeed = 0x0D | ||||
|     GPSTrackRef = 0x0E | ||||
|     GPSTrack = 0x0F | ||||
|     GPSImgDirectionRef = 0x10 | ||||
|     GPSImgDirection = 0x11 | ||||
|     GPSMapDatum = 0x12 | ||||
|     GPSDestLatitudeRef = 0x13 | ||||
|     GPSDestLatitude = 0x14 | ||||
|     GPSDestLongitudeRef = 0x15 | ||||
|     GPSDestLongitude = 0x16 | ||||
|     GPSDestBearingRef = 0x17 | ||||
|     GPSDestBearing = 0x18 | ||||
|     GPSDestDistanceRef = 0x19 | ||||
|     GPSDestDistance = 0x1A | ||||
|     GPSProcessingMethod = 0x1B | ||||
|     GPSAreaInformation = 0x1C | ||||
|     GPSDateStamp = 0x1D | ||||
|     GPSDifferential = 0x1E | ||||
|     GPSHPositioningError = 0x1F | ||||
| 
 | ||||
| 
 | ||||
| """Maps EXIF GPS tags to tag names.""" | ||||
|  | @ -342,40 +342,40 @@ GPSTAGS = {i.value: i.name for i in GPS} | |||
| 
 | ||||
| 
 | ||||
| class Interop(IntEnum): | ||||
|     InteropIndex = 1 | ||||
|     InteropVersion = 2 | ||||
|     RelatedImageFileFormat = 4096 | ||||
|     RelatedImageWidth = 4097 | ||||
|     RelatedImageHeight = 4098 | ||||
|     InteropIndex = 0x0001 | ||||
|     InteropVersion = 0x0002 | ||||
|     RelatedImageFileFormat = 0x1000 | ||||
|     RelatedImageWidth = 0x1001 | ||||
|     RelatedImageHeight = 0x1002 | ||||
| 
 | ||||
| 
 | ||||
| class IFD(IntEnum): | ||||
|     Exif = 34665 | ||||
|     GPSInfo = 34853 | ||||
|     Makernote = 37500 | ||||
|     Interop = 40965 | ||||
|     Exif = 0x8769 | ||||
|     GPSInfo = 0x8825 | ||||
|     MakerNote = 0x927C | ||||
|     Interop = 0xA005 | ||||
|     IFD1 = -1 | ||||
| 
 | ||||
| 
 | ||||
| class LightSource(IntEnum): | ||||
|     Unknown = 0 | ||||
|     Daylight = 1 | ||||
|     Fluorescent = 2 | ||||
|     Tungsten = 3 | ||||
|     Flash = 4 | ||||
|     Fine = 9 | ||||
|     Cloudy = 10 | ||||
|     Shade = 11 | ||||
|     DaylightFluorescent = 12 | ||||
|     DayWhiteFluorescent = 13 | ||||
|     CoolWhiteFluorescent = 14 | ||||
|     WhiteFluorescent = 15 | ||||
|     StandardLightA = 17 | ||||
|     StandardLightB = 18 | ||||
|     StandardLightC = 19 | ||||
|     D55 = 20 | ||||
|     D65 = 21 | ||||
|     D75 = 22 | ||||
|     D50 = 23 | ||||
|     ISO = 24 | ||||
|     Other = 255 | ||||
|     Unknown = 0x00 | ||||
|     Daylight = 0x01 | ||||
|     Fluorescent = 0x02 | ||||
|     Tungsten = 0x03 | ||||
|     Flash = 0x04 | ||||
|     Fine = 0x09 | ||||
|     Cloudy = 0x0A | ||||
|     Shade = 0x0B | ||||
|     DaylightFluorescent = 0x0C | ||||
|     DayWhiteFluorescent = 0x0D | ||||
|     CoolWhiteFluorescent = 0x0E | ||||
|     WhiteFluorescent = 0x0F | ||||
|     StandardLightA = 0x11 | ||||
|     StandardLightB = 0x12 | ||||
|     StandardLightC = 0x13 | ||||
|     D55 = 0x14 | ||||
|     D65 = 0x15 | ||||
|     D75 = 0x16 | ||||
|     D50 = 0x17 | ||||
|     ISO = 0x18 | ||||
|     Other = 0xFF | ||||
|  |  | |||
|  | @ -159,7 +159,7 @@ class FliImageFile(ImageFile.ImageFile): | |||
|         framesize = i32(s) | ||||
| 
 | ||||
|         self.decodermaxblock = framesize | ||||
|         self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset, None)] | ||||
|         self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset)] | ||||
| 
 | ||||
|         self.__offset += framesize | ||||
| 
 | ||||
|  |  | |||
|  | @ -170,7 +170,7 @@ class FpxImageFile(ImageFile.ImageFile): | |||
|                         "raw", | ||||
|                         (x, y, x1, y1), | ||||
|                         i32(s, i) + 28, | ||||
|                         (self.rawmode,), | ||||
|                         self.rawmode, | ||||
|                     ) | ||||
|                 ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -95,7 +95,7 @@ class FtexImageFile(ImageFile.ImageFile): | |||
|             self._mode = "RGBA" | ||||
|             self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))] | ||||
|         elif format == Format.UNCOMPRESSED: | ||||
|             self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] | ||||
|             self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, "RGB")] | ||||
|         else: | ||||
|             msg = f"Invalid texture compression format: {repr(format)}" | ||||
|             raise ValueError(msg) | ||||
|  |  | |||
|  | @ -76,7 +76,7 @@ class GdImageFile(ImageFile.ImageFile): | |||
|                 "raw", | ||||
|                 (0, 0) + self.size, | ||||
|                 7 + true_color_offset + 4 + 256 * 4, | ||||
|                 ("L", 0, 1), | ||||
|                 "L", | ||||
|             ) | ||||
|         ] | ||||
| 
 | ||||
|  |  | |||
|  | @ -103,7 +103,6 @@ class GifImageFile(ImageFile.ImageFile): | |||
| 
 | ||||
|         self.info["version"] = s[:6] | ||||
|         self._size = i16(s, 6), i16(s, 8) | ||||
|         self.tile = [] | ||||
|         flags = s[10] | ||||
|         bits = (flags & 7) + 1 | ||||
| 
 | ||||
|  | @ -696,8 +695,9 @@ def _write_multiple_frames( | |||
|                         ) | ||||
|                         background = _get_background(im_frame, color) | ||||
|                         background_im = Image.new("P", im_frame.size, background) | ||||
|                         assert im_frames[0].im.palette is not None | ||||
|                         background_im.putpalette(im_frames[0].im.palette) | ||||
|                         first_palette = im_frames[0].im.palette | ||||
|                         assert first_palette is not None | ||||
|                         background_im.putpalette(first_palette, first_palette.mode) | ||||
|                     bbox = _getbbox(background_im, im_frame)[1] | ||||
|                 elif encoderinfo.get("optimize") and im_frame.mode != "1": | ||||
|                     if "transparency" not in encoderinfo: | ||||
|  |  | |||
|  | @ -357,7 +357,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | |||
|         name = "".join([name[: 92 - len(ext)], ext]) | ||||
| 
 | ||||
|         fp.write(f"Name: {name}\r\n".encode("ascii")) | ||||
|     fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode("ascii")) | ||||
|     fp.write(f"Image size (x*y): {im.size[0]}*{im.size[1]}\r\n".encode("ascii")) | ||||
|     fp.write(f"File size (no of images): {frames}\r\n".encode("ascii")) | ||||
|     if im.mode in ["P", "PA"]: | ||||
|         fp.write(b"Lut: 1\r\n") | ||||
|  |  | |||
|  | @ -692,13 +692,10 @@ class Image: | |||
|         ) | ||||
| 
 | ||||
|     def __repr__(self) -> str: | ||||
|         return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( | ||||
|             self.__class__.__module__, | ||||
|             self.__class__.__name__, | ||||
|             self.mode, | ||||
|             self.size[0], | ||||
|             self.size[1], | ||||
|             id(self), | ||||
|         return ( | ||||
|             f"<{self.__class__.__module__}.{self.__class__.__name__} " | ||||
|             f"image mode={self.mode} size={self.size[0]}x{self.size[1]} " | ||||
|             f"at 0x{id(self):X}>" | ||||
|         ) | ||||
| 
 | ||||
|     def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: | ||||
|  | @ -707,14 +704,8 @@ class Image: | |||
|         # Same as __repr__ but without unpredictable id(self), | ||||
|         # to keep Jupyter notebook `text/plain` output stable. | ||||
|         p.text( | ||||
|             "<%s.%s image mode=%s size=%dx%d>" | ||||
|             % ( | ||||
|                 self.__class__.__module__, | ||||
|                 self.__class__.__name__, | ||||
|                 self.mode, | ||||
|                 self.size[0], | ||||
|                 self.size[1], | ||||
|             ) | ||||
|             f"<{self.__class__.__module__}.{self.__class__.__name__} " | ||||
|             f"image mode={self.mode} size={self.size[0]}x{self.size[1]}>" | ||||
|         ) | ||||
| 
 | ||||
|     def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None: | ||||
|  | @ -763,7 +754,7 @@ class Image: | |||
| 
 | ||||
|     def __setstate__(self, state: list[Any]) -> None: | ||||
|         Image.__init__(self) | ||||
|         info, mode, size, palette, data = state | ||||
|         info, mode, size, palette, data = state[:5] | ||||
|         self.info = info | ||||
|         self._mode = mode | ||||
|         self._size = size | ||||
|  | @ -1574,7 +1565,7 @@ class Image: | |||
|                 for subifd_offset in subifd_offsets: | ||||
|                     ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) | ||||
|         ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) | ||||
|         if ifd1 and ifd1.get(513): | ||||
|         if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset): | ||||
|             assert exif._info is not None | ||||
|             ifds.append((ifd1, exif._info.next)) | ||||
| 
 | ||||
|  | @ -1586,11 +1577,11 @@ class Image: | |||
| 
 | ||||
|             fp = self.fp | ||||
|             if ifd is not None: | ||||
|                 thumbnail_offset = ifd.get(513) | ||||
|                 thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset) | ||||
|                 if thumbnail_offset is not None: | ||||
|                     thumbnail_offset += getattr(self, "_exif_offset", 0) | ||||
|                     self.fp.seek(thumbnail_offset) | ||||
|                     data = self.fp.read(ifd.get(514)) | ||||
|                     data = self.fp.read(ifd.get(ExifTags.Base.JpegIFByteCount)) | ||||
|                     fp = io.BytesIO(data) | ||||
| 
 | ||||
|             with open(fp) as im: | ||||
|  | @ -2550,7 +2541,7 @@ class Image: | |||
|         filename: str | bytes = "" | ||||
|         open_fp = False | ||||
|         if is_path(fp): | ||||
|             filename = os.path.realpath(os.fspath(fp)) | ||||
|             filename = os.fspath(fp) | ||||
|             open_fp = True | ||||
|         elif fp == sys.stdout: | ||||
|             try: | ||||
|  | @ -2559,13 +2550,13 @@ class Image: | |||
|                 pass | ||||
|         if not filename and hasattr(fp, "name") and is_path(fp.name): | ||||
|             # only set the name for metadata purposes | ||||
|             filename = os.path.realpath(os.fspath(fp.name)) | ||||
|             filename = os.fspath(fp.name) | ||||
| 
 | ||||
|         # may mutate self! | ||||
|         self._ensure_mutable() | ||||
| 
 | ||||
|         save_all = params.pop("save_all", False) | ||||
|         self.encoderinfo = params | ||||
|         self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params} | ||||
|         self.encoderconfig: tuple[Any, ...] = () | ||||
| 
 | ||||
|         preinit() | ||||
|  | @ -2612,6 +2603,11 @@ class Image: | |||
|                 except PermissionError: | ||||
|                     pass | ||||
|             raise | ||||
|         finally: | ||||
|             try: | ||||
|                 del self.encoderinfo | ||||
|             except AttributeError: | ||||
|                 pass | ||||
|         if open_fp: | ||||
|             fp.close() | ||||
| 
 | ||||
|  | @ -3463,7 +3459,7 @@ def open( | |||
|     exclusive_fp = False | ||||
|     filename: str | bytes = "" | ||||
|     if is_path(fp): | ||||
|         filename = os.path.realpath(os.fspath(fp)) | ||||
|         filename = os.fspath(fp) | ||||
| 
 | ||||
|     if filename: | ||||
|         fp = builtins.open(filename, "rb") | ||||
|  | @ -3893,7 +3889,7 @@ class Exif(_ExifBase): | |||
|       gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) | ||||
|       print(gps_ifd) | ||||
| 
 | ||||
|     Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, | ||||
|     Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.MakerNote``, | ||||
|     ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. | ||||
| 
 | ||||
|     :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: | ||||
|  | @ -4056,11 +4052,11 @@ class Exif(_ExifBase): | |||
|                     ifd = self._get_ifd_dict(offset, tag) | ||||
|                     if ifd is not None: | ||||
|                         self._ifds[tag] = ifd | ||||
|             elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: | ||||
|             elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.MakerNote]: | ||||
|                 if ExifTags.IFD.Exif not in self._ifds: | ||||
|                     self.get_ifd(ExifTags.IFD.Exif) | ||||
|                 tag_data = self._ifds[ExifTags.IFD.Exif][tag] | ||||
|                 if tag == ExifTags.IFD.Makernote: | ||||
|                 if tag == ExifTags.IFD.MakerNote: | ||||
|                     from .TiffImagePlugin import ImageFileDirectory_v2 | ||||
| 
 | ||||
|                     if tag_data[:8] == b"FUJIFILM": | ||||
|  | @ -4147,7 +4143,7 @@ class Exif(_ExifBase): | |||
|             ifd = { | ||||
|                 k: v | ||||
|                 for (k, v) in ifd.items() | ||||
|                 if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote) | ||||
|                 if k not in (ExifTags.IFD.Interop, ExifTags.IFD.MakerNote) | ||||
|             } | ||||
|         return ifd | ||||
| 
 | ||||
|  |  | |||
|  | @ -31,6 +31,10 @@ from ._typing import SupportsRead | |||
| 
 | ||||
| try: | ||||
|     from . import _imagingcms as core | ||||
| 
 | ||||
|     _CmsProfileCompatible = Union[ | ||||
|         str, SupportsRead[bytes], core.CmsProfile, "ImageCmsProfile" | ||||
|     ] | ||||
| except ImportError as ex: | ||||
|     # Allow error import for doc purposes, but error out when accessing | ||||
|     # anything in core. | ||||
|  | @ -389,10 +393,6 @@ def get_display_profile(handle: SupportsInt | None = None) -> ImageCmsProfile | | |||
| # pyCMS compatible layer | ||||
| # --------------------------------------------------------------------. | ||||
| 
 | ||||
| _CmsProfileCompatible = Union[ | ||||
|     str, SupportsRead[bytes], core.CmsProfile, ImageCmsProfile | ||||
| ] | ||||
| 
 | ||||
| 
 | ||||
| class PyCMSError(Exception): | ||||
|     """(pyCMS) Exception class. | ||||
|  |  | |||
|  | @ -98,8 +98,8 @@ def _tilesort(t: _Tile) -> int: | |||
| class _Tile(NamedTuple): | ||||
|     codec_name: str | ||||
|     extents: tuple[int, int, int, int] | None | ||||
|     offset: int | ||||
|     args: tuple[Any, ...] | str | None | ||||
|     offset: int = 0 | ||||
|     args: tuple[Any, ...] | str | None = None | ||||
| 
 | ||||
| 
 | ||||
| # | ||||
|  | @ -120,7 +120,7 @@ class ImageFile(Image.Image): | |||
|         self.custom_mimetype: str | None = None | ||||
| 
 | ||||
|         self.tile: list[_Tile] = [] | ||||
|         """ A list of tile descriptors, or ``None`` """ | ||||
|         """ A list of tile descriptors """ | ||||
| 
 | ||||
|         self.readonly = 1  # until we know better | ||||
| 
 | ||||
|  | @ -130,7 +130,7 @@ class ImageFile(Image.Image): | |||
|         if is_path(fp): | ||||
|             # filename | ||||
|             self.fp = open(fp, "rb") | ||||
|             self.filename = os.path.realpath(os.fspath(fp)) | ||||
|             self.filename = os.fspath(fp) | ||||
|             self._exclusive_fp = True | ||||
|         else: | ||||
|             # stream | ||||
|  |  | |||
|  | @ -553,7 +553,7 @@ class Color3DLUT(MultibandFilter): | |||
|         ch_out = channels or ch_in | ||||
|         size_1d, size_2d, size_3d = self.size | ||||
| 
 | ||||
|         table = [0] * (size_1d * size_2d * size_3d * ch_out) | ||||
|         table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out) | ||||
|         idx_in = 0 | ||||
|         idx_out = 0 | ||||
|         for b in range(size_3d): | ||||
|  |  | |||
|  | @ -270,7 +270,7 @@ class FreeTypeFont: | |||
|             ) | ||||
| 
 | ||||
|         if is_path(font): | ||||
|             font = os.path.realpath(os.fspath(font)) | ||||
|             font = os.fspath(font) | ||||
|             if sys.platform == "win32": | ||||
|                 font_bytes_path = font if isinstance(font, bytes) else font.encode() | ||||
|                 try: | ||||
|  |  | |||
|  | @ -104,28 +104,17 @@ def grab( | |||
| 
 | ||||
| def grabclipboard() -> Image.Image | list[str] | None: | ||||
|     if sys.platform == "darwin": | ||||
|         fh, filepath = tempfile.mkstemp(".png") | ||||
|         os.close(fh) | ||||
|         commands = [ | ||||
|             'set theFile to (open for access POSIX file "' | ||||
|             + filepath | ||||
|             + '" with write permission)', | ||||
|             "try", | ||||
|             "    write (the clipboard as «class PNGf») to theFile", | ||||
|             "end try", | ||||
|             "close access theFile", | ||||
|         ] | ||||
|         script = ["osascript"] | ||||
|         for command in commands: | ||||
|             script += ["-e", command] | ||||
|         subprocess.call(script) | ||||
|         p = subprocess.run( | ||||
|             ["osascript", "-e", "get the clipboard as «class PNGf»"], | ||||
|             capture_output=True, | ||||
|         ) | ||||
|         if p.returncode != 0: | ||||
|             return None | ||||
| 
 | ||||
|         im = None | ||||
|         if os.stat(filepath).st_size != 0: | ||||
|             im = Image.open(filepath) | ||||
|             im.load() | ||||
|         os.unlink(filepath) | ||||
|         return im | ||||
|         import binascii | ||||
| 
 | ||||
|         data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3])) | ||||
|         return Image.open(data) | ||||
|     elif sys.platform == "win32": | ||||
|         fmt, data = Image.core.grabclipboard_win32() | ||||
|         if fmt == "file":  # CF_HDROP | ||||
|  |  | |||
|  | @ -173,10 +173,10 @@ class _Operand: | |||
|         return self.apply("rshift", self, other) | ||||
| 
 | ||||
|     # logical | ||||
|     def __eq__(self, other): | ||||
|     def __eq__(self, other: _Operand | float) -> _Operand:  # type: ignore[override] | ||||
|         return self.apply("eq", self, other) | ||||
| 
 | ||||
|     def __ne__(self, other): | ||||
|     def __ne__(self, other: _Operand | float) -> _Operand:  # type: ignore[override] | ||||
|         return self.apply("ne", self, other) | ||||
| 
 | ||||
|     def __lt__(self, other: _Operand | float) -> _Operand: | ||||
|  |  | |||
|  | @ -698,10 +698,11 @@ def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | |||
|         8: Image.Transpose.ROTATE_90, | ||||
|     }.get(orientation) | ||||
|     if method is not None: | ||||
|         transposed_image = image.transpose(method) | ||||
|         if in_place: | ||||
|             image.im = transposed_image.im | ||||
|             image._size = transposed_image._size | ||||
|             image.im = image.im.transpose(method) | ||||
|             image._size = image.im.size | ||||
|         else: | ||||
|             transposed_image = image.transpose(method) | ||||
|         exif_image = image if in_place else transposed_image | ||||
| 
 | ||||
|         exif = exif_image.getexif() | ||||
|  |  | |||
|  | @ -213,4 +213,7 @@ def toqimage(im: Image.Image | str | QByteArray) -> ImageQt: | |||
| 
 | ||||
| def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap: | ||||
|     qimage = toqimage(im) | ||||
|     return getattr(QPixmap, "fromImage")(qimage) | ||||
|     pixmap = getattr(QPixmap, "fromImage")(qimage) | ||||
|     if qt_version == "6": | ||||
|         pixmap.detach() | ||||
|     return pixmap | ||||
|  |  | |||
|  | @ -62,7 +62,7 @@ class ImtImageFile(ImageFile.ImageFile): | |||
|                         "raw", | ||||
|                         (0, 0) + self.size, | ||||
|                         self.fp.tell() - len(buffer), | ||||
|                         (self.mode, 0, 1), | ||||
|                         self.mode, | ||||
|                     ) | ||||
|                 ] | ||||
| 
 | ||||
|  |  | |||
|  | @ -252,6 +252,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): | |||
|         if sig == b"\xff\x4f\xff\x51": | ||||
|             self.codec = "j2k" | ||||
|             self._size, self._mode = _parse_codestream(self.fp) | ||||
|             self._parse_comment() | ||||
|         else: | ||||
|             sig = sig + self.fp.read(8) | ||||
| 
 | ||||
|  | @ -262,6 +263,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile): | |||
|                 if dpi is not None: | ||||
|                     self.info["dpi"] = dpi | ||||
|                 if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): | ||||
|                     hdr = self.fp.read(2) | ||||
|                     length = _binary.i16be(hdr) | ||||
|                     self.fp.seek(length - 2, os.SEEK_CUR) | ||||
|                     self._parse_comment() | ||||
|             else: | ||||
|                 msg = "not a JPEG 2000 file" | ||||
|  | @ -296,10 +300,6 @@ class Jpeg2KImageFile(ImageFile.ImageFile): | |||
|         ] | ||||
| 
 | ||||
|     def _parse_comment(self) -> None: | ||||
|         hdr = self.fp.read(2) | ||||
|         length = _binary.i16be(hdr) | ||||
|         self.fp.seek(length - 2, os.SEEK_CUR) | ||||
| 
 | ||||
|         while True: | ||||
|             marker = self.fp.read(2) | ||||
|             if not marker: | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user