mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-10-30 07:27:49 +03:00 
			
		
		
		
	Merge branch 'main' into msys
This commit is contained in:
		
						commit
						40a874d850
					
				|  | @ -1,99 +0,0 @@ | ||||||
| skip_commits: |  | ||||||
|   files: |  | ||||||
|     - ".github/**/*" |  | ||||||
|     - ".gitmodules" |  | ||||||
|     - "docs/**/*" |  | ||||||
|     - "wheels/**/*" |  | ||||||
| 
 |  | ||||||
| version: '{build}' |  | ||||||
| clone_folder: c:\pillow |  | ||||||
| init: |  | ||||||
| - ECHO %PYTHON% |  | ||||||
| #- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) |  | ||||||
| # Uncomment previous line to get RDP access during the build. |  | ||||||
| 
 |  | ||||||
| environment: |  | ||||||
|   COVERAGE_CORE: sysmon |  | ||||||
|   EXECUTABLE: python.exe |  | ||||||
|   TEST_OPTIONS: |  | ||||||
|   DEPLOY: YES |  | ||||||
|   matrix: |  | ||||||
|   - PYTHON: C:/Python313 |  | ||||||
|     ARCHITECTURE: x86 |  | ||||||
|     APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 |  | ||||||
|   - PYTHON: C:/Python39-x64 |  | ||||||
|     ARCHITECTURE: AMD64 |  | ||||||
|     APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| install: |  | ||||||
| - '%PYTHON%\%EXECUTABLE% --version' |  | ||||||
| - '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip' |  | ||||||
| - curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip |  | ||||||
| - 7z x pillow-test-images.zip -oc:\ |  | ||||||
| - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images |  | ||||||
| - curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip |  | ||||||
| - 7z x nasm-win64.zip -oc:\ |  | ||||||
| - choco install ghostscript --version=10.4.0 |  | ||||||
| - path c:\nasm-2.16.03;C:\Program Files\gs\gs10.04.0\bin;%PATH% |  | ||||||
| - cd c:\pillow\winbuild\ |  | ||||||
| - ps: | |  | ||||||
|         c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ |  | ||||||
|         c:\pillow\winbuild\build\build_dep_all.cmd |  | ||||||
|         $host.SetShouldExit(0) |  | ||||||
| - path C:\pillow\winbuild\build\bin;%PATH% |  | ||||||
| 
 |  | ||||||
| build_script: |  | ||||||
| - cd c:\pillow |  | ||||||
| - winbuild\build\build_env.cmd |  | ||||||
| - '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .' |  | ||||||
| - '%PYTHON%\%EXECUTABLE% selftest.py --installed' |  | ||||||
| 
 |  | ||||||
| test_script: |  | ||||||
| - cd c:\pillow |  | ||||||
| - '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma' |  | ||||||
| - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% |  | ||||||
| - path %PYTHON%;%PATH% |  | ||||||
| - .ci\test.cmd |  | ||||||
| 
 |  | ||||||
| after_test: |  | ||||||
| - curl -Os https://uploader.codecov.io/latest/windows/codecov.exe |  | ||||||
| - .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor |  | ||||||
| 
 |  | ||||||
| matrix: |  | ||||||
|   fast_finish: true |  | ||||||
| 
 |  | ||||||
| cache: |  | ||||||
| - '%LOCALAPPDATA%\pip\Cache' |  | ||||||
| 
 |  | ||||||
| artifacts: |  | ||||||
| - path: pillow\*.egg |  | ||||||
|   name: egg |  | ||||||
| - path: pillow\*.whl |  | ||||||
|   name: wheel |  | ||||||
| 
 |  | ||||||
| before_deploy: |  | ||||||
|   - cd c:\pillow |  | ||||||
|   - '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .' |  | ||||||
|   - ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } |  | ||||||
| 
 |  | ||||||
| deploy: |  | ||||||
|   provider: S3 |  | ||||||
|   region: us-west-2 |  | ||||||
|   access_key_id: AKIAIRAXC62ZNTVQJMOQ |  | ||||||
|   secret_access_key: |  | ||||||
|     secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi |  | ||||||
|   bucket: pillow-nightly |  | ||||||
|   folder: win/$(APPVEYOR_BUILD_NUMBER)/ |  | ||||||
|   artifact: /.*egg|wheel/ |  | ||||||
|   on: |  | ||||||
|     APPVEYOR_REPO_NAME: python-pillow/Pillow |  | ||||||
|     branch: main |  | ||||||
|     deploy: YES |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| # Uncomment the following lines to get RDP access after the build/test and block for |  | ||||||
| # up to the timeout limit (~1hr) |  | ||||||
| # |  | ||||||
| #on_finish: |  | ||||||
| #- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) |  | ||||||
|  | @ -3,8 +3,5 @@ | ||||||
| set -e | set -e | ||||||
| 
 | 
 | ||||||
| python3 -m coverage erase | python3 -m coverage erase | ||||||
| if [ $(uname) == "Darwin" ]; then |  | ||||||
|     export CPPFLAGS="-I/usr/local/miniconda/include"; |  | ||||||
| fi |  | ||||||
| make clean | make clean | ||||||
| make install-coverage | make install-coverage | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							|  | @ -9,7 +9,7 @@ Please send a pull request to the `main` branch. Please include [documentation]( | ||||||
| - Fork the Pillow repository. | - Fork the Pillow repository. | ||||||
| - Create a branch from `main`. | - Create a branch from `main`. | ||||||
| - Develop bug fixes, features, tests, etc. | - Develop bug fixes, features, tests, etc. | ||||||
| - Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests. | - Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests. | ||||||
| - Create a pull request to pull the changes from your branch to the Pillow `main`. | - Create a pull request to pull the changes from your branch to the Pillow `main`. | ||||||
| 
 | 
 | ||||||
| ### Guidelines | ### Guidelines | ||||||
|  | @ -17,7 +17,7 @@ Please send a pull request to the `main` branch. Please include [documentation]( | ||||||
| - Separate code commits from reformatting commits. | - Separate code commits from reformatting commits. | ||||||
| - Provide tests for any newly added code. | - Provide tests for any newly added code. | ||||||
| - Follow PEP 8. | - Follow PEP 8. | ||||||
| - When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. | - When committing only documentation changes please include `[ci skip]` in the commit message to avoid running extra tests. | ||||||
| - 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. | - 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. | ||||||
| 
 | 
 | ||||||
| ## Reporting Issues | ## Reporting Issues | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								.github/mergify.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/mergify.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -9,7 +9,6 @@ pull_request_rules: | ||||||
|       - status-success=Windows Test Successful |       - status-success=Windows Test Successful | ||||||
|       - status-success=MSYS2 Test Successful |       - status-success=MSYS2 Test Successful | ||||||
|       - status-success=Cygwin Test Successful |       - status-success=Cygwin Test Successful | ||||||
|       - status-success=continuous-integration/appveyor/pr |  | ||||||
|     actions: |     actions: | ||||||
|       merge: |       merge: | ||||||
|         method: merge |         method: merge | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							|  | @ -10,15 +10,11 @@ brew install \ | ||||||
|     ghostscript \ |     ghostscript \ | ||||||
|     jpeg-turbo \ |     jpeg-turbo \ | ||||||
|     libimagequant \ |     libimagequant \ | ||||||
|  |     libraqm \ | ||||||
|     libtiff \ |     libtiff \ | ||||||
|     little-cms2 \ |     little-cms2 \ | ||||||
|     openjpeg \ |     openjpeg \ | ||||||
|     webp |     webp | ||||||
| if [[ "$ImageOS" == "macos13" ]]; then |  | ||||||
|     brew install --ignore-dependencies libraqm |  | ||||||
| else |  | ||||||
|     brew install libraqm |  | ||||||
| fi |  | ||||||
| export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" | export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" | ||||||
| 
 | 
 | ||||||
| python3 -m pip install coverage | python3 -m pip install coverage | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								.github/workflows/test-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								.github/workflows/test-docker.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -29,16 +29,12 @@ concurrency: | ||||||
| jobs: | jobs: | ||||||
|   build: |   build: | ||||||
| 
 | 
 | ||||||
|     runs-on: ubuntu-latest |     runs-on: ${{ matrix.os }} | ||||||
|     strategy: |     strategy: | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|  |         os: ["ubuntu-latest"] | ||||||
|         docker: [ |         docker: [ | ||||||
|           # Run slower jobs first to give them a headstart and reduce waiting time |  | ||||||
|           ubuntu-22.04-jammy-arm64v8, |  | ||||||
|           ubuntu-24.04-noble-ppc64le, |  | ||||||
|           ubuntu-24.04-noble-s390x, |  | ||||||
|           # Then run the remainder |  | ||||||
|           alpine, |           alpine, | ||||||
|           amazon-2-amd64, |           amazon-2-amd64, | ||||||
|           amazon-2023-amd64, |           amazon-2023-amd64, | ||||||
|  | @ -55,12 +51,17 @@ jobs: | ||||||
|         ] |         ] | ||||||
|         dockerTag: [main] |         dockerTag: [main] | ||||||
|         include: |         include: | ||||||
|           - docker: "ubuntu-22.04-jammy-arm64v8" |  | ||||||
|             qemu-arch: "aarch64" |  | ||||||
|           - docker: "ubuntu-24.04-noble-ppc64le" |           - docker: "ubuntu-24.04-noble-ppc64le" | ||||||
|  |             os: "ubuntu-22.04" | ||||||
|             qemu-arch: "ppc64le" |             qemu-arch: "ppc64le" | ||||||
|  |             dockerTag: main | ||||||
|           - docker: "ubuntu-24.04-noble-s390x" |           - docker: "ubuntu-24.04-noble-s390x" | ||||||
|  |             os: "ubuntu-22.04" | ||||||
|             qemu-arch: "s390x" |             qemu-arch: "s390x" | ||||||
|  |             dockerTag: main | ||||||
|  |           - docker: "ubuntu-24.04-noble-arm64v8" | ||||||
|  |             os: "ubuntu-24.04-arm" | ||||||
|  |             dockerTag: main | ||||||
| 
 | 
 | ||||||
|     name: ${{ matrix.docker }} |     name: ${{ matrix.docker }} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										14
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -31,15 +31,20 @@ env: | ||||||
| 
 | 
 | ||||||
| jobs: | jobs: | ||||||
|   build: |   build: | ||||||
|     runs-on: windows-latest |     runs-on: ${{ matrix.os }} | ||||||
|     strategy: |     strategy: | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|         python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] |         python-version: ["pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"] | ||||||
|  |         architecture: ["x64"] | ||||||
|  |         os: ["windows-latest"] | ||||||
|  |         include: | ||||||
|  |             # Test the oldest Python on 32-bit | ||||||
|  |             - { python-version: "3.9", architecture: "x86", os: "windows-2019" } | ||||||
| 
 | 
 | ||||||
|     timeout-minutes: 30 |     timeout-minutes: 30 | ||||||
| 
 | 
 | ||||||
|     name: Python ${{ matrix.python-version }} |     name: Python ${{ matrix.python-version }} (${{ matrix.architecture }}) | ||||||
| 
 | 
 | ||||||
|     steps: |     steps: | ||||||
|     - name: Checkout Pillow |     - name: Checkout Pillow | ||||||
|  | @ -67,6 +72,7 @@ jobs: | ||||||
|       with: |       with: | ||||||
|         python-version: ${{ matrix.python-version }} |         python-version: ${{ matrix.python-version }} | ||||||
|         allow-prereleases: true |         allow-prereleases: true | ||||||
|  |         architecture: ${{ matrix.architecture }} | ||||||
|         cache: pip |         cache: pip | ||||||
|         cache-dependency-path: ".github/workflows/test-windows.yml" |         cache-dependency-path: ".github/workflows/test-windows.yml" | ||||||
| 
 | 
 | ||||||
|  | @ -78,7 +84,7 @@ jobs: | ||||||
|         python3 -m pip install --upgrade pip |         python3 -m pip install --upgrade pip | ||||||
| 
 | 
 | ||||||
|     - name: Install CPython dependencies |     - name: Install CPython dependencies | ||||||
|       if: "!contains(matrix.python-version, 'pypy')" |       if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'" | ||||||
|       run: | |       run: | | ||||||
|         python3 -m pip install PyQt6 |         python3 -m pip install PyQt6 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								.github/workflows/wheels-dependencies.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/wheels-dependencies.sh
									
									
									
									
										vendored
									
									
								
							|  | @ -38,11 +38,11 @@ ARCHIVE_SDIR=pillow-depends-main | ||||||
| 
 | 
 | ||||||
| # Package versions for fresh source builds | # Package versions for fresh source builds | ||||||
| FREETYPE_VERSION=2.13.3 | FREETYPE_VERSION=2.13.3 | ||||||
| HARFBUZZ_VERSION=10.1.0 | HARFBUZZ_VERSION=10.2.0 | ||||||
| LIBPNG_VERSION=1.6.45 | LIBPNG_VERSION=1.6.46 | ||||||
| JPEGTURBO_VERSION=3.1.0 | JPEGTURBO_VERSION=3.1.0 | ||||||
| OPENJPEG_VERSION=2.5.3 | OPENJPEG_VERSION=2.5.3 | ||||||
| XZ_VERSION=5.6.3 | XZ_VERSION=5.6.4 | ||||||
| TIFF_VERSION=4.6.0 | TIFF_VERSION=4.6.0 | ||||||
| LCMS2_VERSION=2.16 | LCMS2_VERSION=2.16 | ||||||
| ZLIB_NG_VERSION=2.2.3 | ZLIB_NG_VERSION=2.2.3 | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								.github/workflows/wheels-test.ps1
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/wheels-test.ps1
									
									
									
									
										vendored
									
									
								
							|  | @ -11,6 +11,9 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") { | ||||||
| $env:path += ";$pillow\winbuild\build\bin\" | $env:path += ";$pillow\winbuild\build\bin\" | ||||||
| & "$venv\Scripts\activate.ps1" | & "$venv\Scripts\activate.ps1" | ||||||
| & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f | & reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f | ||||||
|  | if ("$venv" -like "*\cibw-run-*-win_amd64\*") { | ||||||
|  |   & python -m pip install numpy | ||||||
|  | } | ||||||
| cd $pillow | cd $pillow | ||||||
| & python -VV | & python -VV | ||||||
| if (!$?) { exit $LASTEXITCODE } | if (!$?) { exit $LASTEXITCODE } | ||||||
|  |  | ||||||
							
								
								
									
										75
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										75
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -42,62 +42,7 @@ env: | ||||||
|   FORCE_COLOR: 1 |   FORCE_COLOR: 1 | ||||||
| 
 | 
 | ||||||
| jobs: | jobs: | ||||||
|   build-1-QEMU-emulated-wheels: |   build-native-wheels: | ||||||
|     if: github.event_name != 'schedule' |  | ||||||
|     name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     strategy: |  | ||||||
|       fail-fast: false |  | ||||||
|       matrix: |  | ||||||
|         python-version: |  | ||||||
|           - pp310 |  | ||||||
|           - cp3{9,10,11} |  | ||||||
|           - cp3{12,13} |  | ||||||
|         spec: |  | ||||||
|           - manylinux2014 |  | ||||||
|           - manylinux_2_28 |  | ||||||
|           - musllinux |  | ||||||
|         exclude: |  | ||||||
|           - { python-version: pp310, spec: musllinux } |  | ||||||
| 
 |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|         with: |  | ||||||
|           persist-credentials: false |  | ||||||
|           submodules: true |  | ||||||
| 
 |  | ||||||
|       - uses: actions/setup-python@v5 |  | ||||||
|         with: |  | ||||||
|           python-version: "3.x" |  | ||||||
| 
 |  | ||||||
|       # https://github.com/docker/setup-qemu-action |  | ||||||
|       - name: Set up QEMU |  | ||||||
|         uses: docker/setup-qemu-action@v3 |  | ||||||
| 
 |  | ||||||
|       - name: Install cibuildwheel |  | ||||||
|         run: | |  | ||||||
|           python3 -m pip install -r .ci/requirements-cibw.txt |  | ||||||
| 
 |  | ||||||
|       - name: Build wheels |  | ||||||
|         run: | |  | ||||||
|           python3 -m cibuildwheel --output-dir wheelhouse |  | ||||||
|         env: |  | ||||||
|           # Build only the currently selected Linux architecture (so we can |  | ||||||
|           # parallelise for speed). |  | ||||||
|           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_ENABLE: cpython-prerelease |  | ||||||
|           # Extra options for manylinux. |  | ||||||
|           CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} |  | ||||||
|           CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} |  | ||||||
| 
 |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} |  | ||||||
|           path: ./wheelhouse/*.whl |  | ||||||
| 
 |  | ||||||
|   build-2-native-wheels: |  | ||||||
|     if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' |     if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' | ||||||
|     name: ${{ matrix.name }} |     name: ${{ matrix.name }} | ||||||
|     runs-on: ${{ matrix.os }} |     runs-on: ${{ matrix.os }} | ||||||
|  | @ -132,6 +77,14 @@ jobs: | ||||||
|             cibw_arch: x86_64 |             cibw_arch: x86_64 | ||||||
|             build: "*manylinux*" |             build: "*manylinux*" | ||||||
|             manylinux: "manylinux_2_28" |             manylinux: "manylinux_2_28" | ||||||
|  |           - name: "manylinux2014 and musllinux aarch64" | ||||||
|  |             os: ubuntu-24.04-arm | ||||||
|  |             cibw_arch: aarch64 | ||||||
|  |           - name: "manylinux_2_28 aarch64" | ||||||
|  |             os: ubuntu-24.04-arm | ||||||
|  |             cibw_arch: aarch64 | ||||||
|  |             build: "*manylinux*" | ||||||
|  |             manylinux: "manylinux_2_28" | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
|         with: |         with: | ||||||
|  | @ -152,7 +105,9 @@ jobs: | ||||||
|         env: |         env: | ||||||
|           CIBW_ARCHS: ${{ matrix.cibw_arch }} |           CIBW_ARCHS: ${{ matrix.cibw_arch }} | ||||||
|           CIBW_BUILD: ${{ matrix.build }} |           CIBW_BUILD: ${{ matrix.build }} | ||||||
|           CIBW_ENABLE: cpython-prerelease cpython-freethreading |           CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy | ||||||
|  |           CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux }} | ||||||
|  |           CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }} | ||||||
|           CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} |           CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} | ||||||
|           CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} |           CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} | ||||||
|           CIBW_SKIP: pp39-* |           CIBW_SKIP: pp39-* | ||||||
|  | @ -229,7 +184,7 @@ jobs: | ||||||
|           CIBW_ARCHS: ${{ matrix.cibw_arch }} |           CIBW_ARCHS: ${{ matrix.cibw_arch }} | ||||||
|           CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" |           CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" | ||||||
|           CIBW_CACHE_PATH: "C:\\cibw" |           CIBW_CACHE_PATH: "C:\\cibw" | ||||||
|           CIBW_ENABLE: cpython-prerelease cpython-freethreading |           CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy | ||||||
|           CIBW_SKIP: pp39-* |           CIBW_SKIP: pp39-* | ||||||
|           CIBW_TEST_SKIP: "*-win_arm64" |           CIBW_TEST_SKIP: "*-win_arm64" | ||||||
|           CIBW_TEST_COMMAND: 'docker run --rm |           CIBW_TEST_COMMAND: 'docker run --rm | ||||||
|  | @ -275,7 +230,7 @@ jobs: | ||||||
| 
 | 
 | ||||||
|   scientific-python-nightly-wheels-publish: |   scientific-python-nightly-wheels-publish: | ||||||
|     if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') |     if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') | ||||||
|     needs: [build-2-native-wheels, windows] |     needs: [build-native-wheels, windows] | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     name: Upload wheels to scientific-python-nightly-wheels |     name: Upload wheels to scientific-python-nightly-wheels | ||||||
|     steps: |     steps: | ||||||
|  | @ -292,7 +247,7 @@ jobs: | ||||||
| 
 | 
 | ||||||
|   pypi-publish: |   pypi-publish: | ||||||
|     if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') |     if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') | ||||||
|     needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] |     needs: [build-native-wheels, windows, sdist] | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     name: Upload release to PyPI |     name: Upload release to PyPI | ||||||
|     environment: |     environment: | ||||||
|  |  | ||||||
|  | @ -1,17 +1,17 @@ | ||||||
| repos: | repos: | ||||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit |   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||||
|     rev: v0.8.6 |     rev: v0.9.4 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: ruff |       - id: ruff | ||||||
|         args: [--exit-non-zero-on-fix] |         args: [--exit-non-zero-on-fix] | ||||||
| 
 | 
 | ||||||
|   - repo: https://github.com/psf/black-pre-commit-mirror |   - repo: https://github.com/psf/black-pre-commit-mirror | ||||||
|     rev: 24.10.0 |     rev: 25.1.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: black |       - id: black | ||||||
| 
 | 
 | ||||||
|   - repo: https://github.com/PyCQA/bandit |   - repo: https://github.com/PyCQA/bandit | ||||||
|     rev: 1.8.0 |     rev: 1.8.2 | ||||||
|     hooks: |     hooks: | ||||||
|     - id: bandit |     - id: bandit | ||||||
|       args: [--severity-level=high] |       args: [--severity-level=high] | ||||||
|  | @ -24,7 +24,7 @@ repos: | ||||||
|         exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) |         exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) | ||||||
| 
 | 
 | ||||||
|   - repo: https://github.com/pre-commit/mirrors-clang-format |   - repo: https://github.com/pre-commit/mirrors-clang-format | ||||||
|     rev: v19.1.6 |     rev: v19.1.7 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: clang-format |       - id: clang-format | ||||||
|         types: [c] |         types: [c] | ||||||
|  | @ -50,14 +50,14 @@ repos: | ||||||
|         exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ |         exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ | ||||||
| 
 | 
 | ||||||
|   - repo: https://github.com/python-jsonschema/check-jsonschema |   - repo: https://github.com/python-jsonschema/check-jsonschema | ||||||
|     rev: 0.30.0 |     rev: 0.31.1 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: check-github-workflows |       - id: check-github-workflows | ||||||
|       - id: check-readthedocs |       - id: check-readthedocs | ||||||
|       - id: check-renovate |       - id: check-renovate | ||||||
| 
 | 
 | ||||||
|   - repo: https://github.com/woodruffw/zizmor-pre-commit |   - repo: https://github.com/woodruffw/zizmor-pre-commit | ||||||
|     rev: v1.0.0 |     rev: v1.3.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: zizmor |       - id: zizmor | ||||||
| 
 | 
 | ||||||
|  | @ -78,7 +78,7 @@ repos: | ||||||
|         additional_dependencies: [trove-classifiers>=2024.10.12] |         additional_dependencies: [trove-classifiers>=2024.10.12] | ||||||
| 
 | 
 | ||||||
|   - repo: https://github.com/tox-dev/tox-ini-fmt |   - repo: https://github.com/tox-dev/tox-ini-fmt | ||||||
|     rev: 1.4.1 |     rev: 1.5.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: tox-ini-fmt |       - id: tox-ini-fmt | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,7 +20,6 @@ graft docs | ||||||
| graft _custom_build | graft _custom_build | ||||||
| 
 | 
 | ||||||
| # build/src control detritus | # build/src control detritus | ||||||
| exclude .appveyor.yml |  | ||||||
| exclude .clang-format | exclude .clang-format | ||||||
| exclude .coveragerc | exclude .coveragerc | ||||||
| exclude .editorconfig | exclude .editorconfig | ||||||
|  |  | ||||||
|  | @ -42,9 +42,6 @@ As of 2019, Pillow development is | ||||||
|             <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img |             <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img | ||||||
|                 alt="GitHub Actions build status (Test Docker)" |                 alt="GitHub Actions build status (Test Docker)" | ||||||
|                 src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a> |                 src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a> | ||||||
|             <a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img |  | ||||||
|                 alt="AppVeyor CI build status (Windows)" |  | ||||||
|                 src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a> |  | ||||||
|             <a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img |             <a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img | ||||||
|                 alt="GitHub Actions build status (Wheels)" |                 alt="GitHub Actions build status (Wheels)" | ||||||
|                 src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a> |                 src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a> | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. | ||||||
| 
 | 
 | ||||||
| * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 | * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 | ||||||
| * [ ] Develop and prepare release in `main` branch. | * [ ] Develop and prepare release in `main` branch. | ||||||
| * [ ] 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 [GitHub Actions](https://github.com/python-pillow/Pillow/actions) 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. | * [ ] 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` | * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` | ||||||
| * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. | * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. | ||||||
|  | @ -38,7 +38,7 @@ Released as needed for security, installation or critical bug fixes. | ||||||
|   git checkout -t remotes/origin/5.2.x |   git checkout -t remotes/origin/5.2.x | ||||||
|   ``` |   ``` | ||||||
| * [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`. | * [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`. | ||||||
| * [ ] 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 release branch e.g. `5.2.x`. | * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in release branch e.g. `5.2.x`. | ||||||
| * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` | * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` | ||||||
| * [ ] Run pre-release check via `make release-test`. | * [ ] Run pre-release check via `make release-test`. | ||||||
| * [ ] Create tag for release e.g.: | * [ ] Create tag for release e.g.: | ||||||
|  |  | ||||||
|  | @ -3,19 +3,18 @@ from __future__ import annotations | ||||||
| import zlib | import zlib | ||||||
| from io import BytesIO | from io import BytesIO | ||||||
| 
 | 
 | ||||||
|  | import pytest | ||||||
|  | 
 | ||||||
| from PIL import Image, ImageFile, PngImagePlugin | from PIL import Image, ImageFile, PngImagePlugin | ||||||
| 
 | 
 | ||||||
| TEST_FILE = "Tests/images/png_decompression_dos.png" | TEST_FILE = "Tests/images/png_decompression_dos.png" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_ignore_dos_text() -> None: | def test_ignore_dos_text(monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|     ImageFile.LOAD_TRUNCATED_IMAGES = True |     monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
| 
 | 
 | ||||||
|     try: |     with Image.open(TEST_FILE) as im: | ||||||
|         im = Image.open(TEST_FILE) |  | ||||||
|         im.load() |         im.load() | ||||||
|     finally: |  | ||||||
|         ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
| 
 | 
 | ||||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         for s in im.text.values(): |         for s in im.text.values(): | ||||||
|  |  | ||||||
|  | @ -320,16 +320,7 @@ def magick_command() -> list[str] | None: | ||||||
|     return None |     return None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def on_appveyor() -> bool: |  | ||||||
|     return "APPVEYOR" in os.environ |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def on_github_actions() -> bool: |  | ||||||
|     return "GITHUB_ACTIONS" in os.environ |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def on_ci() -> bool: | def on_ci() -> bool: | ||||||
|     # GitHub Actions and AppVeyor have "CI" |  | ||||||
|     return "CI" in os.environ |     return "CI" in os.environ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								Tests/images/multiline_text_justify.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/multiline_text_justify.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.2 KiB | 
|  | @ -19,7 +19,7 @@ except ImportError: | ||||||
| class TestColorLut3DCoreAPI: | class TestColorLut3DCoreAPI: | ||||||
|     def generate_identity_table( |     def generate_identity_table( | ||||||
|         self, channels: int, size: int | tuple[int, int, int] |         self, channels: int, size: int | tuple[int, int, int] | ||||||
|     ) -> tuple[int, int, int, int, list[float]]: |     ) -> tuple[int, tuple[int, int, int], list[float]]: | ||||||
|         if isinstance(size, tuple): |         if isinstance(size, tuple): | ||||||
|             size_1d, size_2d, size_3d = size |             size_1d, size_2d, size_3d = size | ||||||
|         else: |         else: | ||||||
|  | @ -39,9 +39,7 @@ class TestColorLut3DCoreAPI: | ||||||
|         ] |         ] | ||||||
|         return ( |         return ( | ||||||
|             channels, |             channels, | ||||||
|             size_1d, |             (size_1d, size_2d, size_3d), | ||||||
|             size_2d, |  | ||||||
|             size_3d, |  | ||||||
|             [item for sublist in table for item in sublist], |             [item for sublist in table for item in sublist], | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  | @ -89,21 +87,21 @@ class TestColorLut3DCoreAPI: | ||||||
| 
 | 
 | ||||||
|         with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): |         with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): | ||||||
|             im.im.color_lut_3d( |             im.im.color_lut_3d( | ||||||
|                 "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7 |                 "RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, 0] * 7 | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): |         with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): | ||||||
|             im.im.color_lut_3d( |             im.im.color_lut_3d( | ||||||
|                 "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9 |                 "RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, 0] * 9 | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         with pytest.raises(TypeError): |         with pytest.raises(TypeError): | ||||||
|             im.im.color_lut_3d( |             im.im.color_lut_3d( | ||||||
|                 "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8 |                 "RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, "0"] * 8 | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         with pytest.raises(TypeError): |         with pytest.raises(TypeError): | ||||||
|             im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) |             im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), 16) | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize( |     @pytest.mark.parametrize( | ||||||
|         "lut_mode, table_channels, table_size", |         "lut_mode, table_channels, table_size", | ||||||
|  | @ -264,7 +262,7 @@ class TestColorLut3DCoreAPI: | ||||||
|         assert_image_equal( |         assert_image_equal( | ||||||
|             Image.merge('RGB', im.split()[::-1]), |             Image.merge('RGB', im.split()[::-1]), | ||||||
|             im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, |             im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, | ||||||
|                     3, 2, 2, 2, [ |                     3, (2, 2, 2), [ | ||||||
|                         0, 0, 0,  0, 0, 1, |                         0, 0, 0,  0, 0, 1, | ||||||
|                         0, 1, 0,  0, 1, 1, |                         0, 1, 0,  0, 1, 1, | ||||||
| 
 | 
 | ||||||
|  | @ -286,7 +284,7 @@ class TestColorLut3DCoreAPI: | ||||||
| 
 | 
 | ||||||
|         # fmt: off |         # fmt: off | ||||||
|         transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, |         transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, | ||||||
|                               3, 2, 2, 2, |                               3, (2, 2, 2), | ||||||
|                               [ |                               [ | ||||||
|                                   -1, -1, -1,   2, -1, -1, |                                   -1, -1, -1,   2, -1, -1, | ||||||
|                                   -1,  2, -1,   2,  2, -1, |                                   -1,  2, -1,   2,  2, -1, | ||||||
|  | @ -307,7 +305,7 @@ class TestColorLut3DCoreAPI: | ||||||
| 
 | 
 | ||||||
|         # fmt: off |         # fmt: off | ||||||
|         transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, |         transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, | ||||||
|                               3, 2, 2, 2, |                               3, (2, 2, 2), | ||||||
|                               [ |                               [ | ||||||
|                                   -3, -3, -3,   5, -3, -3, |                                   -3, -3, -3,   5, -3, -3, | ||||||
|                                   -3,  5, -3,   5,  5, -3, |                                   -3,  5, -3,   5,  5, -3, | ||||||
|  |  | ||||||
|  | @ -12,19 +12,16 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestDecompressionBomb: | class TestDecompressionBomb: | ||||||
|     def teardown_method(self) -> None: |  | ||||||
|         Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT |  | ||||||
| 
 |  | ||||||
|     def test_no_warning_small_file(self) -> None: |     def test_no_warning_small_file(self) -> None: | ||||||
|         # Implicit assert: no warning. |         # Implicit assert: no warning. | ||||||
|         # A warning would cause a failure. |         # A warning would cause a failure. | ||||||
|         with Image.open(TEST_FILE): |         with Image.open(TEST_FILE): | ||||||
|             pass |             pass | ||||||
| 
 | 
 | ||||||
|     def test_no_warning_no_limit(self) -> None: |     def test_no_warning_no_limit(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         # Arrange |         # Arrange | ||||||
|         # Turn limit off |         # Turn limit off | ||||||
|         Image.MAX_IMAGE_PIXELS = None |         monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None) | ||||||
|         assert Image.MAX_IMAGE_PIXELS is None |         assert Image.MAX_IMAGE_PIXELS is None | ||||||
| 
 | 
 | ||||||
|         # Act / Assert |         # Act / Assert | ||||||
|  | @ -33,18 +30,18 @@ class TestDecompressionBomb: | ||||||
|         with Image.open(TEST_FILE): |         with Image.open(TEST_FILE): | ||||||
|             pass |             pass | ||||||
| 
 | 
 | ||||||
|     def test_warning(self) -> None: |     def test_warning(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         # Set limit to trigger warning on the test file |         # Set limit to trigger warning on the test file | ||||||
|         Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 |         monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 128 * 128 - 1) | ||||||
|         assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 |         assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 | ||||||
| 
 | 
 | ||||||
|         with pytest.warns(Image.DecompressionBombWarning): |         with pytest.warns(Image.DecompressionBombWarning): | ||||||
|             with Image.open(TEST_FILE): |             with Image.open(TEST_FILE): | ||||||
|                 pass |                 pass | ||||||
| 
 | 
 | ||||||
|     def test_exception(self) -> None: |     def test_exception(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         # Set limit to trigger exception on the test file |         # Set limit to trigger exception on the test file | ||||||
|         Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 |         monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 64 * 128 - 1) | ||||||
|         assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1 |         assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1 | ||||||
| 
 | 
 | ||||||
|         with pytest.raises(Image.DecompressionBombError): |         with pytest.raises(Image.DecompressionBombError): | ||||||
|  | @ -66,9 +63,9 @@ class TestDecompressionBomb: | ||||||
|             with pytest.raises(Image.DecompressionBombError): |             with pytest.raises(Image.DecompressionBombError): | ||||||
|                 im.seek(1) |                 im.seek(1) | ||||||
| 
 | 
 | ||||||
|     def test_exception_gif_zero_width(self) -> None: |     def test_exception_gif_zero_width(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         # Set limit to trigger exception on the test file |         # Set limit to trigger exception on the test file | ||||||
|         Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 |         monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 4 * 64 * 128) | ||||||
|         assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 |         assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 | ||||||
| 
 | 
 | ||||||
|         with pytest.raises(Image.DecompressionBombError): |         with pytest.raises(Image.DecompressionBombError): | ||||||
|  |  | ||||||
|  | @ -35,9 +35,8 @@ def test_sanity() -> None: | ||||||
|         assert im.is_animated |         assert im.is_animated | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_prefix_chunk() -> None: | def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|     ImageFile.LOAD_TRUNCATED_IMAGES = True |     monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|     try: |  | ||||||
|     with Image.open(animated_test_file_with_prefix_chunk) as im: |     with Image.open(animated_test_file_with_prefix_chunk) as im: | ||||||
|         assert im.mode == "P" |         assert im.mode == "P" | ||||||
|         assert im.size == (320, 200) |         assert im.size == (320, 200) | ||||||
|  | @ -49,8 +48,6 @@ def test_prefix_chunk() -> None: | ||||||
|         assert palette[3:6] == [255, 255, 255] |         assert palette[3:6] == [255, 255, 255] | ||||||
|         assert palette[381:384] == [204, 204, 12] |         assert palette[381:384] == [204, 204, 12] | ||||||
|         assert palette[765:] == [252, 0, 0] |         assert palette[765:] == [252, 0, 0] | ||||||
|     finally: |  | ||||||
|         ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.skipif(is_pypy(), reason="Requires CPython") | @pytest.mark.skipif(is_pypy(), reason="Requires CPython") | ||||||
|  |  | ||||||
|  | @ -86,12 +86,12 @@ def test_invalid_file() -> None: | ||||||
| def test_l_mode_transparency() -> None: | def test_l_mode_transparency() -> None: | ||||||
|     with Image.open("Tests/images/no_palette_with_transparency.gif") as im: |     with Image.open("Tests/images/no_palette_with_transparency.gif") as im: | ||||||
|         assert im.mode == "L" |         assert im.mode == "L" | ||||||
|         assert im.load()[0, 0] == 128 |         assert im.getpixel((0, 0)) == 128 | ||||||
|         assert im.info["transparency"] == 255 |         assert im.info["transparency"] == 255 | ||||||
| 
 | 
 | ||||||
|         im.seek(1) |         im.seek(1) | ||||||
|         assert im.mode == "L" |         assert im.mode == "L" | ||||||
|         assert im.load()[0, 0] == 128 |         assert im.getpixel((0, 0)) == 128 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_l_mode_after_rgb() -> None: | def test_l_mode_after_rgb() -> None: | ||||||
|  | @ -109,7 +109,7 @@ def test_palette_not_needed_for_second_frame() -> None: | ||||||
|         assert_image_similar(im, hopper("L").convert("RGB"), 8) |         assert_image_similar(im, hopper("L").convert("RGB"), 8) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_strategy() -> None: | def test_strategy(monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|     with Image.open("Tests/images/iss634.gif") as im: |     with Image.open("Tests/images/iss634.gif") as im: | ||||||
|         expected_rgb_always = im.convert("RGB") |         expected_rgb_always = im.convert("RGB") | ||||||
| 
 | 
 | ||||||
|  | @ -119,8 +119,9 @@ def test_strategy() -> None: | ||||||
|         im.seek(1) |         im.seek(1) | ||||||
|         expected_different = im.convert("RGB") |         expected_different = im.convert("RGB") | ||||||
| 
 | 
 | ||||||
|     try: |     monkeypatch.setattr( | ||||||
|         GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS |         GifImagePlugin, "LOADING_STRATEGY", GifImagePlugin.LoadingStrategy.RGB_ALWAYS | ||||||
|  |     ) | ||||||
|     with Image.open("Tests/images/iss634.gif") as im: |     with Image.open("Tests/images/iss634.gif") as im: | ||||||
|         assert im.mode == "RGB" |         assert im.mode == "RGB" | ||||||
|         assert_image_equal(im, expected_rgb_always) |         assert_image_equal(im, expected_rgb_always) | ||||||
|  | @ -129,8 +130,10 @@ def test_strategy() -> None: | ||||||
|         assert im.mode == "RGBA" |         assert im.mode == "RGBA" | ||||||
|         assert_image_equal(im, expected_rgb_always_rgba) |         assert_image_equal(im, expected_rgb_always_rgba) | ||||||
| 
 | 
 | ||||||
|         GifImagePlugin.LOADING_STRATEGY = ( |     monkeypatch.setattr( | ||||||
|             GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY |         GifImagePlugin, | ||||||
|  |         "LOADING_STRATEGY", | ||||||
|  |         GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY, | ||||||
|     ) |     ) | ||||||
|     # Stay in P mode with only a global palette |     # Stay in P mode with only a global palette | ||||||
|     with Image.open("Tests/images/chi.gif") as im: |     with Image.open("Tests/images/chi.gif") as im: | ||||||
|  | @ -146,8 +149,6 @@ def test_strategy() -> None: | ||||||
| 
 | 
 | ||||||
|         im.seek(1) |         im.seek(1) | ||||||
|         assert im.mode == "RGB" |         assert im.mode == "RGB" | ||||||
|     finally: |  | ||||||
|         GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_optimize() -> None: | def test_optimize() -> None: | ||||||
|  | @ -310,7 +311,7 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None: | ||||||
|     with Image.open(path) as im: |     with Image.open(path) as im: | ||||||
|         assert im.mode == "P" |         assert im.mode == "P" | ||||||
|         first_frame_colors = im.palette.colors.keys() |         first_frame_colors = im.palette.colors.keys() | ||||||
|         original_color = im.convert("RGB").load()[0, 0] |         original_color = im.convert("RGB").getpixel((0, 0)) | ||||||
| 
 | 
 | ||||||
|         im.seek(1) |         im.seek(1) | ||||||
|         assert im.mode == mode |         assert im.mode == mode | ||||||
|  | @ -318,10 +319,10 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None: | ||||||
|             im = im.convert("RGB") |             im = im.convert("RGB") | ||||||
| 
 | 
 | ||||||
|         # Check a color only from the old palette |         # Check a color only from the old palette | ||||||
|         assert im.load()[0, 0] == original_color |         assert im.getpixel((0, 0)) == original_color | ||||||
| 
 | 
 | ||||||
|         # Check a color from the new palette |         # Check a color from the new palette | ||||||
|         assert im.load()[24, 24] not in first_frame_colors |         assert im.getpixel((24, 24)) not in first_frame_colors | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: | def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: | ||||||
|  | @ -487,8 +488,7 @@ def test_eoferror() -> None: | ||||||
| 
 | 
 | ||||||
| def test_first_frame_transparency() -> None: | def test_first_frame_transparency() -> None: | ||||||
|     with Image.open("Tests/images/first_frame_transparency.gif") as im: |     with Image.open("Tests/images/first_frame_transparency.gif") as im: | ||||||
|         px = im.load() |         assert im.getpixel((0, 0)) == im.info["transparency"] | ||||||
|         assert px[0, 0] == im.info["transparency"] |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_dispose_none() -> None: | def test_dispose_none() -> None: | ||||||
|  | @ -555,17 +555,15 @@ def test_dispose_background_transparency() -> None: | ||||||
| def test_transparent_dispose( | def test_transparent_dispose( | ||||||
|     loading_strategy: GifImagePlugin.LoadingStrategy, |     loading_strategy: GifImagePlugin.LoadingStrategy, | ||||||
|     expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]], |     expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]], | ||||||
|  |     monkeypatch: pytest.MonkeyPatch, | ||||||
| ) -> None: | ) -> None: | ||||||
|     GifImagePlugin.LOADING_STRATEGY = loading_strategy |     monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) | ||||||
|     try: |  | ||||||
|     with Image.open("Tests/images/transparent_dispose.gif") as img: |     with Image.open("Tests/images/transparent_dispose.gif") as img: | ||||||
|         for frame in range(3): |         for frame in range(3): | ||||||
|             img.seek(frame) |             img.seek(frame) | ||||||
|             for x in range(3): |             for x in range(3): | ||||||
|                 color = img.getpixel((x, 0)) |                 color = img.getpixel((x, 0)) | ||||||
|                 assert color == expected_colors[frame][x] |                 assert color == expected_colors[frame][x] | ||||||
|     finally: |  | ||||||
|         GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_dispose_previous() -> None: | def test_dispose_previous() -> None: | ||||||
|  | @ -1398,10 +1396,11 @@ def test_lzw_bits() -> None: | ||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
| def test_extents( | def test_extents( | ||||||
|     test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy |     test_file: str, | ||||||
|  |     loading_strategy: GifImagePlugin.LoadingStrategy, | ||||||
|  |     monkeypatch: pytest.MonkeyPatch, | ||||||
| ) -> None: | ) -> None: | ||||||
|     GifImagePlugin.LOADING_STRATEGY = loading_strategy |     monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) | ||||||
|     try: |  | ||||||
|     with Image.open("Tests/images/" + test_file) as im: |     with Image.open("Tests/images/" + test_file) as im: | ||||||
|         assert im.size == (100, 100) |         assert im.size == (100, 100) | ||||||
| 
 | 
 | ||||||
|  | @ -1414,8 +1413,6 @@ def test_extents( | ||||||
| 
 | 
 | ||||||
|         im.load() |         im.load() | ||||||
|         assert im.im.size == (150, 150) |         assert im.im.size == (150, 150) | ||||||
|     finally: |  | ||||||
|         GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_missing_background() -> None: | def test_missing_background() -> None: | ||||||
|  |  | ||||||
|  | @ -243,15 +243,14 @@ def test_draw_reloaded(tmp_path: Path) -> None: | ||||||
|         assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") |         assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_truncated_mask() -> None: | def test_truncated_mask(monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|     # 1 bpp |     # 1 bpp | ||||||
|     with open("Tests/images/hopper_mask.ico", "rb") as fp: |     with open("Tests/images/hopper_mask.ico", "rb") as fp: | ||||||
|         data = fp.read() |         data = fp.read() | ||||||
| 
 | 
 | ||||||
|     ImageFile.LOAD_TRUNCATED_IMAGES = True |     monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|     data = data[:-3] |     data = data[:-3] | ||||||
| 
 | 
 | ||||||
|     try: |  | ||||||
|     with Image.open(io.BytesIO(data)) as im: |     with Image.open(io.BytesIO(data)) as im: | ||||||
|         assert im.mode == "1" |         assert im.mode == "1" | ||||||
| 
 | 
 | ||||||
|  | @ -264,5 +263,3 @@ def test_truncated_mask() -> None: | ||||||
| 
 | 
 | ||||||
|     with Image.open(io.BytesIO(data)) as im: |     with Image.open(io.BytesIO(data)) as im: | ||||||
|         assert im.mode == "RGB" |         assert im.mode == "RGB" | ||||||
|     finally: |  | ||||||
|         ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
|  |  | ||||||
|  | @ -530,12 +530,13 @@ class TestFileJpeg: | ||||||
|     @mark_if_feature_version( |     @mark_if_feature_version( | ||||||
|         pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" |         pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" | ||||||
|     ) |     ) | ||||||
|     def test_truncated_jpeg_should_read_all_the_data(self) -> None: |     def test_truncated_jpeg_should_read_all_the_data( | ||||||
|  |         self, monkeypatch: pytest.MonkeyPatch | ||||||
|  |     ) -> None: | ||||||
|         filename = "Tests/images/truncated_jpeg.jpg" |         filename = "Tests/images/truncated_jpeg.jpg" | ||||||
|         ImageFile.LOAD_TRUNCATED_IMAGES = True |         monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|         with Image.open(filename) as im: |         with Image.open(filename) as im: | ||||||
|             im.load() |             im.load() | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
|             assert im.getbbox() is not None |             assert im.getbbox() is not None | ||||||
| 
 | 
 | ||||||
|     def test_truncated_jpeg_throws_oserror(self) -> None: |     def test_truncated_jpeg_throws_oserror(self) -> None: | ||||||
|  | @ -933,7 +934,7 @@ class TestFileJpeg: | ||||||
| 
 | 
 | ||||||
|     def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None: |     def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         size = 4097 |         size = 4097 | ||||||
|         buffer = BytesIO(b"\xFF" * size)  # Many xFF bytes |         buffer = BytesIO(b"\xff" * size)  # Many xff bytes | ||||||
|         max_pos = 0 |         max_pos = 0 | ||||||
|         orig_read = buffer.read |         orig_read = buffer.read | ||||||
| 
 | 
 | ||||||
|  | @ -1024,7 +1025,7 @@ class TestFileJpeg: | ||||||
|             im.save(f, xmp=b"1" * 65505) |             im.save(f, xmp=b"1" * 65505) | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.timeout(timeout=1) |     @pytest.mark.timeout(timeout=1) | ||||||
|     def test_eof(self) -> None: |     def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         # Even though this decoder never says that it is finished |         # Even though this decoder never says that it is finished | ||||||
|         # the image should still end when there is no new data |         # the image should still end when there is no new data | ||||||
|         class InfiniteMockPyDecoder(ImageFile.PyDecoder): |         class InfiniteMockPyDecoder(ImageFile.PyDecoder): | ||||||
|  | @ -1039,9 +1040,8 @@ class TestFileJpeg: | ||||||
|             im.tile = [ |             im.tile = [ | ||||||
|                 ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), |                 ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), | ||||||
|             ] |             ] | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = True |             monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|             im.load() |             im.load() | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
| 
 | 
 | ||||||
|     def test_separate_tables(self) -> None: |     def test_separate_tables(self) -> None: | ||||||
|         im = hopper() |         im = hopper() | ||||||
|  |  | ||||||
|  | @ -181,14 +181,11 @@ def test_load_dpi() -> None: | ||||||
|         assert "dpi" not in im.info |         assert "dpi" not in im.info | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_restricted_icc_profile() -> None: | def test_restricted_icc_profile(monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|     ImageFile.LOAD_TRUNCATED_IMAGES = True |     monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|     try: |  | ||||||
|     # JPEG2000 image with a restricted ICC profile and a known colorspace |     # JPEG2000 image with a restricted ICC profile and a known colorspace | ||||||
|     with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im: |     with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im: | ||||||
|         assert im.mode == "RGB" |         assert im.mode == "RGB" | ||||||
|     finally: |  | ||||||
|         ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.skipif( | @pytest.mark.skipif( | ||||||
|  |  | ||||||
|  | @ -309,7 +309,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         def check_tags( |         def check_tags( | ||||||
|             tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] |             tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str], | ||||||
|         ) -> None: |         ) -> None: | ||||||
|             im = hopper() |             im = hopper() | ||||||
| 
 | 
 | ||||||
|  | @ -1103,11 +1103,13 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|     ) |     ) | ||||||
|     def test_buffering(self, test_file: str) -> None: |     def test_buffering(self, test_file: str) -> None: | ||||||
|         # load exif first |         # load exif first | ||||||
|         with Image.open(open(test_file, "rb", buffering=1048576)) as im: |         with open(test_file, "rb", buffering=1048576) as f: | ||||||
|  |             with Image.open(f) as im: | ||||||
|                 exif = dict(im.getexif()) |                 exif = dict(im.getexif()) | ||||||
| 
 | 
 | ||||||
|         # load image before exif |         # load image before exif | ||||||
|         with Image.open(open(test_file, "rb", buffering=1048576)) as im2: |         with open(test_file, "rb", buffering=1048576) as f: | ||||||
|  |             with Image.open(f) as im2: | ||||||
|                 im2.load() |                 im2.load() | ||||||
|                 exif_after_load = dict(im2.getexif()) |                 exif_after_load = dict(im2.getexif()) | ||||||
| 
 | 
 | ||||||
|  | @ -1156,13 +1158,14 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|             assert len(im.tag_v2[STRIPOFFSETS]) > 1 |             assert len(im.tag_v2[STRIPOFFSETS]) > 1 | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("argument", (True, False)) |     @pytest.mark.parametrize("argument", (True, False)) | ||||||
|     def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None: |     def test_save_single_strip( | ||||||
|  |         self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch | ||||||
|  |     ) -> None: | ||||||
|         im = hopper("RGB").resize((256, 256)) |         im = hopper("RGB").resize((256, 256)) | ||||||
|         out = str(tmp_path / "temp.tif") |         out = str(tmp_path / "temp.tif") | ||||||
| 
 | 
 | ||||||
|         if not argument: |         if not argument: | ||||||
|             TiffImagePlugin.STRIP_SIZE = 2**18 |             monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18) | ||||||
|         try: |  | ||||||
|         arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} |         arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} | ||||||
|         if argument: |         if argument: | ||||||
|             arguments["strip_size"] = 2**18 |             arguments["strip_size"] = 2**18 | ||||||
|  | @ -1171,8 +1174,6 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         with Image.open(out) as im: |         with Image.open(out) as im: | ||||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert len(im.tag_v2[STRIPOFFSETS]) == 1 |             assert len(im.tag_v2[STRIPOFFSETS]) == 1 | ||||||
|         finally: |  | ||||||
|             TiffImagePlugin.STRIP_SIZE = 65536 |  | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) |     @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) | ||||||
|     def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: |     def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: | ||||||
|  |  | ||||||
|  | @ -264,7 +264,7 @@ def test_pdf_append(tmp_path: Path) -> None: | ||||||
|         # append some info |         # append some info | ||||||
|         pdf.info.Title = "abc" |         pdf.info.Title = "abc" | ||||||
|         pdf.info.Author = "def" |         pdf.info.Author = "def" | ||||||
|         pdf.info.Subject = "ghi\uABCD" |         pdf.info.Subject = "ghi\uabcd" | ||||||
|         pdf.info.Keywords = "qw)e\\r(ty" |         pdf.info.Keywords = "qw)e\\r(ty" | ||||||
|         pdf.info.Creator = "hopper()" |         pdf.info.Creator = "hopper()" | ||||||
|         pdf.start_writing() |         pdf.start_writing() | ||||||
|  | @ -292,7 +292,7 @@ def test_pdf_append(tmp_path: Path) -> None: | ||||||
|         assert pdf.info.Title == "abc" |         assert pdf.info.Title == "abc" | ||||||
|         assert pdf.info.Producer == "PdfParser" |         assert pdf.info.Producer == "PdfParser" | ||||||
|         assert pdf.info.Keywords == "qw)e\\r(ty" |         assert pdf.info.Keywords == "qw)e\\r(ty" | ||||||
|         assert pdf.info.Subject == "ghi\uABCD" |         assert pdf.info.Subject == "ghi\uabcd" | ||||||
|         assert b"CreationDate" in pdf.info |         assert b"CreationDate" in pdf.info | ||||||
|         assert b"ModDate" in pdf.info |         assert b"ModDate" in pdf.info | ||||||
|         check_pdf_pages_consistency(pdf) |         check_pdf_pages_consistency(pdf) | ||||||
|  |  | ||||||
|  | @ -363,7 +363,7 @@ class TestFilePng: | ||||||
|                 with pytest.raises((OSError, SyntaxError)): |                 with pytest.raises((OSError, SyntaxError)): | ||||||
|                     im.verify() |                     im.verify() | ||||||
| 
 | 
 | ||||||
|     def test_verify_ignores_crc_error(self) -> None: |     def test_verify_ignores_crc_error(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         # check ignores crc errors in ancillary chunks |         # check ignores crc errors in ancillary chunks | ||||||
| 
 | 
 | ||||||
|         chunk_data = chunk(b"tEXt", b"spam") |         chunk_data = chunk(b"tEXt", b"spam") | ||||||
|  | @ -373,24 +373,20 @@ class TestFilePng: | ||||||
|         with pytest.raises(SyntaxError): |         with pytest.raises(SyntaxError): | ||||||
|             PngImagePlugin.PngImageFile(BytesIO(image_data)) |             PngImagePlugin.PngImageFile(BytesIO(image_data)) | ||||||
| 
 | 
 | ||||||
|         ImageFile.LOAD_TRUNCATED_IMAGES = True |         monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|         try: |  | ||||||
|         im = load(image_data) |         im = load(image_data) | ||||||
|         assert im is not None |         assert im is not None | ||||||
|         finally: |  | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
| 
 | 
 | ||||||
|     def test_verify_not_ignores_crc_error_in_required_chunk(self) -> None: |     def test_verify_not_ignores_crc_error_in_required_chunk( | ||||||
|  |         self, monkeypatch: pytest.MonkeyPatch | ||||||
|  |     ) -> None: | ||||||
|         # check does not ignore crc errors in required chunks |         # check does not ignore crc errors in required chunks | ||||||
| 
 | 
 | ||||||
|         image_data = MAGIC + IHDR[:-1] + b"q" + TAIL |         image_data = MAGIC + IHDR[:-1] + b"q" + TAIL | ||||||
| 
 | 
 | ||||||
|         ImageFile.LOAD_TRUNCATED_IMAGES = True |         monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|         try: |  | ||||||
|         with pytest.raises(SyntaxError): |         with pytest.raises(SyntaxError): | ||||||
|             PngImagePlugin.PngImageFile(BytesIO(image_data)) |             PngImagePlugin.PngImageFile(BytesIO(image_data)) | ||||||
|         finally: |  | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
| 
 | 
 | ||||||
|     def test_roundtrip_dpi(self) -> None: |     def test_roundtrip_dpi(self) -> None: | ||||||
|         # Check dpi roundtripping |         # Check dpi roundtripping | ||||||
|  | @ -600,7 +596,7 @@ class TestFilePng: | ||||||
|             (b"prIV", b"VALUE3", True), |             (b"prIV", b"VALUE3", True), | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|     def test_textual_chunks_after_idat(self) -> None: |     def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         with Image.open("Tests/images/hopper.png") as im: |         with Image.open("Tests/images/hopper.png") as im: | ||||||
|             assert "comment" in im.text |             assert "comment" in im.text | ||||||
|             for k, v in { |             for k, v in { | ||||||
|  | @ -614,18 +610,17 @@ class TestFilePng: | ||||||
|             with pytest.raises(OSError): |             with pytest.raises(OSError): | ||||||
|                 assert isinstance(im.text, dict) |                 assert isinstance(im.text, dict) | ||||||
| 
 | 
 | ||||||
|  |         # Raises an EOFError in load_end | ||||||
|  |         with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: | ||||||
|  |             assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} | ||||||
|  | 
 | ||||||
|         # Raises a UnicodeDecodeError in load_end |         # Raises a UnicodeDecodeError in load_end | ||||||
|         with Image.open("Tests/images/truncated_image.png") as im: |         with Image.open("Tests/images/truncated_image.png") as im: | ||||||
|             # The file is truncated |             # The file is truncated | ||||||
|             with pytest.raises(OSError): |             with pytest.raises(OSError): | ||||||
|                 im.text |                 im.text | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = True |             monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|             assert isinstance(im.text, dict) |             assert isinstance(im.text, dict) | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
| 
 |  | ||||||
|         # Raises an EOFError in load_end |  | ||||||
|         with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: |  | ||||||
|             assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} |  | ||||||
| 
 | 
 | ||||||
|     def test_unknown_compression_method(self) -> None: |     def test_unknown_compression_method(self) -> None: | ||||||
|         with pytest.raises(SyntaxError, match="Unknown compression method"): |         with pytest.raises(SyntaxError, match="Unknown compression method"): | ||||||
|  | @ -651,15 +646,16 @@ class TestFilePng: | ||||||
|     @pytest.mark.parametrize( |     @pytest.mark.parametrize( | ||||||
|         "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") |         "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") | ||||||
|     ) |     ) | ||||||
|     def test_truncated_chunks(self, cid: bytes) -> None: |     def test_truncated_chunks( | ||||||
|  |         self, cid: bytes, monkeypatch: pytest.MonkeyPatch | ||||||
|  |     ) -> None: | ||||||
|         fp = BytesIO() |         fp = BytesIO() | ||||||
|         with PngImagePlugin.PngStream(fp) as png: |         with PngImagePlugin.PngStream(fp) as png: | ||||||
|             with pytest.raises(ValueError): |             with pytest.raises(ValueError): | ||||||
|                 png.call(cid, 0, 0) |                 png.call(cid, 0, 0) | ||||||
| 
 | 
 | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = True |             monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|             png.call(cid, 0, 0) |             png.call(cid, 0, 0) | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("save_all", (True, False)) |     @pytest.mark.parametrize("save_all", (True, False)) | ||||||
|     def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None: |     def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None: | ||||||
|  | @ -789,17 +785,14 @@ class TestFilePng: | ||||||
|         with Image.open(mystdout) as reloaded: |         with Image.open(mystdout) as reloaded: | ||||||
|             assert_image_equal_tofile(reloaded, TEST_PNG_FILE) |             assert_image_equal_tofile(reloaded, TEST_PNG_FILE) | ||||||
| 
 | 
 | ||||||
|     def test_truncated_end_chunk(self) -> None: |     def test_truncated_end_chunk(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         with Image.open("Tests/images/truncated_end_chunk.png") as im: |         with Image.open("Tests/images/truncated_end_chunk.png") as im: | ||||||
|             with pytest.raises(OSError): |             with pytest.raises(OSError): | ||||||
|                 im.load() |                 im.load() | ||||||
| 
 | 
 | ||||||
|         ImageFile.LOAD_TRUNCATED_IMAGES = True |         monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|         try: |  | ||||||
|         with Image.open("Tests/images/truncated_end_chunk.png") as im: |         with Image.open("Tests/images/truncated_end_chunk.png") as im: | ||||||
|             assert_image_equal_tofile(im, "Tests/images/hopper.png") |             assert_image_equal_tofile(im, "Tests/images/hopper.png") | ||||||
|         finally: |  | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") | @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") | ||||||
|  | @ -808,11 +801,11 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): | ||||||
|     mem_limit = 2 * 1024  # max increase in K |     mem_limit = 2 * 1024  # max increase in K | ||||||
|     iterations = 100  # Leak is 56k/iteration, this will leak 5.6megs |     iterations = 100  # Leak is 56k/iteration, this will leak 5.6megs | ||||||
| 
 | 
 | ||||||
|     def test_leak_load(self) -> None: |     def test_leak_load(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         with open("Tests/images/hopper.png", "rb") as f: |         with open("Tests/images/hopper.png", "rb") as f: | ||||||
|             DATA = BytesIO(f.read(16 * 1024)) |             DATA = BytesIO(f.read(16 * 1024)) | ||||||
| 
 | 
 | ||||||
|         ImageFile.LOAD_TRUNCATED_IMAGES = True |         monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|         with Image.open(DATA) as im: |         with Image.open(DATA) as im: | ||||||
|             im.load() |             im.load() | ||||||
| 
 | 
 | ||||||
|  | @ -820,7 +813,4 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): | ||||||
|             with Image.open(DATA) as im: |             with Image.open(DATA) as im: | ||||||
|                 im.load() |                 im.load() | ||||||
| 
 | 
 | ||||||
|         try: |  | ||||||
|         self._test_leak(core) |         self._test_leak(core) | ||||||
|         finally: |  | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
|  |  | ||||||
|  | @ -49,7 +49,7 @@ def test_sanity() -> None: | ||||||
|         (b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)), |         (b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)), | ||||||
|         # P6 with maxval < 255 |         # P6 with maxval < 255 | ||||||
|         ( |         ( | ||||||
|             b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11", |             b"P6 3 1 17 \x00\x01\x02\x08\x09\x0a\x0f\x10\x11", | ||||||
|             "RGB", |             "RGB", | ||||||
|             ( |             ( | ||||||
|                 (0, 15, 30), |                 (0, 15, 30), | ||||||
|  | @ -60,7 +60,7 @@ def test_sanity() -> None: | ||||||
|         # P6 with maxval > 255 |         # P6 with maxval > 255 | ||||||
|         ( |         ( | ||||||
|             b"P6 3 1 257 \x00\x00\x00\x01\x00\x02" |             b"P6 3 1 257 \x00\x00\x00\x01\x00\x02" | ||||||
|             b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF", |             b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xff\xff", | ||||||
|             "RGB", |             "RGB", | ||||||
|             ( |             ( | ||||||
|                 (0, 1, 2), |                 (0, 1, 2), | ||||||
|  |  | ||||||
|  | @ -746,7 +746,7 @@ class TestFileTiff: | ||||||
|             assert reread.n_frames == 3 |             assert reread.n_frames == 3 | ||||||
| 
 | 
 | ||||||
|     def test_fixoffsets(self) -> None: |     def test_fixoffsets(self) -> None: | ||||||
|         b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00") |         b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") | ||||||
|         with TiffImagePlugin.AppendingTiffWriter(b) as a: |         with TiffImagePlugin.AppendingTiffWriter(b) as a: | ||||||
|             b.seek(0) |             b.seek(0) | ||||||
|             a.fixOffsets(1, isShort=True) |             a.fixOffsets(1, isShort=True) | ||||||
|  | @ -759,14 +759,14 @@ class TestFileTiff: | ||||||
|             with pytest.raises(RuntimeError): |             with pytest.raises(RuntimeError): | ||||||
|                 a.fixOffsets(1) |                 a.fixOffsets(1) | ||||||
| 
 | 
 | ||||||
|         b = BytesIO(b"II\x2A\x00\x00\x00\x00\x00") |         b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00") | ||||||
|         with TiffImagePlugin.AppendingTiffWriter(b) as a: |         with TiffImagePlugin.AppendingTiffWriter(b) as a: | ||||||
|             a.offsetOfNewPage = 2**16 |             a.offsetOfNewPage = 2**16 | ||||||
| 
 | 
 | ||||||
|             b.seek(0) |             b.seek(0) | ||||||
|             a.fixOffsets(1, isShort=True) |             a.fixOffsets(1, isShort=True) | ||||||
| 
 | 
 | ||||||
|         b = BytesIO(b"II\x2B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") |         b = BytesIO(b"II\x2b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") | ||||||
|         with TiffImagePlugin.AppendingTiffWriter(b) as a: |         with TiffImagePlugin.AppendingTiffWriter(b) as a: | ||||||
|             a.offsetOfNewPage = 2**32 |             a.offsetOfNewPage = 2**32 | ||||||
| 
 | 
 | ||||||
|  | @ -777,18 +777,20 @@ class TestFileTiff: | ||||||
|             a.fixOffsets(1, isLong=True) |             a.fixOffsets(1, isLong=True) | ||||||
| 
 | 
 | ||||||
|     def test_appending_tiff_writer_writelong(self) -> None: |     def test_appending_tiff_writer_writelong(self) -> None: | ||||||
|         data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" |         data = b"II\x2a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" | ||||||
|         b = BytesIO(data) |         b = BytesIO(data) | ||||||
|         with TiffImagePlugin.AppendingTiffWriter(b) as a: |         with TiffImagePlugin.AppendingTiffWriter(b) as a: | ||||||
|  |             a.seek(-4, os.SEEK_CUR) | ||||||
|             a.writeLong(2**32 - 1) |             a.writeLong(2**32 - 1) | ||||||
|             assert b.getvalue() == data + b"\xff\xff\xff\xff" |             assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff" | ||||||
| 
 | 
 | ||||||
|     def test_appending_tiff_writer_rewritelastshorttolong(self) -> None: |     def test_appending_tiff_writer_rewritelastshorttolong(self) -> None: | ||||||
|         data = b"II\x2A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" |         data = b"II\x2a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" | ||||||
|         b = BytesIO(data) |         b = BytesIO(data) | ||||||
|         with TiffImagePlugin.AppendingTiffWriter(b) as a: |         with TiffImagePlugin.AppendingTiffWriter(b) as a: | ||||||
|  |             a.seek(-2, os.SEEK_CUR) | ||||||
|             a.rewriteLastShortToLong(2**32 - 1) |             a.rewriteLastShortToLong(2**32 - 1) | ||||||
|             assert b.getvalue() == data[:-2] + b"\xff\xff\xff\xff" |             assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff" | ||||||
| 
 | 
 | ||||||
|     def test_saving_icc_profile(self, tmp_path: Path) -> None: |     def test_saving_icc_profile(self, tmp_path: Path) -> None: | ||||||
|         # Tests saving TIFF with icc_profile set. |         # Tests saving TIFF with icc_profile set. | ||||||
|  | @ -939,11 +941,10 @@ class TestFileTiff: | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.timeout(6) |     @pytest.mark.timeout(6) | ||||||
|     @pytest.mark.filterwarnings("ignore:Truncated File Read") |     @pytest.mark.filterwarnings("ignore:Truncated File Read") | ||||||
|     def test_timeout(self) -> None: |     def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         with Image.open("Tests/images/timeout-6646305047838720") as im: |         with Image.open("Tests/images/timeout-6646305047838720") as im: | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = True |             monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|             im.load() |             im.load() | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize( |     @pytest.mark.parametrize( | ||||||
|         "test_file", |         "test_file", | ||||||
|  |  | ||||||
|  | @ -28,9 +28,9 @@ except ImportError: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestUnsupportedWebp: | class TestUnsupportedWebp: | ||||||
|     def test_unsupported(self) -> None: |     def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         if HAVE_WEBP: |         if HAVE_WEBP: | ||||||
|             WebPImagePlugin.SUPPORTED = False |             monkeypatch.setattr(WebPImagePlugin, "SUPPORTED", False) | ||||||
| 
 | 
 | ||||||
|         file_path = "Tests/images/hopper.webp" |         file_path = "Tests/images/hopper.webp" | ||||||
|         with pytest.warns(UserWarning): |         with pytest.warns(UserWarning): | ||||||
|  | @ -38,9 +38,6 @@ class TestUnsupportedWebp: | ||||||
|                 with Image.open(file_path): |                 with Image.open(file_path): | ||||||
|                     pass |                     pass | ||||||
| 
 | 
 | ||||||
|         if HAVE_WEBP: |  | ||||||
|             WebPImagePlugin.SUPPORTED = True |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| @skip_unless_feature("webp") | @skip_unless_feature("webp") | ||||||
| class TestFileWebp: | class TestFileWebp: | ||||||
|  |  | ||||||
|  | @ -71,7 +71,7 @@ def test_load_float_dpi() -> None: | ||||||
| 
 | 
 | ||||||
|     with open("Tests/images/drawing.emf", "rb") as fp: |     with open("Tests/images/drawing.emf", "rb") as fp: | ||||||
|         data = fp.read() |         data = fp.read() | ||||||
|     b = BytesIO(data[:8] + b"\x06\xFA" + data[10:]) |     b = BytesIO(data[:8] + b"\x06\xfa" + data[10:]) | ||||||
|     with Image.open(b) as im: |     with Image.open(b) as im: | ||||||
|         assert im.info["dpi"][0] == 2540 |         assert im.info["dpi"][0] == 2540 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -578,9 +578,7 @@ class TestImage: | ||||||
|     def test_one_item_tuple(self) -> None: |     def test_one_item_tuple(self) -> None: | ||||||
|         for mode in ("I", "F", "L"): |         for mode in ("I", "F", "L"): | ||||||
|             im = Image.new(mode, (100, 100), (5,)) |             im = Image.new(mode, (100, 100), (5,)) | ||||||
|             px = im.load() |             assert im.getpixel((0, 0)) == 5 | ||||||
|             assert px is not None |  | ||||||
|             assert px[0, 0] == 5 |  | ||||||
| 
 | 
 | ||||||
|     def test_linear_gradient_wrong_mode(self) -> None: |     def test_linear_gradient_wrong_mode(self) -> None: | ||||||
|         # Arrange |         # Arrange | ||||||
|  | @ -665,7 +663,7 @@ class TestImage: | ||||||
|         # Test illegal image mode |         # Test illegal image mode | ||||||
|         with hopper() as im: |         with hopper() as im: | ||||||
|             with pytest.raises(ValueError): |             with pytest.raises(ValueError): | ||||||
|                 im.remap_palette(None) |                 im.remap_palette([]) | ||||||
| 
 | 
 | ||||||
|     def test_remap_palette_transparency(self) -> None: |     def test_remap_palette_transparency(self) -> None: | ||||||
|         im = Image.new("P", (1, 2), (0, 0, 0)) |         im = Image.new("P", (1, 2), (0, 0, 0)) | ||||||
|  | @ -768,7 +766,7 @@ class TestImage: | ||||||
|         assert dict(exif) |         assert dict(exif) | ||||||
| 
 | 
 | ||||||
|         # Test that exif data is cleared after another load |         # Test that exif data is cleared after another load | ||||||
|         exif.load(None) |         exif.load(b"") | ||||||
|         assert not dict(exif) |         assert not dict(exif) | ||||||
| 
 | 
 | ||||||
|         # Test loading just the EXIF header |         # Test loading just the EXIF header | ||||||
|  | @ -989,6 +987,11 @@ class TestImage: | ||||||
|         else: |         else: | ||||||
|             assert im.getxmp() == {"xmpmeta": None} |             assert im.getxmp() == {"xmpmeta": None} | ||||||
| 
 | 
 | ||||||
|  |     def test_get_child_images(self) -> None: | ||||||
|  |         im = Image.new("RGB", (1, 1)) | ||||||
|  |         with pytest.warns(DeprecationWarning): | ||||||
|  |             assert im.get_child_images() == [] | ||||||
|  | 
 | ||||||
|     @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) |     @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) | ||||||
|     def test_zero_tobytes(self, size: tuple[int, int]) -> None: |     def test_zero_tobytes(self, size: tuple[int, int]) -> None: | ||||||
|         im = Image.new("RGB", size) |         im = Image.new("RGB", size) | ||||||
|  |  | ||||||
|  | @ -222,9 +222,7 @@ def test_l_macro_rounding(convert_mode: str) -> None: | ||||||
|         im.palette.getcolor((0, 1, 2)) |         im.palette.getcolor((0, 1, 2)) | ||||||
| 
 | 
 | ||||||
|         converted_im = im.convert(convert_mode) |         converted_im = im.convert(convert_mode) | ||||||
|         px = converted_im.load() |         converted_color = converted_im.getpixel((0, 0)) | ||||||
|         assert px is not None |  | ||||||
|         converted_color = px[0, 0] |  | ||||||
|         if convert_mode == "LA": |         if convert_mode == "LA": | ||||||
|             assert isinstance(converted_color, tuple) |             assert isinstance(converted_color, tuple) | ||||||
|             converted_color = converted_color[0] |             converted_color = converted_color[0] | ||||||
|  |  | ||||||
|  | @ -148,10 +148,8 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None: | ||||||
|     im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color) |     im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color) | ||||||
| 
 | 
 | ||||||
|     converted = im.quantize(method=method) |     converted = im.quantize(method=method) | ||||||
|     converted_px = converted.load() |  | ||||||
|     assert converted_px is not None |  | ||||||
|     assert converted.palette is not None |     assert converted.palette is not None | ||||||
|     assert converted_px[0, 0] == converted.palette.colors[color] |     assert converted.getpixel((0, 0)) == converted.palette.colors[color] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_small_palette() -> None: | def test_small_palette() -> None: | ||||||
|  |  | ||||||
|  | @ -309,7 +309,7 @@ class TestImageResize: | ||||||
|         # Test unknown resampling filter |         # Test unknown resampling filter | ||||||
|         with hopper() as im: |         with hopper() as im: | ||||||
|             with pytest.raises(ValueError): |             with pytest.raises(ValueError): | ||||||
|                 im.resize((10, 10), "unknown") |                 im.resize((10, 10), -1) | ||||||
| 
 | 
 | ||||||
|     @skip_unless_feature("libtiff") |     @skip_unless_feature("libtiff") | ||||||
|     def test_transposed(self) -> None: |     def test_transposed(self) -> None: | ||||||
|  |  | ||||||
|  | @ -812,7 +812,7 @@ def test_rounded_rectangle( | ||||||
|         tuple[int, int, int, int] |         tuple[int, int, int, int] | ||||||
|         | tuple[list[int]] |         | tuple[list[int]] | ||||||
|         | tuple[tuple[int, int], tuple[int, int]] |         | tuple[tuple[int, int], tuple[int, int]] | ||||||
|     ) |     ), | ||||||
| ) -> None: | ) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     im = Image.new("RGB", (200, 200)) |     im = Image.new("RGB", (200, 200)) | ||||||
|  | @ -1396,6 +1396,28 @@ def test_stroke_descender() -> None: | ||||||
|     assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) |     assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @skip_unless_feature("freetype2") | ||||||
|  | def test_stroke_inside_gap() -> None: | ||||||
|  |     # Arrange | ||||||
|  |     im = Image.new("RGB", (120, 130)) | ||||||
|  |     draw = ImageDraw.Draw(im) | ||||||
|  |     font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) | ||||||
|  | 
 | ||||||
|  |     # Act | ||||||
|  |     draw.text((12, 12), "i", "#f00", font, stroke_width=20) | ||||||
|  | 
 | ||||||
|  |     # Assert | ||||||
|  |     for y in range(im.height): | ||||||
|  |         glyph = "" | ||||||
|  |         for x in range(im.width): | ||||||
|  |             if im.getpixel((x, y)) == (0, 0, 0): | ||||||
|  |                 if glyph == "started": | ||||||
|  |                     glyph = "ended" | ||||||
|  |             else: | ||||||
|  |                 assert glyph != "ended", "Gap inside stroked glyph" | ||||||
|  |                 glyph = "started" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @skip_unless_feature("freetype2") | @skip_unless_feature("freetype2") | ||||||
| def test_split_word() -> None: | def test_split_word() -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|  |  | ||||||
|  | @ -191,13 +191,10 @@ class TestImageFile: | ||||||
|                 im.load() |                 im.load() | ||||||
| 
 | 
 | ||||||
|     @skip_unless_feature("zlib") |     @skip_unless_feature("zlib") | ||||||
|     def test_truncated_without_errors(self) -> None: |     def test_truncated_without_errors(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         with Image.open("Tests/images/truncated_image.png") as im: |         with Image.open("Tests/images/truncated_image.png") as im: | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = True |             monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|             try: |  | ||||||
|             im.load() |             im.load() | ||||||
|             finally: |  | ||||||
|                 ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
| 
 | 
 | ||||||
|     @skip_unless_feature("zlib") |     @skip_unless_feature("zlib") | ||||||
|     def test_broken_datastream_with_errors(self) -> None: |     def test_broken_datastream_with_errors(self) -> None: | ||||||
|  | @ -206,13 +203,12 @@ class TestImageFile: | ||||||
|                 im.load() |                 im.load() | ||||||
| 
 | 
 | ||||||
|     @skip_unless_feature("zlib") |     @skip_unless_feature("zlib") | ||||||
|     def test_broken_datastream_without_errors(self) -> None: |     def test_broken_datastream_without_errors( | ||||||
|  |         self, monkeypatch: pytest.MonkeyPatch | ||||||
|  |     ) -> None: | ||||||
|         with Image.open("Tests/images/broken_data_stream.png") as im: |         with Image.open("Tests/images/broken_data_stream.png") as im: | ||||||
|             ImageFile.LOAD_TRUNCATED_IMAGES = True |             monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True) | ||||||
|             try: |  | ||||||
|             im.load() |             im.load() | ||||||
|             finally: |  | ||||||
|                 ImageFile.LOAD_TRUNCATED_IMAGES = False |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MockPyDecoder(ImageFile.PyDecoder): | class MockPyDecoder(ImageFile.PyDecoder): | ||||||
|  |  | ||||||
|  | @ -254,7 +254,8 @@ def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) |     "align, ext", | ||||||
|  |     (("left", ""), ("center", "_center"), ("right", "_right"), ("justify", "_justify")), | ||||||
| ) | ) | ||||||
| def test_render_multiline_text_align( | def test_render_multiline_text_align( | ||||||
|     font: ImageFont.FreeTypeFont, align: str, ext: str |     font: ImageFont.FreeTypeFont, align: str, ext: str | ||||||
|  | @ -461,6 +462,20 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: | ||||||
|     assert mask.size == (108, 13) |     assert mask.size == (108, 13) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def test_stroke_mask() -> None: | ||||||
|  |     # Arrange | ||||||
|  |     text = "i" | ||||||
|  | 
 | ||||||
|  |     # Act | ||||||
|  |     font = ImageFont.truetype(FONT_PATH, 128) | ||||||
|  |     mask = font.getmask(text, stroke_width=2) | ||||||
|  | 
 | ||||||
|  |     # Assert | ||||||
|  |     assert mask.getpixel((34, 5)) == 255 | ||||||
|  |     assert mask.getpixel((38, 5)) == 0 | ||||||
|  |     assert mask.getpixel((42, 5)) == 255 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def test_load_when_image_not_found() -> None: | def test_load_when_image_not_found() -> None: | ||||||
|     with tempfile.NamedTemporaryFile(delete=False) as tmp: |     with tempfile.NamedTemporaryFile(delete=False) as tmp: | ||||||
|         pass |         pass | ||||||
|  | @ -543,7 +558,7 @@ def test_render_empty(font: ImageFont.FreeTypeFont) -> None: | ||||||
| 
 | 
 | ||||||
| def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: | def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: | ||||||
|     # issue #3777 |     # issue #3777 | ||||||
|     text = "A\u278A\U0001F12B" |     text = "A\u278a\U0001f12b" | ||||||
|     target = "Tests/images/unicode_extended.png" |     target = "Tests/images/unicode_extended.png" | ||||||
| 
 | 
 | ||||||
|     ttf = ImageFont.truetype( |     ttf = ImageFont.truetype( | ||||||
|  | @ -1012,7 +1027,7 @@ def test_sbix(layout_engine: ImageFont.Layout) -> None: | ||||||
|         im = Image.new("RGB", (400, 400), "white") |         im = Image.new("RGB", (400, 400), "white") | ||||||
|         d = ImageDraw.Draw(im) |         d = ImageDraw.Draw(im) | ||||||
| 
 | 
 | ||||||
|         d.text((50, 50), "\uE901", font=font, embedded_color=True) |         d.text((50, 50), "\ue901", font=font, embedded_color=True) | ||||||
| 
 | 
 | ||||||
|         assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) |         assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) | ||||||
|     except OSError as e:  # pragma: no cover |     except OSError as e:  # pragma: no cover | ||||||
|  | @ -1029,7 +1044,7 @@ def test_sbix_mask(layout_engine: ImageFont.Layout) -> None: | ||||||
|         im = Image.new("RGB", (400, 400), "white") |         im = Image.new("RGB", (400, 400), "white") | ||||||
|         d = ImageDraw.Draw(im) |         d = ImageDraw.Draw(im) | ||||||
| 
 | 
 | ||||||
|         d.text((50, 50), "\uE901", (100, 0, 0), font=font) |         d.text((50, 50), "\ue901", (100, 0, 0), font=font) | ||||||
| 
 | 
 | ||||||
|         assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) |         assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) | ||||||
|     except OSError as e:  # pragma: no cover |     except OSError as e:  # pragma: no cover | ||||||
|  |  | ||||||
|  | @ -229,7 +229,7 @@ def test_getlength( | ||||||
| @pytest.mark.parametrize("direction", ("ltr", "ttb")) | @pytest.mark.parametrize("direction", ("ltr", "ttb")) | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "text", |     "text", | ||||||
|     ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), |     ("i" + ("\u030c" * 15) + "i", "i" + "\u032c" * 15 + "i", "\u035cii", "i\u0305i"), | ||||||
|     ids=("caron-above", "caron-below", "double-breve", "overline"), |     ids=("caron-above", "caron-below", "double-breve", "overline"), | ||||||
| ) | ) | ||||||
| def test_getlength_combine(mode: str, direction: str, text: str) -> None: | def test_getlength_combine(mode: str, direction: str, text: str) -> None: | ||||||
|  | @ -272,27 +272,27 @@ def test_anchor_ttb(anchor: str) -> None: | ||||||
| 
 | 
 | ||||||
| combine_tests = ( | combine_tests = ( | ||||||
|     # extends above (e.g. issue #4553) |     # extends above (e.g. issue #4553) | ||||||
|     ("caron", "a\u030C\u030C\u030C\u030C\u030Cb", None, None, 0.08), |     ("caron", "a\u030c\u030c\u030c\u030c\u030cb", None, None, 0.08), | ||||||
|     ("caron_la", "a\u030C\u030C\u030C\u030C\u030Cb", "la", None, 0.08), |     ("caron_la", "a\u030c\u030c\u030c\u030c\u030cb", "la", None, 0.08), | ||||||
|     ("caron_lt", "a\u030C\u030C\u030C\u030C\u030Cb", "lt", None, 0.08), |     ("caron_lt", "a\u030c\u030c\u030c\u030c\u030cb", "lt", None, 0.08), | ||||||
|     ("caron_ls", "a\u030C\u030C\u030C\u030C\u030Cb", "ls", None, 0.08), |     ("caron_ls", "a\u030c\u030c\u030c\u030c\u030cb", "ls", None, 0.08), | ||||||
|     ("caron_ttb", "ca" + ("\u030C" * 15) + "b", None, "ttb", 0.3), |     ("caron_ttb", "ca" + ("\u030c" * 15) + "b", None, "ttb", 0.3), | ||||||
|     ("caron_ttb_lt", "ca" + ("\u030C" * 15) + "b", "lt", "ttb", 0.3), |     ("caron_ttb_lt", "ca" + ("\u030c" * 15) + "b", "lt", "ttb", 0.3), | ||||||
|     # extends below |     # extends below | ||||||
|     ("caron_below", "a\u032C\u032C\u032C\u032C\u032Cb", None, None, 0.02), |     ("caron_below", "a\u032c\u032c\u032c\u032c\u032cb", None, None, 0.02), | ||||||
|     ("caron_below_ld", "a\u032C\u032C\u032C\u032C\u032Cb", "ld", None, 0.02), |     ("caron_below_ld", "a\u032c\u032c\u032c\u032c\u032cb", "ld", None, 0.02), | ||||||
|     ("caron_below_lb", "a\u032C\u032C\u032C\u032C\u032Cb", "lb", None, 0.02), |     ("caron_below_lb", "a\u032c\u032c\u032c\u032c\u032cb", "lb", None, 0.02), | ||||||
|     ("caron_below_ls", "a\u032C\u032C\u032C\u032C\u032Cb", "ls", None, 0.02), |     ("caron_below_ls", "a\u032c\u032c\u032c\u032c\u032cb", "ls", None, 0.02), | ||||||
|     ("caron_below_ttb", "a" + ("\u032C" * 15) + "b", None, "ttb", 0.03), |     ("caron_below_ttb", "a" + ("\u032c" * 15) + "b", None, "ttb", 0.03), | ||||||
|     ("caron_below_ttb_lb", "a" + ("\u032C" * 15) + "b", "lb", "ttb", 0.03), |     ("caron_below_ttb_lb", "a" + ("\u032c" * 15) + "b", "lb", "ttb", 0.03), | ||||||
|     # extends to the right (e.g. issue #3745) |     # extends to the right (e.g. issue #3745) | ||||||
|     ("double_breve_below", "a\u035Ci", None, None, 0.02), |     ("double_breve_below", "a\u035ci", None, None, 0.02), | ||||||
|     ("double_breve_below_ma", "a\u035Ci", "ma", None, 0.02), |     ("double_breve_below_ma", "a\u035ci", "ma", None, 0.02), | ||||||
|     ("double_breve_below_ra", "a\u035Ci", "ra", None, 0.02), |     ("double_breve_below_ra", "a\u035ci", "ra", None, 0.02), | ||||||
|     ("double_breve_below_ttb", "a\u035Cb", None, "ttb", 0.02), |     ("double_breve_below_ttb", "a\u035cb", None, "ttb", 0.02), | ||||||
|     ("double_breve_below_ttb_rt", "a\u035Cb", "rt", "ttb", 0.02), |     ("double_breve_below_ttb_rt", "a\u035cb", "rt", "ttb", 0.02), | ||||||
|     ("double_breve_below_ttb_mt", "a\u035Cb", "mt", "ttb", 0.02), |     ("double_breve_below_ttb_mt", "a\u035cb", "mt", "ttb", 0.02), | ||||||
|     ("double_breve_below_ttb_st", "a\u035Cb", "st", "ttb", 0.02), |     ("double_breve_below_ttb_st", "a\u035cb", "st", "ttb", 0.02), | ||||||
|     # extends to the left (fail=0.064) |     # extends to the left (fail=0.064) | ||||||
|     ("overline", "i\u0305", None, None, 0.02), |     ("overline", "i\u0305", None, None, 0.02), | ||||||
|     ("overline_la", "i\u0305", "la", None, 0.02), |     ("overline_la", "i\u0305", "la", None, 0.02), | ||||||
|  | @ -346,7 +346,7 @@ def test_combine_multiline(anchor: str, align: str) -> None: | ||||||
| 
 | 
 | ||||||
|     path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" |     path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" | ||||||
|     f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) |     f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) | ||||||
|     text = "i\u0305\u035C\ntext"  # i with overline and double breve, and a word |     text = "i\u0305\u035c\ntext"  # i with overline and double breve, and a word | ||||||
| 
 | 
 | ||||||
|     im = Image.new("RGB", (400, 400), "white") |     im = Image.new("RGB", (400, 400), "white") | ||||||
|     d = ImageDraw.Draw(im) |     d = ImageDraw.Draw(im) | ||||||
|  |  | ||||||
|  | @ -165,14 +165,10 @@ def test_pad() -> None: | ||||||
| def test_pad_round() -> None: | def test_pad_round() -> None: | ||||||
|     im = Image.new("1", (1, 1), 1) |     im = Image.new("1", (1, 1), 1) | ||||||
|     new_im = ImageOps.pad(im, (4, 1)) |     new_im = ImageOps.pad(im, (4, 1)) | ||||||
|     px = new_im.load() |     assert new_im.getpixel((2, 0)) == 1 | ||||||
|     assert px is not None |  | ||||||
|     assert px[2, 0] == 1 |  | ||||||
| 
 | 
 | ||||||
|     new_im = ImageOps.pad(im, (1, 4)) |     new_im = ImageOps.pad(im, (1, 4)) | ||||||
|     px = new_im.load() |     assert new_im.getpixel((0, 2)) == 1 | ||||||
|     assert px is not None |  | ||||||
|     assert px[0, 2] == 1 |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("mode", ("P", "PA")) | @pytest.mark.parametrize("mode", ("P", "PA")) | ||||||
|  |  | ||||||
|  | @ -189,7 +189,7 @@ def test_2bit_palette(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|     rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2 |     rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2 | ||||||
|     img = Image.frombytes("P", (6, 1), rgb) |     img = Image.frombytes("P", (6, 1), rgb) | ||||||
|     img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF")  # RGB |     img.putpalette(b"\xff\x00\x00\x00\xff\x00\x00\x00\xff")  # RGB | ||||||
|     img.save(outfile, format="PNG") |     img.save(outfile, format="PNG") | ||||||
| 
 | 
 | ||||||
|     assert_image_equal_tofile(img, outfile) |     assert_image_equal_tofile(img, outfile) | ||||||
|  |  | ||||||
|  | @ -79,7 +79,7 @@ def test_path_constructors( | ||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
| def test_invalid_path_constructors( | def test_invalid_path_constructors( | ||||||
|     coords: tuple[str, str] | Sequence[Sequence[int]] |     coords: tuple[str, str] | Sequence[Sequence[int]], | ||||||
| ) -> None: | ) -> None: | ||||||
|     # Act |     # Act | ||||||
|     with pytest.raises(ValueError) as e: |     with pytest.raises(ValueError) as e: | ||||||
|  |  | ||||||
|  | @ -7,36 +7,30 @@ import pytest | ||||||
| from PIL import Image | from PIL import Image | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_overflow() -> None: | def test_overflow(monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|     # There is the potential to overflow comparisons in map.c |     # There is the potential to overflow comparisons in map.c | ||||||
|     # if there are > SIZE_MAX bytes in the image or if |     # if there are > SIZE_MAX bytes in the image or if | ||||||
|     # the file encodes an offset that makes |     # the file encodes an offset that makes | ||||||
|     # (offset + size(bytes)) > SIZE_MAX |     # (offset + size(bytes)) > SIZE_MAX | ||||||
| 
 | 
 | ||||||
|     # Note that this image triggers the decompression bomb warning: |     # Note that this image triggers the decompression bomb warning: | ||||||
|     max_pixels = Image.MAX_IMAGE_PIXELS |     monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None) | ||||||
|     Image.MAX_IMAGE_PIXELS = None |  | ||||||
| 
 | 
 | ||||||
|     # This image hits the offset test. |     # This image hits the offset test. | ||||||
|     with Image.open("Tests/images/l2rgb_read.bmp") as im: |     with Image.open("Tests/images/l2rgb_read.bmp") as im: | ||||||
|         with pytest.raises((ValueError, MemoryError, OSError)): |         with pytest.raises((ValueError, MemoryError, OSError)): | ||||||
|             im.load() |             im.load() | ||||||
| 
 | 
 | ||||||
|     Image.MAX_IMAGE_PIXELS = max_pixels |  | ||||||
| 
 | 
 | ||||||
| 
 | def test_tobytes(monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
| def test_tobytes() -> None: |  | ||||||
|     # Note that this image triggers the decompression bomb warning: |     # Note that this image triggers the decompression bomb warning: | ||||||
|     max_pixels = Image.MAX_IMAGE_PIXELS |     monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None) | ||||||
|     Image.MAX_IMAGE_PIXELS = None |  | ||||||
| 
 | 
 | ||||||
|     # Previously raised an access violation on Windows |     # Previously raised an access violation on Windows | ||||||
|     with Image.open("Tests/images/l2rgb_read.bmp") as im: |     with Image.open("Tests/images/l2rgb_read.bmp") as im: | ||||||
|         with pytest.raises((ValueError, MemoryError, OSError)): |         with pytest.raises((ValueError, MemoryError, OSError)): | ||||||
|             im.tobytes() |             im.tobytes() | ||||||
| 
 | 
 | ||||||
|     Image.MAX_IMAGE_PIXELS = max_pixels |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") | @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") | ||||||
| def test_ysize() -> None: | def test_ysize() -> None: | ||||||
|  |  | ||||||
|  | @ -141,9 +141,7 @@ def test_save_tiff_uint16() -> None: | ||||||
|     a.shape = TEST_IMAGE_SIZE |     a.shape = TEST_IMAGE_SIZE | ||||||
|     img = Image.fromarray(a) |     img = Image.fromarray(a) | ||||||
| 
 | 
 | ||||||
|     img_px = img.load() |     assert img.getpixel((0, 0)) == pixel_value | ||||||
|     assert img_px is not None |  | ||||||
|     assert img_px[0, 0] == pixel_value |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|  |  | ||||||
|  | @ -20,10 +20,10 @@ from PIL.PdfParser import ( | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_text_encode_decode() -> None: | def test_text_encode_decode() -> None: | ||||||
|     assert encode_text("abc") == b"\xFE\xFF\x00a\x00b\x00c" |     assert encode_text("abc") == b"\xfe\xff\x00a\x00b\x00c" | ||||||
|     assert decode_text(b"\xFE\xFF\x00a\x00b\x00c") == "abc" |     assert decode_text(b"\xfe\xff\x00a\x00b\x00c") == "abc" | ||||||
|     assert decode_text(b"abc") == "abc" |     assert decode_text(b"abc") == "abc" | ||||||
|     assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD" |     assert decode_text(b"\x1b a \x1c") == "\u02d9 a \u02dd" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_indirect_refs() -> None: | def test_indirect_refs() -> None: | ||||||
|  | @ -45,8 +45,8 @@ def test_parsing() -> None: | ||||||
|     assert PdfParser.get_value(b"false%", 0) == (False, 5) |     assert PdfParser.get_value(b"false%", 0) == (False, 5) | ||||||
|     assert PdfParser.get_value(b"null<", 0) == (None, 4) |     assert PdfParser.get_value(b"null<", 0) == (None, 4) | ||||||
|     assert PdfParser.get_value(b"%cmt\n %cmt\n 123\n", 0) == (123, 15) |     assert PdfParser.get_value(b"%cmt\n %cmt\n 123\n", 0) == (123, 15) | ||||||
|     assert PdfParser.get_value(b"<901FA3>", 0) == (b"\x90\x1F\xA3", 8) |     assert PdfParser.get_value(b"<901FA3>", 0) == (b"\x90\x1f\xa3", 8) | ||||||
|     assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1F\xA0", 17) |     assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1f\xa0", 17) | ||||||
|     assert PdfParser.get_value(b"(asd)", 0) == (b"asd", 5) |     assert PdfParser.get_value(b"(asd)", 0) == (b"asd", 5) | ||||||
|     assert PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0) == (b"asd(qwe)zxc", 13) |     assert PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0) == (b"asd(qwe)zxc", 13) | ||||||
|     assert PdfParser.get_value(b"(Two \\\nwords.)", 0) == (b"Two words.", 14) |     assert PdfParser.get_value(b"(Two \\\nwords.)", 0) == (b"Two words.", 14) | ||||||
|  | @ -56,9 +56,9 @@ def test_parsing() -> None: | ||||||
|     assert PdfParser.get_value(b"(One\\(paren).", 0) == (b"One(paren", 12) |     assert PdfParser.get_value(b"(One\\(paren).", 0) == (b"One(paren", 12) | ||||||
|     assert PdfParser.get_value(b"(One\\)paren).", 0) == (b"One)paren", 12) |     assert PdfParser.get_value(b"(One\\)paren).", 0) == (b"One)paren", 12) | ||||||
|     assert PdfParser.get_value(b"(\\0053)", 0) == (b"\x053", 7) |     assert PdfParser.get_value(b"(\\0053)", 0) == (b"\x053", 7) | ||||||
|     assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2B", 6) |     assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2b", 6) | ||||||
|     assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2B", 5) |     assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2b", 5) | ||||||
|     assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2Ba", 6) |     assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2ba", 6) | ||||||
|     assert PdfParser.get_value(b"(\\1111)", 0) == (b"\x491", 7) |     assert PdfParser.get_value(b"(\\1111)", 0) == (b"\x491", 7) | ||||||
|     assert PdfParser.get_value(b" 123 (", 0) == (123, 4) |     assert PdfParser.get_value(b" 123 (", 0) == (123, 4) | ||||||
|     assert round(abs(PdfParser.get_value(b" 123.4 %", 0)[0] - 123.4), 7) == 0 |     assert round(abs(PdfParser.get_value(b" 123.4 %", 0)[0] - 123.4), 7) == 0 | ||||||
|  | @ -118,7 +118,7 @@ def test_pdf_repr() -> None: | ||||||
|     assert pdf_repr(None) == b"null" |     assert pdf_repr(None) == b"null" | ||||||
|     assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)" |     assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)" | ||||||
|     assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" |     assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" | ||||||
|     assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" |     assert pdf_repr(PdfBinary(b"\x90\x1f\xa0")) == b"<901FA0>" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_duplicate_xref_entry() -> None: | def test_duplicate_xref_entry() -> None: | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| # install libimagequant | # install libimagequant | ||||||
| 
 | 
 | ||||||
| archive_name=libimagequant | archive_name=libimagequant | ||||||
| archive_version=4.3.3 | archive_version=4.3.4 | ||||||
| 
 | 
 | ||||||
| archive=$archive_name-$archive_version | archive=$archive_name-$archive_version | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,12 +6,11 @@ Goals | ||||||
| 
 | 
 | ||||||
| The fork author's goal is to foster and support active development of PIL through: | The fork author's goal is to foster and support active development of PIL through: | ||||||
| 
 | 
 | ||||||
| - Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_ | - Continuous integration testing via `GitHub Actions`_ | ||||||
| - Publicized development activity on `GitHub`_ | - Publicized development activity on `GitHub`_ | ||||||
| - Regular releases to the `Python Package Index`_ | - Regular releases to the `Python Package Index`_ | ||||||
| 
 | 
 | ||||||
| .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions | .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions | ||||||
| .. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow |  | ||||||
| .. _GitHub: https://github.com/python-pillow/Pillow | .. _GitHub: https://github.com/python-pillow/Pillow | ||||||
| .. _Python Package Index: https://pypi.org/project/pillow/ | .. _Python Package Index: https://pypi.org/project/pillow/ | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -183,6 +183,16 @@ ExifTags.IFD.Makernote | ||||||
| ``ExifTags.IFD.Makernote`` has been deprecated. Instead, use | ``ExifTags.IFD.Makernote`` has been deprecated. Instead, use | ||||||
| ``ExifTags.IFD.MakerNote``. | ``ExifTags.IFD.MakerNote``. | ||||||
| 
 | 
 | ||||||
|  | Image.Image.get_child_images() | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | 
 | ||||||
|  | .. deprecated:: 11.2.0 | ||||||
|  | 
 | ||||||
|  | ``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow | ||||||
|  | 13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The | ||||||
|  | method uses an image's file pointer, and so child images could only be retrieved from | ||||||
|  | an :py:class:`PIL.ImageFile.ImageFile` instance. | ||||||
|  | 
 | ||||||
| Removed features | Removed features | ||||||
| ---------------- | ---------------- | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -33,10 +33,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h | ||||||
|    :target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml |    :target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml | ||||||
|    :alt: GitHub Actions build status (Test Cygwin) |    :alt: GitHub Actions build status (Test Cygwin) | ||||||
| 
 | 
 | ||||||
| .. image:: https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build |  | ||||||
|    :target: https://ci.appveyor.com/project/python-pillow/Pillow |  | ||||||
|    :alt: AppVeyor CI build status (Windows) |  | ||||||
| 
 |  | ||||||
| .. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg | .. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg | ||||||
|    :target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml |    :target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml | ||||||
|    :alt: GitHub Actions build status (Wheels) |    :alt: GitHub Actions build status (Wheels) | ||||||
|  |  | ||||||
|  | @ -64,7 +64,7 @@ Many of Pillow's features require external libraries: | ||||||
| 
 | 
 | ||||||
| * **libimagequant** provides improved color quantization | * **libimagequant** provides improved color quantization | ||||||
| 
 | 
 | ||||||
|   * Pillow has been tested with libimagequant **2.6-4.3.3** |   * Pillow has been tested with libimagequant **2.6-4.3.4** | ||||||
|   * Libimagequant is licensed GPLv3, which is more restrictive than |   * Libimagequant is licensed GPLv3, which is more restrictive than | ||||||
|     the Pillow license, therefore we will not be distributing binaries |     the Pillow license, therefore we will not be distributing binaries | ||||||
|     with libimagequant support enabled. |     with libimagequant support enabled. | ||||||
|  |  | ||||||
|  | @ -44,18 +44,14 @@ These platforms are built and tested for every change. | ||||||
| +----------------------------------+----------------------------+---------------------+ | +----------------------------------+----------------------------+---------------------+ | ||||||
| | Ubuntu Linux 22.04 LTS (Jammy)   | 3.9, 3.10, 3.11,           | x86-64              | | | Ubuntu Linux 22.04 LTS (Jammy)   | 3.9, 3.10, 3.11,           | x86-64              | | ||||||
| |                                  | 3.12, 3.13, PyPy3          |                     | | |                                  | 3.12, 3.13, PyPy3          |                     | | ||||||
| |                                  +----------------------------+---------------------+ |  | ||||||
| |                                  | 3.10                       | arm64v8             | |  | ||||||
| +----------------------------------+----------------------------+---------------------+ | +----------------------------------+----------------------------+---------------------+ | ||||||
| | Ubuntu Linux 24.04 LTS (Noble)   | 3.12                       | x86-64, ppc64le,    | | | Ubuntu Linux 24.04 LTS (Noble)   | 3.12                       | x86-64, arm64v8,    | | ||||||
| |                                  |                            | s390x               | | |                                  |                            | ppc64le, s390x      | | ||||||
| +----------------------------------+----------------------------+---------------------+ | +----------------------------------+----------------------------+---------------------+ | ||||||
| | Windows Server 2019              | 3.9                        | x86-64              | | | Windows Server 2019              | 3.9                        | x86                 | | ||||||
| +----------------------------------+----------------------------+---------------------+ | +----------------------------------+----------------------------+---------------------+ | ||||||
| | Windows Server 2022              | 3.9, 3.10, 3.11,           | x86-64              | | | Windows Server 2022              | 3.10, 3.11, 3.12, 3.13,    | x86-64              | | ||||||
| |                                  | 3.12, 3.13, PyPy3          |                     | | |                                  | PyPy3                      |                     | | ||||||
| |                                  +----------------------------+---------------------+ |  | ||||||
| |                                  | 3.13                       | x86                 | |  | ||||||
| |                                  +----------------------------+---------------------+ | |                                  +----------------------------+---------------------+ | ||||||
| |                                  | 3.12 (CLANG64, MINGW64)    | x86-64              | | |                                  | 3.12 (CLANG64, MINGW64)    | x86-64              | | ||||||
| |                                  +----------------------------+---------------------+ | |                                  +----------------------------+---------------------+ | ||||||
|  |  | ||||||
|  | @ -387,8 +387,11 @@ Methods | ||||||
|                     the number of pixels between lines. |                     the number of pixels between lines. | ||||||
|     :param align: If the text is passed on to |     :param align: If the text is passed on to | ||||||
|                   :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, |                   :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`, | ||||||
|                   ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. |                   ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines | ||||||
|                   Use the ``anchor`` parameter to specify the alignment to ``xy``. |                   the relative alignment of lines. Use the ``anchor`` parameter to | ||||||
|  |                   specify the alignment to ``xy``. | ||||||
|  | 
 | ||||||
|  |                   .. versionadded:: 11.2.0 ``"justify"`` | ||||||
|     :param direction: Direction of the text. It can be ``"rtl"`` (right to |     :param direction: Direction of the text. It can be ``"rtl"`` (right to | ||||||
|                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). |                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). | ||||||
|                       Requires libraqm. |                       Requires libraqm. | ||||||
|  | @ -455,8 +458,11 @@ Methods | ||||||
|                               of Pillow, but implemented only in version 8.0.0. |                               of Pillow, but implemented only in version 8.0.0. | ||||||
| 
 | 
 | ||||||
|     :param spacing: The number of pixels between lines. |     :param spacing: The number of pixels between lines. | ||||||
|     :param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. |     :param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines | ||||||
|                   Use the ``anchor`` parameter to specify the alignment to ``xy``. |                   the relative alignment of lines. Use the ``anchor`` parameter to | ||||||
|  |                   specify the alignment to ``xy``. | ||||||
|  | 
 | ||||||
|  |                   .. versionadded:: 11.2.0 ``"justify"`` | ||||||
|     :param direction: Direction of the text. It can be ``"rtl"`` (right to |     :param direction: Direction of the text. It can be ``"rtl"`` (right to | ||||||
|                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). |                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). | ||||||
|                       Requires libraqm. |                       Requires libraqm. | ||||||
|  | @ -599,8 +605,11 @@ Methods | ||||||
|                     the number of pixels between lines. |                     the number of pixels between lines. | ||||||
|     :param align: If the text is passed on to |     :param align: If the text is passed on to | ||||||
|                   :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`, |                   :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`, | ||||||
|                   ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. |                   ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines | ||||||
|                   Use the ``anchor`` parameter to specify the alignment to ``xy``. |                   the relative alignment of lines. Use the ``anchor`` parameter to | ||||||
|  |                   specify the alignment to ``xy``. | ||||||
|  | 
 | ||||||
|  |                   .. versionadded:: 11.2.0 ``"justify"`` | ||||||
|     :param direction: Direction of the text. It can be ``"rtl"`` (right to |     :param direction: Direction of the text. It can be ``"rtl"`` (right to | ||||||
|                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). |                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). | ||||||
|                       Requires libraqm. |                       Requires libraqm. | ||||||
|  | @ -650,8 +659,11 @@ Methods | ||||||
|                    vertical text. See :ref:`text-anchors` for details. |                    vertical text. See :ref:`text-anchors` for details. | ||||||
|                    This parameter is ignored for non-TrueType fonts. |                    This parameter is ignored for non-TrueType fonts. | ||||||
|     :param spacing: The number of pixels between lines. |     :param spacing: The number of pixels between lines. | ||||||
|     :param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines. |     :param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines | ||||||
|                   Use the ``anchor`` parameter to specify the alignment to ``xy``. |                   the relative alignment of lines. Use the ``anchor`` parameter to | ||||||
|  |                   specify the alignment to ``xy``. | ||||||
|  | 
 | ||||||
|  |                   .. versionadded:: 11.2.0 ``"justify"`` | ||||||
|     :param direction: Direction of the text. It can be ``"rtl"`` (right to |     :param direction: Direction of the text. It can be ``"rtl"`` (right to | ||||||
|                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). |                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). | ||||||
|                       Requires libraqm. |                       Requires libraqm. | ||||||
|  |  | ||||||
|  | @ -54,6 +54,7 @@ Feature version numbers are available only where stated. | ||||||
| Support for the following features can be checked: | 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. | * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. | ||||||
|  | * ``mozjpeg``: (compile time) Whether Pillow was compiled against the MozJPEG 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. | * ``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. | * ``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. | * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. | ||||||
|  |  | ||||||
							
								
								
									
										75
									
								
								docs/releasenotes/11.2.0.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								docs/releasenotes/11.2.0.rst
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,75 @@ | ||||||
|  | 11.2.0 | ||||||
|  | ------ | ||||||
|  | 
 | ||||||
|  | Security | ||||||
|  | ======== | ||||||
|  | 
 | ||||||
|  | TODO | ||||||
|  | ^^^^ | ||||||
|  | 
 | ||||||
|  | TODO | ||||||
|  | 
 | ||||||
|  | :cve:`YYYY-XXXXX`: TODO | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | 
 | ||||||
|  | TODO | ||||||
|  | 
 | ||||||
|  | Backwards Incompatible Changes | ||||||
|  | ============================== | ||||||
|  | 
 | ||||||
|  | TODO | ||||||
|  | ^^^^ | ||||||
|  | 
 | ||||||
|  | Deprecations | ||||||
|  | ============ | ||||||
|  | 
 | ||||||
|  | Image.Image.get_child_images() | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | 
 | ||||||
|  | .. deprecated:: 11.2.0 | ||||||
|  | 
 | ||||||
|  | ``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow | ||||||
|  | 13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The | ||||||
|  | method uses an image's file pointer, and so child images could only be retrieved from | ||||||
|  | an :py:class:`PIL.ImageFile.ImageFile` instance. | ||||||
|  | 
 | ||||||
|  | API Changes | ||||||
|  | =========== | ||||||
|  | 
 | ||||||
|  | TODO | ||||||
|  | ^^^^ | ||||||
|  | 
 | ||||||
|  | TODO | ||||||
|  | 
 | ||||||
|  | API Additions | ||||||
|  | ============= | ||||||
|  | 
 | ||||||
|  | ``"justify"`` multiline text alignment | ||||||
|  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | 
 | ||||||
|  | In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be | ||||||
|  | aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`:: | ||||||
|  | 
 | ||||||
|  |     from PIL import Image, ImageDraw | ||||||
|  |     im = Image.new("RGB", (50, 25)) | ||||||
|  |     draw = ImageDraw.Draw(im) | ||||||
|  |     draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify") | ||||||
|  |     draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify") | ||||||
|  | 
 | ||||||
|  | Check for MozJPEG | ||||||
|  | ^^^^^^^^^^^^^^^^^ | ||||||
|  | 
 | ||||||
|  | You can check if Pillow has been built against the MozJPEG version of the | ||||||
|  | libjpeg library, and what version of MozJPEG is being used:: | ||||||
|  | 
 | ||||||
|  |     from PIL import features | ||||||
|  |     features.check_feature("mozjpeg")  # True or False | ||||||
|  |     features.version_feature("mozjpeg")  # "4.1.1" for example, or None | ||||||
|  | 
 | ||||||
|  | Other Changes | ||||||
|  | ============= | ||||||
|  | 
 | ||||||
|  | TODO | ||||||
|  | ^^^^ | ||||||
|  | 
 | ||||||
|  | TODO | ||||||
|  | @ -14,6 +14,7 @@ expected to be backported to earlier versions. | ||||||
| .. toctree:: | .. toctree:: | ||||||
|   :maxdepth: 2 |   :maxdepth: 2 | ||||||
| 
 | 
 | ||||||
|  |   11.2.0 | ||||||
|   11.1.0 |   11.1.0 | ||||||
|   11.0.0 |   11.0.0 | ||||||
|   10.4.0 |   10.4.0 | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ | ||||||
| # | # | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
|  | import os | ||||||
| from typing import IO | from typing import IO | ||||||
| 
 | 
 | ||||||
| from . import Image, ImageFile | from . import Image, ImageFile | ||||||
|  | @ -40,13 +41,11 @@ class BufrStubImageFile(ImageFile.StubImageFile): | ||||||
|     format_description = "BUFR" |     format_description = "BUFR" | ||||||
| 
 | 
 | ||||||
|     def _open(self) -> None: |     def _open(self) -> None: | ||||||
|         offset = self.fp.tell() |  | ||||||
| 
 |  | ||||||
|         if not _accept(self.fp.read(4)): |         if not _accept(self.fp.read(4)): | ||||||
|             msg = "Not a BUFR file" |             msg = "Not a BUFR file" | ||||||
|             raise SyntaxError(msg) |             raise SyntaxError(msg) | ||||||
| 
 | 
 | ||||||
|         self.fp.seek(offset) |         self.fp.seek(-4, os.SEEK_CUR) | ||||||
| 
 | 
 | ||||||
|         # make something up |         # make something up | ||||||
|         self._mode = "F" |         self._mode = "F" | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ | ||||||
| # | # | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
|  | import os | ||||||
| from typing import IO | from typing import IO | ||||||
| 
 | 
 | ||||||
| from . import Image, ImageFile | from . import Image, ImageFile | ||||||
|  | @ -40,13 +41,11 @@ class GribStubImageFile(ImageFile.StubImageFile): | ||||||
|     format_description = "GRIB" |     format_description = "GRIB" | ||||||
| 
 | 
 | ||||||
|     def _open(self) -> None: |     def _open(self) -> None: | ||||||
|         offset = self.fp.tell() |  | ||||||
| 
 |  | ||||||
|         if not _accept(self.fp.read(8)): |         if not _accept(self.fp.read(8)): | ||||||
|             msg = "Not a GRIB file" |             msg = "Not a GRIB file" | ||||||
|             raise SyntaxError(msg) |             raise SyntaxError(msg) | ||||||
| 
 | 
 | ||||||
|         self.fp.seek(offset) |         self.fp.seek(-8, os.SEEK_CUR) | ||||||
| 
 | 
 | ||||||
|         # make something up |         # make something up | ||||||
|         self._mode = "F" |         self._mode = "F" | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ | ||||||
| # | # | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
|  | import os | ||||||
| from typing import IO | from typing import IO | ||||||
| 
 | 
 | ||||||
| from . import Image, ImageFile | from . import Image, ImageFile | ||||||
|  | @ -40,13 +41,11 @@ class HDF5StubImageFile(ImageFile.StubImageFile): | ||||||
|     format_description = "HDF5" |     format_description = "HDF5" | ||||||
| 
 | 
 | ||||||
|     def _open(self) -> None: |     def _open(self) -> None: | ||||||
|         offset = self.fp.tell() |  | ||||||
| 
 |  | ||||||
|         if not _accept(self.fp.read(8)): |         if not _accept(self.fp.read(8)): | ||||||
|             msg = "Not an HDF file" |             msg = "Not an HDF file" | ||||||
|             raise SyntaxError(msg) |             raise SyntaxError(msg) | ||||||
| 
 | 
 | ||||||
|         self.fp.seek(offset) |         self.fp.seek(-8, os.SEEK_CUR) | ||||||
| 
 | 
 | ||||||
|         # make something up |         # make something up | ||||||
|         self._mode = "F" |         self._mode = "F" | ||||||
|  |  | ||||||
|  | @ -145,7 +145,7 @@ class ImImageFile(ImageFile.ImageFile): | ||||||
|             if s == b"\r": |             if s == b"\r": | ||||||
|                 continue |                 continue | ||||||
| 
 | 
 | ||||||
|             if not s or s == b"\0" or s == b"\x1A": |             if not s or s == b"\0" or s == b"\x1a": | ||||||
|                 break |                 break | ||||||
| 
 | 
 | ||||||
|             # FIXME: this may read whole file if not a text file |             # FIXME: this may read whole file if not a text file | ||||||
|  | @ -209,7 +209,7 @@ class ImImageFile(ImageFile.ImageFile): | ||||||
|         self._mode = self.info[MODE] |         self._mode = self.info[MODE] | ||||||
| 
 | 
 | ||||||
|         # Skip forward to start of image data |         # Skip forward to start of image data | ||||||
|         while s and s[:1] != b"\x1A": |         while s and s[:1] != b"\x1a": | ||||||
|             s = self.fp.read(1) |             s = self.fp.read(1) | ||||||
|         if not s: |         if not s: | ||||||
|             msg = "File truncated" |             msg = "File truncated" | ||||||
|  |  | ||||||
|  | @ -514,7 +514,7 @@ class ImagePointTransform: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _getscaleoffset( | def _getscaleoffset( | ||||||
|     expr: Callable[[ImagePointTransform], ImagePointTransform | float] |     expr: Callable[[ImagePointTransform], ImagePointTransform | float], | ||||||
| ) -> tuple[float, float]: | ) -> tuple[float, float]: | ||||||
|     a = expr(ImagePointTransform(1, 0)) |     a = expr(ImagePointTransform(1, 0)) | ||||||
|     return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a) |     return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a) | ||||||
|  | @ -603,24 +603,16 @@ class Image: | ||||||
|     def __enter__(self): |     def __enter__(self): | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def _close_fp(self): |  | ||||||
|         if getattr(self, "_fp", False): |  | ||||||
|             if self._fp != self.fp: |  | ||||||
|                 self._fp.close() |  | ||||||
|             self._fp = DeferredError(ValueError("Operation on closed image")) |  | ||||||
|         if self.fp: |  | ||||||
|             self.fp.close() |  | ||||||
| 
 |  | ||||||
|     def __exit__(self, *args): |     def __exit__(self, *args): | ||||||
|         if hasattr(self, "fp"): |         from . import ImageFile | ||||||
|  | 
 | ||||||
|  |         if isinstance(self, ImageFile.ImageFile): | ||||||
|             if getattr(self, "_exclusive_fp", False): |             if getattr(self, "_exclusive_fp", False): | ||||||
|                 self._close_fp() |                 self._close_fp() | ||||||
|             self.fp = None |             self.fp = None | ||||||
| 
 | 
 | ||||||
|     def close(self) -> None: |     def close(self) -> None: | ||||||
|         """ |         """ | ||||||
|         Closes the file pointer, if possible. |  | ||||||
| 
 |  | ||||||
|         This operation will destroy the image core and release its memory. |         This operation will destroy the image core and release its memory. | ||||||
|         The image data will be unusable afterward. |         The image data will be unusable afterward. | ||||||
| 
 | 
 | ||||||
|  | @ -629,13 +621,6 @@ class Image: | ||||||
|         :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for |         :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for | ||||||
|         more information. |         more information. | ||||||
|         """ |         """ | ||||||
|         if hasattr(self, "fp"): |  | ||||||
|             try: |  | ||||||
|                 self._close_fp() |  | ||||||
|                 self.fp = None |  | ||||||
|             except Exception as msg: |  | ||||||
|                 logger.debug("Error closing: %s", msg) |  | ||||||
| 
 |  | ||||||
|         if getattr(self, "map", None): |         if getattr(self, "map", None): | ||||||
|             self.map: mmap.mmap | None = None |             self.map: mmap.mmap | None = None | ||||||
| 
 | 
 | ||||||
|  | @ -1554,50 +1539,10 @@ class Image: | ||||||
|         self.getexif() |         self.getexif() | ||||||
| 
 | 
 | ||||||
|     def get_child_images(self) -> list[ImageFile.ImageFile]: |     def get_child_images(self) -> list[ImageFile.ImageFile]: | ||||||
|         child_images = [] |         from . import ImageFile | ||||||
|         exif = self.getexif() |  | ||||||
|         ifds = [] |  | ||||||
|         if ExifTags.Base.SubIFDs in exif: |  | ||||||
|             subifd_offsets = exif[ExifTags.Base.SubIFDs] |  | ||||||
|             if subifd_offsets: |  | ||||||
|                 if not isinstance(subifd_offsets, tuple): |  | ||||||
|                     subifd_offsets = (subifd_offsets,) |  | ||||||
|                 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(ExifTags.Base.JpegIFOffset): |  | ||||||
|             assert exif._info is not None |  | ||||||
|             ifds.append((ifd1, exif._info.next)) |  | ||||||
| 
 | 
 | ||||||
|         offset = None |         deprecate("Image.Image.get_child_images", 13) | ||||||
|         for ifd, ifd_offset in ifds: |         return ImageFile.ImageFile.get_child_images(self)  # type: ignore[arg-type] | ||||||
|             current_offset = self.fp.tell() |  | ||||||
|             if offset is None: |  | ||||||
|                 offset = current_offset |  | ||||||
| 
 |  | ||||||
|             fp = self.fp |  | ||||||
|             if ifd is not None: |  | ||||||
|                 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(ExifTags.Base.JpegIFByteCount)) |  | ||||||
|                     fp = io.BytesIO(data) |  | ||||||
| 
 |  | ||||||
|             with open(fp) as im: |  | ||||||
|                 from . import TiffImagePlugin |  | ||||||
| 
 |  | ||||||
|                 if thumbnail_offset is None and isinstance( |  | ||||||
|                     im, TiffImagePlugin.TiffImageFile |  | ||||||
|                 ): |  | ||||||
|                     im._frame_pos = [ifd_offset] |  | ||||||
|                     im._seek(0) |  | ||||||
|                 im.load() |  | ||||||
|                 child_images.append(im) |  | ||||||
| 
 |  | ||||||
|         if offset is not None: |  | ||||||
|             self.fp.seek(offset) |  | ||||||
|         return child_images |  | ||||||
| 
 | 
 | ||||||
|     def getim(self) -> CapsuleType: |     def getim(self) -> CapsuleType: | ||||||
|         """ |         """ | ||||||
|  | @ -3939,7 +3884,7 @@ class Exif(_ExifBase): | ||||||
|             return self._fixup_dict(dict(info)) |             return self._fixup_dict(dict(info)) | ||||||
| 
 | 
 | ||||||
|     def _get_head(self) -> bytes: |     def _get_head(self) -> bytes: | ||||||
|         version = b"\x2B" if self.bigtiff else b"\x2A" |         version = b"\x2b" if self.bigtiff else b"\x2a" | ||||||
|         if self.endian == "<": |         if self.endian == "<": | ||||||
|             head = b"II" + version + b"\x00" + o32le(8) |             head = b"II" + version + b"\x00" + o32le(8) | ||||||
|         else: |         else: | ||||||
|  |  | ||||||
|  | @ -557,21 +557,6 @@ class ImageDraw: | ||||||
| 
 | 
 | ||||||
|         return split_character in text |         return split_character in text | ||||||
| 
 | 
 | ||||||
|     def _multiline_split(self, text: AnyStr) -> list[AnyStr]: |  | ||||||
|         return text.split("\n" if isinstance(text, str) else b"\n") |  | ||||||
| 
 |  | ||||||
|     def _multiline_spacing( |  | ||||||
|         self, |  | ||||||
|         font: ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, |  | ||||||
|         spacing: float, |  | ||||||
|         stroke_width: float, |  | ||||||
|     ) -> float: |  | ||||||
|         return ( |  | ||||||
|             self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] |  | ||||||
|             + stroke_width |  | ||||||
|             + spacing |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|     def text( |     def text( | ||||||
|         self, |         self, | ||||||
|         xy: tuple[float, float], |         xy: tuple[float, float], | ||||||
|  | @ -643,6 +628,7 @@ class ImageDraw: | ||||||
|                     features=features, |                     features=features, | ||||||
|                     language=language, |                     language=language, | ||||||
|                     stroke_width=stroke_width, |                     stroke_width=stroke_width, | ||||||
|  |                     stroke_filled=True, | ||||||
|                     anchor=anchor, |                     anchor=anchor, | ||||||
|                     ink=ink, |                     ink=ink, | ||||||
|                     start=start, |                     start=start, | ||||||
|  | @ -692,11 +678,125 @@ class ImageDraw: | ||||||
|                 draw_text(stroke_ink, stroke_width) |                 draw_text(stroke_ink, stroke_width) | ||||||
| 
 | 
 | ||||||
|                 # Draw normal text |                 # Draw normal text | ||||||
|                 draw_text(ink, 0) |                 if ink != stroke_ink: | ||||||
|  |                     draw_text(ink) | ||||||
|             else: |             else: | ||||||
|                 # Only draw normal text |                 # Only draw normal text | ||||||
|                 draw_text(ink) |                 draw_text(ink) | ||||||
| 
 | 
 | ||||||
|  |     def _prepare_multiline_text( | ||||||
|  |         self, | ||||||
|  |         xy: tuple[float, float], | ||||||
|  |         text: AnyStr, | ||||||
|  |         font: ( | ||||||
|  |             ImageFont.ImageFont | ||||||
|  |             | ImageFont.FreeTypeFont | ||||||
|  |             | ImageFont.TransposedFont | ||||||
|  |             | None | ||||||
|  |         ), | ||||||
|  |         anchor: str | None, | ||||||
|  |         spacing: float, | ||||||
|  |         align: str, | ||||||
|  |         direction: str | None, | ||||||
|  |         features: list[str] | None, | ||||||
|  |         language: str | None, | ||||||
|  |         stroke_width: float, | ||||||
|  |         embedded_color: bool, | ||||||
|  |         font_size: float | None, | ||||||
|  |     ) -> tuple[ | ||||||
|  |         ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont, | ||||||
|  |         str, | ||||||
|  |         list[tuple[tuple[float, float], AnyStr]], | ||||||
|  |     ]: | ||||||
|  |         if direction == "ttb": | ||||||
|  |             msg = "ttb direction is unsupported for multiline text" | ||||||
|  |             raise ValueError(msg) | ||||||
|  | 
 | ||||||
|  |         if anchor is None: | ||||||
|  |             anchor = "la" | ||||||
|  |         elif len(anchor) != 2: | ||||||
|  |             msg = "anchor must be a 2 character string" | ||||||
|  |             raise ValueError(msg) | ||||||
|  |         elif anchor[1] in "tb": | ||||||
|  |             msg = "anchor not supported for multiline text" | ||||||
|  |             raise ValueError(msg) | ||||||
|  | 
 | ||||||
|  |         if font is None: | ||||||
|  |             font = self._getfont(font_size) | ||||||
|  | 
 | ||||||
|  |         widths = [] | ||||||
|  |         max_width: float = 0 | ||||||
|  |         lines = text.split("\n" if isinstance(text, str) else b"\n") | ||||||
|  |         line_spacing = ( | ||||||
|  |             self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] | ||||||
|  |             + stroke_width | ||||||
|  |             + spacing | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         for line in lines: | ||||||
|  |             line_width = self.textlength( | ||||||
|  |                 line, | ||||||
|  |                 font, | ||||||
|  |                 direction=direction, | ||||||
|  |                 features=features, | ||||||
|  |                 language=language, | ||||||
|  |                 embedded_color=embedded_color, | ||||||
|  |             ) | ||||||
|  |             widths.append(line_width) | ||||||
|  |             max_width = max(max_width, line_width) | ||||||
|  | 
 | ||||||
|  |         top = xy[1] | ||||||
|  |         if anchor[1] == "m": | ||||||
|  |             top -= (len(lines) - 1) * line_spacing / 2.0 | ||||||
|  |         elif anchor[1] == "d": | ||||||
|  |             top -= (len(lines) - 1) * line_spacing | ||||||
|  | 
 | ||||||
|  |         parts = [] | ||||||
|  |         for idx, line in enumerate(lines): | ||||||
|  |             left = xy[0] | ||||||
|  |             width_difference = max_width - widths[idx] | ||||||
|  | 
 | ||||||
|  |             # first align left by anchor | ||||||
|  |             if anchor[0] == "m": | ||||||
|  |                 left -= width_difference / 2.0 | ||||||
|  |             elif anchor[0] == "r": | ||||||
|  |                 left -= width_difference | ||||||
|  | 
 | ||||||
|  |             # then align by align parameter | ||||||
|  |             if align in ("left", "justify"): | ||||||
|  |                 pass | ||||||
|  |             elif align == "center": | ||||||
|  |                 left += width_difference / 2.0 | ||||||
|  |             elif align == "right": | ||||||
|  |                 left += width_difference | ||||||
|  |             else: | ||||||
|  |                 msg = 'align must be "left", "center", "right" or "justify"' | ||||||
|  |                 raise ValueError(msg) | ||||||
|  | 
 | ||||||
|  |             if align == "justify" and width_difference != 0: | ||||||
|  |                 words = line.split(" " if isinstance(text, str) else b" ") | ||||||
|  |                 word_widths = [ | ||||||
|  |                     self.textlength( | ||||||
|  |                         word, | ||||||
|  |                         font, | ||||||
|  |                         direction=direction, | ||||||
|  |                         features=features, | ||||||
|  |                         language=language, | ||||||
|  |                         embedded_color=embedded_color, | ||||||
|  |                     ) | ||||||
|  |                     for word in words | ||||||
|  |                 ] | ||||||
|  |                 width_difference = max_width - sum(word_widths) | ||||||
|  |                 for i, word in enumerate(words): | ||||||
|  |                     parts.append(((left, top), word)) | ||||||
|  |                     left += word_widths[i] + width_difference / (len(words) - 1) | ||||||
|  |             else: | ||||||
|  |                 parts.append(((left, top), line)) | ||||||
|  | 
 | ||||||
|  |             top += line_spacing | ||||||
|  | 
 | ||||||
|  |         return font, anchor, parts | ||||||
|  | 
 | ||||||
|     def multiline_text( |     def multiline_text( | ||||||
|         self, |         self, | ||||||
|         xy: tuple[float, float], |         xy: tuple[float, float], | ||||||
|  | @ -720,62 +820,24 @@ class ImageDraw: | ||||||
|         *, |         *, | ||||||
|         font_size: float | None = None, |         font_size: float | None = None, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         if direction == "ttb": |         font, anchor, lines = self._prepare_multiline_text( | ||||||
|             msg = "ttb direction is unsupported for multiline text" |             xy, | ||||||
|             raise ValueError(msg) |             text, | ||||||
| 
 |             font, | ||||||
|         if anchor is None: |             anchor, | ||||||
|             anchor = "la" |             spacing, | ||||||
|         elif len(anchor) != 2: |             align, | ||||||
|             msg = "anchor must be a 2 character string" |             direction, | ||||||
|             raise ValueError(msg) |             features, | ||||||
|         elif anchor[1] in "tb": |             language, | ||||||
|             msg = "anchor not supported for multiline text" |             stroke_width, | ||||||
|             raise ValueError(msg) |             embedded_color, | ||||||
| 
 |             font_size, | ||||||
|         if font is None: |  | ||||||
|             font = self._getfont(font_size) |  | ||||||
| 
 |  | ||||||
|         widths = [] |  | ||||||
|         max_width: float = 0 |  | ||||||
|         lines = self._multiline_split(text) |  | ||||||
|         line_spacing = self._multiline_spacing(font, spacing, stroke_width) |  | ||||||
|         for line in lines: |  | ||||||
|             line_width = self.textlength( |  | ||||||
|                 line, font, direction=direction, features=features, language=language |  | ||||||
|         ) |         ) | ||||||
|             widths.append(line_width) |  | ||||||
|             max_width = max(max_width, line_width) |  | ||||||
| 
 |  | ||||||
|         top = xy[1] |  | ||||||
|         if anchor[1] == "m": |  | ||||||
|             top -= (len(lines) - 1) * line_spacing / 2.0 |  | ||||||
|         elif anchor[1] == "d": |  | ||||||
|             top -= (len(lines) - 1) * line_spacing |  | ||||||
| 
 |  | ||||||
|         for idx, line in enumerate(lines): |  | ||||||
|             left = xy[0] |  | ||||||
|             width_difference = max_width - widths[idx] |  | ||||||
| 
 |  | ||||||
|             # first align left by anchor |  | ||||||
|             if anchor[0] == "m": |  | ||||||
|                 left -= width_difference / 2.0 |  | ||||||
|             elif anchor[0] == "r": |  | ||||||
|                 left -= width_difference |  | ||||||
| 
 |  | ||||||
|             # then align by align parameter |  | ||||||
|             if align == "left": |  | ||||||
|                 pass |  | ||||||
|             elif align == "center": |  | ||||||
|                 left += width_difference / 2.0 |  | ||||||
|             elif align == "right": |  | ||||||
|                 left += width_difference |  | ||||||
|             else: |  | ||||||
|                 msg = 'align must be "left", "center" or "right"' |  | ||||||
|                 raise ValueError(msg) |  | ||||||
| 
 | 
 | ||||||
|  |         for xy, line in lines: | ||||||
|             self.text( |             self.text( | ||||||
|                 (left, top), |                 xy, | ||||||
|                 line, |                 line, | ||||||
|                 fill, |                 fill, | ||||||
|                 font, |                 font, | ||||||
|  | @ -787,7 +849,6 @@ class ImageDraw: | ||||||
|                 stroke_fill=stroke_fill, |                 stroke_fill=stroke_fill, | ||||||
|                 embedded_color=embedded_color, |                 embedded_color=embedded_color, | ||||||
|             ) |             ) | ||||||
|             top += line_spacing |  | ||||||
| 
 | 
 | ||||||
|     def textlength( |     def textlength( | ||||||
|         self, |         self, | ||||||
|  | @ -889,69 +950,26 @@ class ImageDraw: | ||||||
|         *, |         *, | ||||||
|         font_size: float | None = None, |         font_size: float | None = None, | ||||||
|     ) -> tuple[float, float, float, float]: |     ) -> tuple[float, float, float, float]: | ||||||
|         if direction == "ttb": |         font, anchor, lines = self._prepare_multiline_text( | ||||||
|             msg = "ttb direction is unsupported for multiline text" |             xy, | ||||||
|             raise ValueError(msg) |             text, | ||||||
| 
 |  | ||||||
|         if anchor is None: |  | ||||||
|             anchor = "la" |  | ||||||
|         elif len(anchor) != 2: |  | ||||||
|             msg = "anchor must be a 2 character string" |  | ||||||
|             raise ValueError(msg) |  | ||||||
|         elif anchor[1] in "tb": |  | ||||||
|             msg = "anchor not supported for multiline text" |  | ||||||
|             raise ValueError(msg) |  | ||||||
| 
 |  | ||||||
|         if font is None: |  | ||||||
|             font = self._getfont(font_size) |  | ||||||
| 
 |  | ||||||
|         widths = [] |  | ||||||
|         max_width: float = 0 |  | ||||||
|         lines = self._multiline_split(text) |  | ||||||
|         line_spacing = self._multiline_spacing(font, spacing, stroke_width) |  | ||||||
|         for line in lines: |  | ||||||
|             line_width = self.textlength( |  | ||||||
|                 line, |  | ||||||
|             font, |             font, | ||||||
|                 direction=direction, |             anchor, | ||||||
|                 features=features, |             spacing, | ||||||
|                 language=language, |             align, | ||||||
|                 embedded_color=embedded_color, |             direction, | ||||||
|  |             features, | ||||||
|  |             language, | ||||||
|  |             stroke_width, | ||||||
|  |             embedded_color, | ||||||
|  |             font_size, | ||||||
|         ) |         ) | ||||||
|             widths.append(line_width) |  | ||||||
|             max_width = max(max_width, line_width) |  | ||||||
| 
 |  | ||||||
|         top = xy[1] |  | ||||||
|         if anchor[1] == "m": |  | ||||||
|             top -= (len(lines) - 1) * line_spacing / 2.0 |  | ||||||
|         elif anchor[1] == "d": |  | ||||||
|             top -= (len(lines) - 1) * line_spacing |  | ||||||
| 
 | 
 | ||||||
|         bbox: tuple[float, float, float, float] | None = None |         bbox: tuple[float, float, float, float] | None = None | ||||||
| 
 | 
 | ||||||
|         for idx, line in enumerate(lines): |         for xy, line in lines: | ||||||
|             left = xy[0] |  | ||||||
|             width_difference = max_width - widths[idx] |  | ||||||
| 
 |  | ||||||
|             # first align left by anchor |  | ||||||
|             if anchor[0] == "m": |  | ||||||
|                 left -= width_difference / 2.0 |  | ||||||
|             elif anchor[0] == "r": |  | ||||||
|                 left -= width_difference |  | ||||||
| 
 |  | ||||||
|             # then align by align parameter |  | ||||||
|             if align == "left": |  | ||||||
|                 pass |  | ||||||
|             elif align == "center": |  | ||||||
|                 left += width_difference / 2.0 |  | ||||||
|             elif align == "right": |  | ||||||
|                 left += width_difference |  | ||||||
|             else: |  | ||||||
|                 msg = 'align must be "left", "center" or "right"' |  | ||||||
|                 raise ValueError(msg) |  | ||||||
| 
 |  | ||||||
|             bbox_line = self.textbbox( |             bbox_line = self.textbbox( | ||||||
|                 (left, top), |                 xy, | ||||||
|                 line, |                 line, | ||||||
|                 font, |                 font, | ||||||
|                 anchor, |                 anchor, | ||||||
|  | @ -971,8 +989,6 @@ class ImageDraw: | ||||||
|                     max(bbox[3], bbox_line[3]), |                     max(bbox[3], bbox_line[3]), | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|             top += line_spacing |  | ||||||
| 
 |  | ||||||
|         if bbox is None: |         if bbox is None: | ||||||
|             return xy[0], xy[1], xy[0], xy[1] |             return xy[0], xy[1], xy[0], xy[1] | ||||||
|         return bbox |         return bbox | ||||||
|  |  | ||||||
|  | @ -31,18 +31,21 @@ from __future__ import annotations | ||||||
| import abc | import abc | ||||||
| import io | import io | ||||||
| import itertools | import itertools | ||||||
|  | import logging | ||||||
| import os | import os | ||||||
| import struct | import struct | ||||||
| import sys | import sys | ||||||
| from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast | from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast | ||||||
| 
 | 
 | ||||||
| from . import Image | from . import ExifTags, Image | ||||||
| from ._deprecate import deprecate | from ._deprecate import deprecate | ||||||
| from ._util import is_path | from ._util import DeferredError, is_path | ||||||
| 
 | 
 | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from ._typing import StrOrBytesPath |     from ._typing import StrOrBytesPath | ||||||
| 
 | 
 | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  | 
 | ||||||
| MAXBLOCK = 65536 | MAXBLOCK = 65536 | ||||||
| 
 | 
 | ||||||
| SAFEBLOCK = 1024 * 1024 | SAFEBLOCK = 1024 * 1024 | ||||||
|  | @ -163,6 +166,85 @@ class ImageFile(Image.Image): | ||||||
|     def _open(self) -> None: |     def _open(self) -> None: | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
|  |     def _close_fp(self): | ||||||
|  |         if getattr(self, "_fp", False): | ||||||
|  |             if self._fp != self.fp: | ||||||
|  |                 self._fp.close() | ||||||
|  |             self._fp = DeferredError(ValueError("Operation on closed image")) | ||||||
|  |         if self.fp: | ||||||
|  |             self.fp.close() | ||||||
|  | 
 | ||||||
|  |     def close(self) -> None: | ||||||
|  |         """ | ||||||
|  |         Closes the file pointer, if possible. | ||||||
|  | 
 | ||||||
|  |         This operation will destroy the image core and release its memory. | ||||||
|  |         The image data will be unusable afterward. | ||||||
|  | 
 | ||||||
|  |         This function is required to close images that have multiple frames or | ||||||
|  |         have not had their file read and closed by the | ||||||
|  |         :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for | ||||||
|  |         more information. | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             self._close_fp() | ||||||
|  |             self.fp = None | ||||||
|  |         except Exception as msg: | ||||||
|  |             logger.debug("Error closing: %s", msg) | ||||||
|  | 
 | ||||||
|  |         super().close() | ||||||
|  | 
 | ||||||
|  |     def get_child_images(self) -> list[ImageFile]: | ||||||
|  |         child_images = [] | ||||||
|  |         exif = self.getexif() | ||||||
|  |         ifds = [] | ||||||
|  |         if ExifTags.Base.SubIFDs in exif: | ||||||
|  |             subifd_offsets = exif[ExifTags.Base.SubIFDs] | ||||||
|  |             if subifd_offsets: | ||||||
|  |                 if not isinstance(subifd_offsets, tuple): | ||||||
|  |                     subifd_offsets = (subifd_offsets,) | ||||||
|  |                 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(ExifTags.Base.JpegIFOffset): | ||||||
|  |             assert exif._info is not None | ||||||
|  |             ifds.append((ifd1, exif._info.next)) | ||||||
|  | 
 | ||||||
|  |         offset = None | ||||||
|  |         for ifd, ifd_offset in ifds: | ||||||
|  |             assert self.fp is not None | ||||||
|  |             current_offset = self.fp.tell() | ||||||
|  |             if offset is None: | ||||||
|  |                 offset = current_offset | ||||||
|  | 
 | ||||||
|  |             fp = self.fp | ||||||
|  |             if ifd is not None: | ||||||
|  |                 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) | ||||||
|  | 
 | ||||||
|  |                     length = ifd.get(ExifTags.Base.JpegIFByteCount) | ||||||
|  |                     assert isinstance(length, int) | ||||||
|  |                     data = self.fp.read(length) | ||||||
|  |                     fp = io.BytesIO(data) | ||||||
|  | 
 | ||||||
|  |             with Image.open(fp) as im: | ||||||
|  |                 from . import TiffImagePlugin | ||||||
|  | 
 | ||||||
|  |                 if thumbnail_offset is None and isinstance( | ||||||
|  |                     im, TiffImagePlugin.TiffImageFile | ||||||
|  |                 ): | ||||||
|  |                     im._frame_pos = [ifd_offset] | ||||||
|  |                     im._seek(0) | ||||||
|  |                 im.load() | ||||||
|  |                 child_images.append(im) | ||||||
|  | 
 | ||||||
|  |         if offset is not None: | ||||||
|  |             assert self.fp is not None | ||||||
|  |             self.fp.seek(offset) | ||||||
|  |         return child_images | ||||||
|  | 
 | ||||||
|     def get_format_mimetype(self) -> str | None: |     def get_format_mimetype(self) -> str | None: | ||||||
|         if self.custom_mimetype: |         if self.custom_mimetype: | ||||||
|             return self.custom_mimetype |             return self.custom_mimetype | ||||||
|  |  | ||||||
|  | @ -598,8 +598,6 @@ class Color3DLUT(MultibandFilter): | ||||||
|             self.mode or image.mode, |             self.mode or image.mode, | ||||||
|             Image.Resampling.BILINEAR, |             Image.Resampling.BILINEAR, | ||||||
|             self.channels, |             self.channels, | ||||||
|             self.size[0], |             self.size, | ||||||
|             self.size[1], |  | ||||||
|             self.size[2], |  | ||||||
|             self.table, |             self.table, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | @ -644,10 +644,10 @@ class FreeTypeFont: | ||||||
|             features, |             features, | ||||||
|             language, |             language, | ||||||
|             stroke_width, |             stroke_width, | ||||||
|  |             kwargs.get("stroke_filled", False), | ||||||
|             anchor, |             anchor, | ||||||
|             ink, |             ink, | ||||||
|             start[0], |             start, | ||||||
|             start[1], |  | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     def font_variant( |     def font_variant( | ||||||
|  |  | ||||||
|  | @ -48,9 +48,9 @@ class AffineTransform(Transform): | ||||||
|     Define an affine image transform. |     Define an affine image transform. | ||||||
| 
 | 
 | ||||||
|     This function takes a 6-tuple (a, b, c, d, e, f) which contain the first |     This function takes a 6-tuple (a, b, c, d, e, f) which contain the first | ||||||
|     two rows from an affine transform matrix. For each pixel (x, y) in the |     two rows from the inverse of an affine transform matrix. For each pixel | ||||||
|     output image, the new value is taken from a position (a x + b y + c, |     (x, y) in the output image, the new value is taken from a position (a x + | ||||||
|     d x + e y + f) in the input image, rounded to nearest pixel. |     b y + c, d x + e y + f) in the input image, rounded to nearest pixel. | ||||||
| 
 | 
 | ||||||
|     This function can be used to scale, translate, rotate, and shear the |     This function can be used to scale, translate, rotate, and shear the | ||||||
|     original image. |     original image. | ||||||
|  | @ -58,7 +58,7 @@ class AffineTransform(Transform): | ||||||
|     See :py:meth:`.Image.transform` |     See :py:meth:`.Image.transform` | ||||||
| 
 | 
 | ||||||
|     :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows |     :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows | ||||||
|         from an affine transform matrix. |         from the inverse of an affine transform matrix. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     method = Image.Transform.AFFINE |     method = Image.Transform.AFFINE | ||||||
|  |  | ||||||
|  | @ -55,7 +55,7 @@ class ImtImageFile(ImageFile.ImageFile): | ||||||
|             if not s: |             if not s: | ||||||
|                 break |                 break | ||||||
| 
 | 
 | ||||||
|             if s == b"\x0C": |             if s == b"\x0c": | ||||||
|                 # image data begins |                 # image data begins | ||||||
|                 self.tile = [ |                 self.tile = [ | ||||||
|                     ImageFile._Tile( |                     ImageFile._Tile( | ||||||
|  |  | ||||||
|  | @ -325,7 +325,7 @@ MARKER = { | ||||||
| 
 | 
 | ||||||
| def _accept(prefix: bytes) -> bool: | def _accept(prefix: bytes) -> bool: | ||||||
|     # Magic number was taken from https://en.wikipedia.org/wiki/JPEG |     # Magic number was taken from https://en.wikipedia.org/wiki/JPEG | ||||||
|     return prefix[:3] == b"\xFF\xD8\xFF" |     return prefix[:3] == b"\xff\xd8\xff" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| ## | ## | ||||||
|  | @ -342,7 +342,7 @@ class JpegImageFile(ImageFile.ImageFile): | ||||||
|         if not _accept(s): |         if not _accept(s): | ||||||
|             msg = "not a JPEG file" |             msg = "not a JPEG file" | ||||||
|             raise SyntaxError(msg) |             raise SyntaxError(msg) | ||||||
|         s = b"\xFF" |         s = b"\xff" | ||||||
| 
 | 
 | ||||||
|         # Create attributes |         # Create attributes | ||||||
|         self.bits = self.layers = 0 |         self.bits = self.layers = 0 | ||||||
|  | @ -417,7 +417,7 @@ class JpegImageFile(ImageFile.ImageFile): | ||||||
|             # Premature EOF. |             # Premature EOF. | ||||||
|             # Pretend file is finished adding EOI marker |             # Pretend file is finished adding EOI marker | ||||||
|             self._ended = True |             self._ended = True | ||||||
|             return b"\xFF\xD9" |             return b"\xff\xd9" | ||||||
| 
 | 
 | ||||||
|         return s |         return s | ||||||
| 
 | 
 | ||||||
|  | @ -712,7 +712,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | ||||||
|     def validate_qtables( |     def validate_qtables( | ||||||
|         qtables: ( |         qtables: ( | ||||||
|             str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None |             str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None | ||||||
|         ) |         ), | ||||||
|     ) -> list[list[int]] | None: |     ) -> list[list[int]] | None: | ||||||
|         if qtables is None: |         if qtables is None: | ||||||
|             return qtables |             return qtables | ||||||
|  | @ -769,7 +769,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | ||||||
|             msg = "XMP data is too long" |             msg = "XMP data is too long" | ||||||
|             raise ValueError(msg) |             raise ValueError(msg) | ||||||
|         size = o16(2 + overhead_len + len(xmp)) |         size = o16(2 + overhead_len + len(xmp)) | ||||||
|         extra += b"\xFF\xE1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp |         extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp | ||||||
| 
 | 
 | ||||||
|     icc_profile = info.get("icc_profile") |     icc_profile = info.get("icc_profile") | ||||||
|     if icc_profile: |     if icc_profile: | ||||||
|  | @ -783,7 +783,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | ||||||
|         for marker in markers: |         for marker in markers: | ||||||
|             size = o16(2 + overhead_len + len(marker)) |             size = o16(2 + overhead_len + len(marker)) | ||||||
|             extra += ( |             extra += ( | ||||||
|                 b"\xFF\xE2" |                 b"\xff\xe2" | ||||||
|                 + size |                 + size | ||||||
|                 + b"ICC_PROFILE\0" |                 + b"ICC_PROFILE\0" | ||||||
|                 + o8(i) |                 + o8(i) | ||||||
|  | @ -816,8 +816,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | ||||||
|         optimize, |         optimize, | ||||||
|         info.get("keep_rgb", False), |         info.get("keep_rgb", False), | ||||||
|         info.get("streamtype", 0), |         info.get("streamtype", 0), | ||||||
|         dpi[0], |         dpi, | ||||||
|         dpi[1], |  | ||||||
|         subsampling, |         subsampling, | ||||||
|         info.get("restart_marker_blocks", 0), |         info.get("restart_marker_blocks", 0), | ||||||
|         info.get("restart_marker_rows", 0), |         info.get("restart_marker_rows", 0), | ||||||
|  |  | ||||||
|  | @ -51,7 +51,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | ||||||
|             if not offsets: |             if not offsets: | ||||||
|                 # APP2 marker |                 # APP2 marker | ||||||
|                 im_frame.encoderinfo["extra"] = ( |                 im_frame.encoderinfo["extra"] = ( | ||||||
|                     b"\xFF\xE2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 |                     b"\xff\xe2" + struct.pack(">H", 6 + 82) + b"MPF\0" + b" " * 82 | ||||||
|                 ) |                 ) | ||||||
|                 exif = im_frame.encoderinfo.get("exif") |                 exif = im_frame.encoderinfo.get("exif") | ||||||
|                 if isinstance(exif, Image.Exif): |                 if isinstance(exif, Image.Exif): | ||||||
|  | @ -84,7 +84,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | ||||||
|     ifd[0xB002] = mpentries |     ifd[0xB002] = mpentries | ||||||
| 
 | 
 | ||||||
|     fp.seek(mpf_offset) |     fp.seek(mpf_offset) | ||||||
|     fp.write(b"II\x2A\x00" + o32le(8) + ifd.tobytes(8)) |     fp.write(b"II\x2a\x00" + o32le(8) + ifd.tobytes(8)) | ||||||
|     fp.seek(0, os.SEEK_END) |     fp.seek(0, os.SEEK_END) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | ||||||
|         + o16(dpi[0]) |         + o16(dpi[0]) | ||||||
|         + o16(dpi[1]) |         + o16(dpi[1]) | ||||||
|         + b"\0" * 24 |         + b"\0" * 24 | ||||||
|         + b"\xFF" * 24 |         + b"\xff" * 24 | ||||||
|         + b"\0" |         + b"\0" | ||||||
|         + o8(planes) |         + o8(planes) | ||||||
|         + o16(stride) |         + o16(stride) | ||||||
|  |  | ||||||
|  | @ -19,14 +19,14 @@ def encode_text(s: str) -> bytes: | ||||||
| 
 | 
 | ||||||
| PDFDocEncoding = { | PDFDocEncoding = { | ||||||
|     0x16: "\u0017", |     0x16: "\u0017", | ||||||
|     0x18: "\u02D8", |     0x18: "\u02d8", | ||||||
|     0x19: "\u02C7", |     0x19: "\u02c7", | ||||||
|     0x1A: "\u02C6", |     0x1A: "\u02c6", | ||||||
|     0x1B: "\u02D9", |     0x1B: "\u02d9", | ||||||
|     0x1C: "\u02DD", |     0x1C: "\u02dd", | ||||||
|     0x1D: "\u02DB", |     0x1D: "\u02db", | ||||||
|     0x1E: "\u02DA", |     0x1E: "\u02da", | ||||||
|     0x1F: "\u02DC", |     0x1F: "\u02dc", | ||||||
|     0x80: "\u2022", |     0x80: "\u2022", | ||||||
|     0x81: "\u2020", |     0x81: "\u2020", | ||||||
|     0x82: "\u2021", |     0x82: "\u2021", | ||||||
|  | @ -36,29 +36,29 @@ PDFDocEncoding = { | ||||||
|     0x86: "\u0192", |     0x86: "\u0192", | ||||||
|     0x87: "\u2044", |     0x87: "\u2044", | ||||||
|     0x88: "\u2039", |     0x88: "\u2039", | ||||||
|     0x89: "\u203A", |     0x89: "\u203a", | ||||||
|     0x8A: "\u2212", |     0x8A: "\u2212", | ||||||
|     0x8B: "\u2030", |     0x8B: "\u2030", | ||||||
|     0x8C: "\u201E", |     0x8C: "\u201e", | ||||||
|     0x8D: "\u201C", |     0x8D: "\u201c", | ||||||
|     0x8E: "\u201D", |     0x8E: "\u201d", | ||||||
|     0x8F: "\u2018", |     0x8F: "\u2018", | ||||||
|     0x90: "\u2019", |     0x90: "\u2019", | ||||||
|     0x91: "\u201A", |     0x91: "\u201a", | ||||||
|     0x92: "\u2122", |     0x92: "\u2122", | ||||||
|     0x93: "\uFB01", |     0x93: "\ufb01", | ||||||
|     0x94: "\uFB02", |     0x94: "\ufb02", | ||||||
|     0x95: "\u0141", |     0x95: "\u0141", | ||||||
|     0x96: "\u0152", |     0x96: "\u0152", | ||||||
|     0x97: "\u0160", |     0x97: "\u0160", | ||||||
|     0x98: "\u0178", |     0x98: "\u0178", | ||||||
|     0x99: "\u017D", |     0x99: "\u017d", | ||||||
|     0x9A: "\u0131", |     0x9A: "\u0131", | ||||||
|     0x9B: "\u0142", |     0x9B: "\u0142", | ||||||
|     0x9C: "\u0153", |     0x9C: "\u0153", | ||||||
|     0x9D: "\u0161", |     0x9D: "\u0161", | ||||||
|     0x9E: "\u017E", |     0x9E: "\u017e", | ||||||
|     0xA0: "\u20AC", |     0xA0: "\u20ac", | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1382,7 +1382,7 @@ def _save( | ||||||
|         b"\0",  # 12: interlace flag |         b"\0",  # 12: interlace flag | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"] |     chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"] | ||||||
| 
 | 
 | ||||||
|     icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) |     icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) | ||||||
|     if icc: |     if icc: | ||||||
|  | @ -1433,7 +1433,7 @@ def _save( | ||||||
|                 chunk(fp, b"tRNS", transparency[:alpha_bytes]) |                 chunk(fp, b"tRNS", transparency[:alpha_bytes]) | ||||||
|             else: |             else: | ||||||
|                 transparency = max(0, min(255, transparency)) |                 transparency = max(0, min(255, transparency)) | ||||||
|                 alpha = b"\xFF" * transparency + b"\0" |                 alpha = b"\xff" * transparency + b"\0" | ||||||
|                 chunk(fp, b"tRNS", alpha[:alpha_bytes]) |                 chunk(fp, b"tRNS", alpha[:alpha_bytes]) | ||||||
|         elif im.mode in ("1", "L", "I", "I;16"): |         elif im.mode in ("1", "L", "I", "I;16"): | ||||||
|             transparency = max(0, min(65535, transparency)) |             transparency = max(0, min(65535, transparency)) | ||||||
|  |  | ||||||
|  | @ -230,7 +230,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): | ||||||
|                     msg = b"Invalid token for this mode: %s" % bytes([token]) |                     msg = b"Invalid token for this mode: %s" % bytes([token]) | ||||||
|                     raise ValueError(msg) |                     raise ValueError(msg) | ||||||
|             data = (data + tokens)[:total_bytes] |             data = (data + tokens)[:total_bytes] | ||||||
|         invert = bytes.maketrans(b"01", b"\xFF\x00") |         invert = bytes.maketrans(b"01", b"\xff\x00") | ||||||
|         return data.translate(invert) |         return data.translate(invert) | ||||||
| 
 | 
 | ||||||
|     def _decode_blocks(self, maxval: int) -> bytearray: |     def _decode_blocks(self, maxval: int) -> bytearray: | ||||||
|  |  | ||||||
|  | @ -275,12 +275,12 @@ OPEN_INFO = { | ||||||
| MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) | MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) | ||||||
| 
 | 
 | ||||||
| PREFIXES = [ | PREFIXES = [ | ||||||
|     b"MM\x00\x2A",  # Valid TIFF header with big-endian byte order |     b"MM\x00\x2a",  # Valid TIFF header with big-endian byte order | ||||||
|     b"II\x2A\x00",  # Valid TIFF header with little-endian byte order |     b"II\x2a\x00",  # Valid TIFF header with little-endian byte order | ||||||
|     b"MM\x2A\x00",  # Invalid TIFF header, assume big-endian |     b"MM\x2a\x00",  # Invalid TIFF header, assume big-endian | ||||||
|     b"II\x00\x2A",  # Invalid TIFF header, assume little-endian |     b"II\x00\x2a",  # Invalid TIFF header, assume little-endian | ||||||
|     b"MM\x00\x2B",  # BigTIFF with big-endian byte order |     b"MM\x00\x2b",  # BigTIFF with big-endian byte order | ||||||
|     b"II\x2B\x00",  # BigTIFF with little-endian byte order |     b"II\x2b\x00",  # BigTIFF with little-endian byte order | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| if not getattr(Image.core, "libtiff_support_custom_tags", True): | if not getattr(Image.core, "libtiff_support_custom_tags", True): | ||||||
|  | @ -582,7 +582,7 @@ class ImageFileDirectory_v2(_IFDv2Base): | ||||||
| 
 | 
 | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, | ||||||
|         ifh: bytes = b"II\x2A\x00\x00\x00\x00\x00", |         ifh: bytes = b"II\x2a\x00\x00\x00\x00\x00", | ||||||
|         prefix: bytes | None = None, |         prefix: bytes | None = None, | ||||||
|         group: int | None = None, |         group: int | None = None, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|  | @ -2047,7 +2047,7 @@ class AppendingTiffWriter(io.BytesIO): | ||||||
|         self.offsetOfNewPage = 0 |         self.offsetOfNewPage = 0 | ||||||
| 
 | 
 | ||||||
|         self.IIMM = iimm = self.f.read(4) |         self.IIMM = iimm = self.f.read(4) | ||||||
|         self._bigtiff = b"\x2B" in iimm |         self._bigtiff = b"\x2b" in iimm | ||||||
|         if not iimm: |         if not iimm: | ||||||
|             # empty file - first page |             # empty file - first page | ||||||
|             self.isFirst = True |             self.isFirst = True | ||||||
|  | @ -2232,6 +2232,16 @@ class AppendingTiffWriter(io.BytesIO): | ||||||
|             if tag in self.Tags: |             if tag in self.Tags: | ||||||
|                 cur_pos = self.f.tell() |                 cur_pos = self.f.tell() | ||||||
| 
 | 
 | ||||||
|  |                 logger.debug( | ||||||
|  |                     "fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d", | ||||||
|  |                     TiffTags.lookup(tag).name, | ||||||
|  |                     tag, | ||||||
|  |                     TYPES.get(field_type, "unknown"), | ||||||
|  |                     field_type, | ||||||
|  |                     field_size, | ||||||
|  |                     count, | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|                 if is_local: |                 if is_local: | ||||||
|                     self._fixOffsets(count, field_size) |                     self._fixOffsets(count, field_size) | ||||||
|                     self.f.seek(cur_pos + fmt_size) |                     self.f.seek(cur_pos + fmt_size) | ||||||
|  |  | ||||||
|  | @ -223,8 +223,7 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | ||||||
| 
 | 
 | ||||||
|     # Setup the WebP animation encoder |     # Setup the WebP animation encoder | ||||||
|     enc = _webp.WebPAnimEncoder( |     enc = _webp.WebPAnimEncoder( | ||||||
|         im.size[0], |         im.size, | ||||||
|         im.size[1], |  | ||||||
|         background, |         background, | ||||||
|         loop, |         loop, | ||||||
|         minimize_size, |         minimize_size, | ||||||
|  |  | ||||||
|  | @ -47,6 +47,8 @@ def deprecate( | ||||||
|         raise RuntimeError(msg) |         raise RuntimeError(msg) | ||||||
|     elif when == 12: |     elif when == 12: | ||||||
|         removed = "Pillow 12 (2025-10-15)" |         removed = "Pillow 12 (2025-10-15)" | ||||||
|  |     elif when == 13: | ||||||
|  |         removed = "Pillow 13 (2026-10-15)" | ||||||
|     else: |     else: | ||||||
|         msg = f"Unknown removal version: {when}. Update {__name__}?" |         msg = f"Unknown removal version: {when}. Update {__name__}?" | ||||||
|         raise ValueError(msg) |         raise ValueError(msg) | ||||||
|  |  | ||||||
|  | @ -28,10 +28,10 @@ class Font: | ||||||
|         features: list[str] | None, |         features: list[str] | None, | ||||||
|         lang: str | None, |         lang: str | None, | ||||||
|         stroke_width: float, |         stroke_width: float, | ||||||
|  |         stroke_filled: bool, | ||||||
|         anchor: str | None, |         anchor: str | None, | ||||||
|         foreground_ink_long: int, |         foreground_ink_long: int, | ||||||
|         x_start: float, |         start: tuple[float, float], | ||||||
|         y_start: float, |  | ||||||
|         /, |         /, | ||||||
|     ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... |     ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ... | ||||||
|     def getsize( |     def getsize( | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| """ Find compiled module linking to Tcl / Tk libraries | """Find compiled module linking to Tcl / Tk libraries""" | ||||||
| """ |  | ||||||
| 
 | 
 | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										106
									
								
								src/_imaging.c
									
									
									
									
									
								
							
							
						
						
									
										106
									
								
								src/_imaging.c
									
									
									
									
									
								
							|  | @ -473,8 +473,7 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /* unknown type */ |     /* unknown type */ | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static char * | static char * | ||||||
|  | @ -867,7 +866,7 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     if (!PyArg_ParseTuple( |     if (!PyArg_ParseTuple( | ||||||
|             args, |             args, | ||||||
|             "siiiiiO:color_lut_3d", |             "sii(iii)O:color_lut_3d", | ||||||
|             &mode, |             &mode, | ||||||
|             &filter, |             &filter, | ||||||
|             &table_channels, |             &table_channels, | ||||||
|  | @ -965,8 +964,7 @@ _convert2(ImagingObject *self, PyObject *args) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -1015,10 +1013,6 @@ _convert_transparent(ImagingObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
| _copy(ImagingObject *self, PyObject *args) { | _copy(ImagingObject *self, PyObject *args) { | ||||||
|     if (!PyArg_ParseTuple(args, "")) { |  | ||||||
|         return NULL; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return PyImagingNew(ImagingCopy(self->image)); |     return PyImagingNew(ImagingCopy(self->image)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -1214,8 +1208,7 @@ _getpixel(ImagingObject *self, PyObject *args) { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (self->access == NULL) { |     if (self->access == NULL) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return getpixel(self->image, self->access, x, y); |     return getpixel(self->image, self->access, x, y); | ||||||
|  | @ -1417,8 +1410,7 @@ _paste(ImagingObject *self, PyObject *args) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -1691,8 +1683,7 @@ _putdata(ImagingObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     Py_XDECREF(seq); |     Py_XDECREF(seq); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -1752,8 +1743,7 @@ _putpalette(ImagingObject *self, PyObject *args) { | ||||||
|     self->image->palette->size = palettesize * 8 / bits; |     self->image->palette->size = palettesize * 8 / bits; | ||||||
|     unpack(self->image->palette->palette, palette, self->image->palette->size); |     unpack(self->image->palette->palette, palette, self->image->palette->size); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -1777,8 +1767,7 @@ _putpalettealpha(ImagingObject *self, PyObject *args) { | ||||||
|     strcpy(self->image->palette->mode, "RGBA"); |     strcpy(self->image->palette->mode, "RGBA"); | ||||||
|     self->image->palette->palette[index * 4 + 3] = (UINT8)alpha; |     self->image->palette->palette[index * 4 + 3] = (UINT8)alpha; | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -1805,8 +1794,7 @@ _putpalettealphas(ImagingObject *self, PyObject *args) { | ||||||
|         self->image->palette->palette[i * 4 + 3] = (UINT8)values[i]; |         self->image->palette->palette[i * 4 + 3] = (UINT8)values[i]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -1842,8 +1830,7 @@ _putpixel(ImagingObject *self, PyObject *args) { | ||||||
|         self->access->put_pixel(im, x, y, ink); |         self->access->put_pixel(im, x, y, ink); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -2010,8 +1997,7 @@ im_setmode(ImagingObject *self, PyObject *args) { | ||||||
|     } |     } | ||||||
|     self->access = ImagingAccessNew(im); |     self->access = ImagingAccessNew(im); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -2074,8 +2060,7 @@ _transform(ImagingObject *self, PyObject *args) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -2202,8 +2187,7 @@ _getbbox(ImagingObject *self, PyObject *args) { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (!ImagingGetBBox(self->image, bbox, alpha_only)) { |     if (!ImagingGetBBox(self->image, bbox, alpha_only)) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return Py_BuildValue("iiii", bbox[0], bbox[1], bbox[2], bbox[3]); |     return Py_BuildValue("iiii", bbox[0], bbox[1], bbox[2], bbox[3]); | ||||||
|  | @ -2283,8 +2267,7 @@ _getextrema(ImagingObject *self) { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -2347,8 +2330,7 @@ _fillband(ImagingObject *self, PyObject *args) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -2363,8 +2345,7 @@ _putband(ImagingObject *self, PyObject *args) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -2950,8 +2931,7 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -2988,8 +2968,7 @@ _draw_bitmap(ImagingDrawObject *self, PyObject *args) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -3045,8 +3024,7 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -3100,8 +3078,7 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -3164,8 +3141,7 @@ _draw_lines(ImagingDrawObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     free(xy); |     free(xy); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -3196,8 +3172,7 @@ _draw_points(ImagingDrawObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     free(xy); |     free(xy); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* from outline.c */ | /* from outline.c */ | ||||||
|  | @ -3225,8 +3200,7 @@ _draw_outline(ImagingDrawObject *self, PyObject *args) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -3282,8 +3256,7 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -3334,8 +3307,7 @@ _draw_polygon(ImagingDrawObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     free(ixy); |     free(ixy); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -3389,8 +3361,7 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static struct PyMethodDef _draw_methods[] = { | static struct PyMethodDef _draw_methods[] = { | ||||||
|  | @ -3595,8 +3566,7 @@ _save_ppm(ImagingObject *self, PyObject *args) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* -------------------------------------------------------------------- */ | /* -------------------------------------------------------------------- */ | ||||||
|  | @ -3984,8 +3954,7 @@ _reset_stats(PyObject *self, PyObject *args) { | ||||||
|     arena->stats_freed_blocks = 0; |     arena->stats_freed_blocks = 0; | ||||||
|     MUTEX_UNLOCK(&ImagingDefaultArena.mutex); |     MUTEX_UNLOCK(&ImagingDefaultArena.mutex); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -4045,8 +4014,7 @@ _set_alignment(PyObject *self, PyObject *args) { | ||||||
|     ImagingDefaultArena.alignment = alignment; |     ImagingDefaultArena.alignment = alignment; | ||||||
|     MUTEX_UNLOCK(&ImagingDefaultArena.mutex); |     MUTEX_UNLOCK(&ImagingDefaultArena.mutex); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -4070,8 +4038,7 @@ _set_block_size(PyObject *self, PyObject *args) { | ||||||
|     ImagingDefaultArena.block_size = block_size; |     ImagingDefaultArena.block_size = block_size; | ||||||
|     MUTEX_UNLOCK(&ImagingDefaultArena.mutex); |     MUTEX_UNLOCK(&ImagingDefaultArena.mutex); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -4099,8 +4066,7 @@ _set_blocks_max(PyObject *self, PyObject *args) { | ||||||
|         return ImagingError_MemoryError(); |         return ImagingError_MemoryError(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -4115,8 +4081,7 @@ _clear_cache(PyObject *self, PyObject *args) { | ||||||
|     ImagingMemoryClearCache(&ImagingDefaultArena, i); |     ImagingMemoryClearCache(&ImagingDefaultArena, i); | ||||||
|     MUTEX_UNLOCK(&ImagingDefaultArena.mutex); |     MUTEX_UNLOCK(&ImagingDefaultArena.mutex); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* -------------------------------------------------------------------- */ | /* -------------------------------------------------------------------- */ | ||||||
|  | @ -4470,10 +4435,9 @@ PyInit__imaging(void) { | ||||||
| 
 | 
 | ||||||
|     static PyModuleDef module_def = { |     static PyModuleDef module_def = { | ||||||
|         PyModuleDef_HEAD_INIT, |         PyModuleDef_HEAD_INIT, | ||||||
|         "_imaging", /* m_name */ |         .m_name = "_imaging", | ||||||
|         NULL,       /* m_doc */ |         .m_size = -1, | ||||||
|         -1,         /* m_size */ |         .m_methods = functions, | ||||||
|         functions,  /* m_methods */ |  | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     m = PyModule_Create(&module_def); |     m = PyModule_Create(&module_def); | ||||||
|  |  | ||||||
|  | @ -654,8 +654,7 @@ cms_get_display_profile_win32(PyObject *self, PyObject *args) { | ||||||
|         return PyUnicode_FromStringAndSize(filename, filename_size - 1); |         return PyUnicode_FromStringAndSize(filename, filename_size - 1); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|  | @ -672,20 +671,17 @@ _profile_read_mlu(CmsProfileObject *self, cmsTagSignature info) { | ||||||
|     wchar_t *buf; |     wchar_t *buf; | ||||||
| 
 | 
 | ||||||
|     if (!cmsIsTag(self->profile, info)) { |     if (!cmsIsTag(self->profile, info)) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     mlu = cmsReadTag(self->profile, info); |     mlu = cmsReadTag(self->profile, info); | ||||||
|     if (!mlu) { |     if (!mlu) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     len = cmsMLUgetWide(mlu, lc, cc, NULL, 0); |     len = cmsMLUgetWide(mlu, lc, cc, NULL, 0); | ||||||
|     if (len == 0) { |     if (len == 0) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     buf = malloc(len); |     buf = malloc(len); | ||||||
|  | @ -723,14 +719,12 @@ _profile_read_signature(CmsProfileObject *self, cmsTagSignature info) { | ||||||
|     unsigned int *sig; |     unsigned int *sig; | ||||||
| 
 | 
 | ||||||
|     if (!cmsIsTag(self->profile, info)) { |     if (!cmsIsTag(self->profile, info)) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     sig = (unsigned int *)cmsReadTag(self->profile, info); |     sig = (unsigned int *)cmsReadTag(self->profile, info); | ||||||
|     if (!sig) { |     if (!sig) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return _profile_read_int_as_string(*sig); |     return _profile_read_int_as_string(*sig); | ||||||
|  | @ -780,14 +774,12 @@ _profile_read_ciexyz(CmsProfileObject *self, cmsTagSignature info, int multi) { | ||||||
|     cmsCIEXYZ *XYZ; |     cmsCIEXYZ *XYZ; | ||||||
| 
 | 
 | ||||||
|     if (!cmsIsTag(self->profile, info)) { |     if (!cmsIsTag(self->profile, info)) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); |     XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); | ||||||
|     if (!XYZ) { |     if (!XYZ) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
|     if (multi) { |     if (multi) { | ||||||
|         return _xyz3_py(XYZ); |         return _xyz3_py(XYZ); | ||||||
|  | @ -801,14 +793,12 @@ _profile_read_ciexyy_triple(CmsProfileObject *self, cmsTagSignature info) { | ||||||
|     cmsCIExyYTRIPLE *triple; |     cmsCIExyYTRIPLE *triple; | ||||||
| 
 | 
 | ||||||
|     if (!cmsIsTag(self->profile, info)) { |     if (!cmsIsTag(self->profile, info)) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     triple = (cmsCIExyYTRIPLE *)cmsReadTag(self->profile, info); |     triple = (cmsCIExyYTRIPLE *)cmsReadTag(self->profile, info); | ||||||
|     if (!triple) { |     if (!triple) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /* Note: lcms does all the heavy lifting and error checking (nr of
 |     /* Note: lcms does all the heavy lifting and error checking (nr of
 | ||||||
|  | @ -835,21 +825,18 @@ _profile_read_named_color_list(CmsProfileObject *self, cmsTagSignature info) { | ||||||
|     PyObject *result; |     PyObject *result; | ||||||
| 
 | 
 | ||||||
|     if (!cmsIsTag(self->profile, info)) { |     if (!cmsIsTag(self->profile, info)) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ncl = (cmsNAMEDCOLORLIST *)cmsReadTag(self->profile, info); |     ncl = (cmsNAMEDCOLORLIST *)cmsReadTag(self->profile, info); | ||||||
|     if (ncl == NULL) { |     if (ncl == NULL) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     n = cmsNamedColorCount(ncl); |     n = cmsNamedColorCount(ncl); | ||||||
|     result = PyList_New(n); |     result = PyList_New(n); | ||||||
|     if (!result) { |     if (!result) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     for (i = 0; i < n; i++) { |     for (i = 0; i < n; i++) { | ||||||
|  | @ -858,8 +845,7 @@ _profile_read_named_color_list(CmsProfileObject *self, cmsTagSignature info) { | ||||||
|         str = PyUnicode_FromString(name); |         str = PyUnicode_FromString(name); | ||||||
|         if (str == NULL) { |         if (str == NULL) { | ||||||
|             Py_DECREF(result); |             Py_DECREF(result); | ||||||
|             Py_INCREF(Py_None); |             Py_RETURN_NONE; | ||||||
|             return Py_None; |  | ||||||
|         } |         } | ||||||
|         PyList_SET_ITEM(result, i, str); |         PyList_SET_ITEM(result, i, str); | ||||||
|     } |     } | ||||||
|  | @ -926,8 +912,7 @@ _is_intent_supported(CmsProfileObject *self, int clut) { | ||||||
| 
 | 
 | ||||||
|     result = PyDict_New(); |     result = PyDict_New(); | ||||||
|     if (result == NULL) { |     if (result == NULL) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     n = cmsGetSupportedIntents(INTENTS, intent_ids, intent_descs); |     n = cmsGetSupportedIntents(INTENTS, intent_ids, intent_descs); | ||||||
|  | @ -957,8 +942,7 @@ _is_intent_supported(CmsProfileObject *self, int clut) { | ||||||
|             Py_XDECREF(id); |             Py_XDECREF(id); | ||||||
|             Py_XDECREF(entry); |             Py_XDECREF(entry); | ||||||
|             Py_XDECREF(result); |             Py_XDECREF(result); | ||||||
|             Py_INCREF(Py_None); |             Py_RETURN_NONE; | ||||||
|             return Py_None; |  | ||||||
|         } |         } | ||||||
|         PyDict_SetItem(result, id, entry); |         PyDict_SetItem(result, id, entry); | ||||||
|         Py_DECREF(id); |         Py_DECREF(id); | ||||||
|  | @ -1042,8 +1026,7 @@ cms_profile_getattr_creation_date(CmsProfileObject *self, void *closure) { | ||||||
| 
 | 
 | ||||||
|     result = cmsGetHeaderCreationDateTime(self->profile, &ct); |     result = cmsGetHeaderCreationDateTime(self->profile, &ct); | ||||||
|     if (!result) { |     if (!result) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return PyDateTime_FromDateAndTime( |     return PyDateTime_FromDateAndTime( | ||||||
|  | @ -1141,8 +1124,7 @@ cms_profile_getattr_saturation_rendering_intent_gamut( | ||||||
| static PyObject * | static PyObject * | ||||||
| cms_profile_getattr_red_colorant(CmsProfileObject *self, void *closure) { | cms_profile_getattr_red_colorant(CmsProfileObject *self, void *closure) { | ||||||
|     if (!cmsIsMatrixShaper(self->profile)) { |     if (!cmsIsMatrixShaper(self->profile)) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
|     return _profile_read_ciexyz(self, cmsSigRedColorantTag, 0); |     return _profile_read_ciexyz(self, cmsSigRedColorantTag, 0); | ||||||
| } | } | ||||||
|  | @ -1150,8 +1132,7 @@ cms_profile_getattr_red_colorant(CmsProfileObject *self, void *closure) { | ||||||
| static PyObject * | static PyObject * | ||||||
| cms_profile_getattr_green_colorant(CmsProfileObject *self, void *closure) { | cms_profile_getattr_green_colorant(CmsProfileObject *self, void *closure) { | ||||||
|     if (!cmsIsMatrixShaper(self->profile)) { |     if (!cmsIsMatrixShaper(self->profile)) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
|     return _profile_read_ciexyz(self, cmsSigGreenColorantTag, 0); |     return _profile_read_ciexyz(self, cmsSigGreenColorantTag, 0); | ||||||
| } | } | ||||||
|  | @ -1159,8 +1140,7 @@ cms_profile_getattr_green_colorant(CmsProfileObject *self, void *closure) { | ||||||
| static PyObject * | static PyObject * | ||||||
| cms_profile_getattr_blue_colorant(CmsProfileObject *self, void *closure) { | cms_profile_getattr_blue_colorant(CmsProfileObject *self, void *closure) { | ||||||
|     if (!cmsIsMatrixShaper(self->profile)) { |     if (!cmsIsMatrixShaper(self->profile)) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
|     return _profile_read_ciexyz(self, cmsSigBlueColorantTag, 0); |     return _profile_read_ciexyz(self, cmsSigBlueColorantTag, 0); | ||||||
| } | } | ||||||
|  | @ -1176,21 +1156,18 @@ cms_profile_getattr_media_white_point_temperature( | ||||||
|     cmsBool result; |     cmsBool result; | ||||||
| 
 | 
 | ||||||
|     if (!cmsIsTag(self->profile, info)) { |     if (!cmsIsTag(self->profile, info)) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); |     XYZ = (cmsCIEXYZ *)cmsReadTag(self->profile, info); | ||||||
|     if (XYZ == NULL || XYZ->X == 0) { |     if (XYZ == NULL || XYZ->X == 0) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     cmsXYZ2xyY(&xyY, XYZ); |     cmsXYZ2xyY(&xyY, XYZ); | ||||||
|     result = cmsTempFromWhitePoint(&tempK, &xyY); |     result = cmsTempFromWhitePoint(&tempK, &xyY); | ||||||
|     if (!result) { |     if (!result) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
|     return PyFloat_FromDouble(tempK); |     return PyFloat_FromDouble(tempK); | ||||||
| } | } | ||||||
|  | @ -1229,8 +1206,7 @@ cms_profile_getattr_red_primary(CmsProfileObject *self, void *closure) { | ||||||
|         result = _calculate_rgb_primaries(self, &primaries); |         result = _calculate_rgb_primaries(self, &primaries); | ||||||
|     } |     } | ||||||
|     if (!result) { |     if (!result) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return _xyz_py(&primaries.Red); |     return _xyz_py(&primaries.Red); | ||||||
|  | @ -1245,8 +1221,7 @@ cms_profile_getattr_green_primary(CmsProfileObject *self, void *closure) { | ||||||
|         result = _calculate_rgb_primaries(self, &primaries); |         result = _calculate_rgb_primaries(self, &primaries); | ||||||
|     } |     } | ||||||
|     if (!result) { |     if (!result) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return _xyz_py(&primaries.Green); |     return _xyz_py(&primaries.Green); | ||||||
|  | @ -1261,8 +1236,7 @@ cms_profile_getattr_blue_primary(CmsProfileObject *self, void *closure) { | ||||||
|         result = _calculate_rgb_primaries(self, &primaries); |         result = _calculate_rgb_primaries(self, &primaries); | ||||||
|     } |     } | ||||||
|     if (!result) { |     if (!result) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return _xyz_py(&primaries.Blue); |     return _xyz_py(&primaries.Blue); | ||||||
|  | @ -1321,14 +1295,12 @@ cms_profile_getattr_icc_measurement_condition(CmsProfileObject *self, void *clos | ||||||
|     const char *geo; |     const char *geo; | ||||||
| 
 | 
 | ||||||
|     if (!cmsIsTag(self->profile, info)) { |     if (!cmsIsTag(self->profile, info)) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     mc = (cmsICCMeasurementConditions *)cmsReadTag(self->profile, info); |     mc = (cmsICCMeasurementConditions *)cmsReadTag(self->profile, info); | ||||||
|     if (!mc) { |     if (!mc) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (mc->Geometry == 1) { |     if (mc->Geometry == 1) { | ||||||
|  | @ -1362,14 +1334,12 @@ cms_profile_getattr_icc_viewing_condition(CmsProfileObject *self, void *closure) | ||||||
|     cmsTagSignature info = cmsSigViewingConditionsTag; |     cmsTagSignature info = cmsSigViewingConditionsTag; | ||||||
| 
 | 
 | ||||||
|     if (!cmsIsTag(self->profile, info)) { |     if (!cmsIsTag(self->profile, info)) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     vc = (cmsICCViewingConditions *)cmsReadTag(self->profile, info); |     vc = (cmsICCViewingConditions *)cmsReadTag(self->profile, info); | ||||||
|     if (!vc) { |     if (!vc) { | ||||||
|         Py_INCREF(Py_None); |         Py_RETURN_NONE; | ||||||
|         return Py_None; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return Py_BuildValue( |     return Py_BuildValue( | ||||||
|  | @ -1550,10 +1520,9 @@ PyInit__imagingcms(void) { | ||||||
| 
 | 
 | ||||||
|     static PyModuleDef module_def = { |     static PyModuleDef module_def = { | ||||||
|         PyModuleDef_HEAD_INIT, |         PyModuleDef_HEAD_INIT, | ||||||
|         "_imagingcms",    /* m_name */ |         .m_name = "_imagingcms", | ||||||
|         NULL,             /* m_doc */ |         .m_size = -1, | ||||||
|         -1,               /* m_size */ |         .m_methods = pyCMSdll_methods, | ||||||
|         pyCMSdll_methods, /* m_methods */ |  | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     m = PyModule_Create(&module_def); |     m = PyModule_Create(&module_def); | ||||||
|  |  | ||||||
|  | @ -834,6 +834,7 @@ font_render(FontObject *self, PyObject *args) { | ||||||
|     int mask = 0;  /* is FT_LOAD_TARGET_MONO enabled? */ |     int mask = 0;  /* is FT_LOAD_TARGET_MONO enabled? */ | ||||||
|     int color = 0; /* is FT_LOAD_COLOR enabled? */ |     int color = 0; /* is FT_LOAD_COLOR enabled? */ | ||||||
|     float stroke_width = 0; |     float stroke_width = 0; | ||||||
|  |     int stroke_filled = 0; | ||||||
|     PY_LONG_LONG foreground_ink_long = 0; |     PY_LONG_LONG foreground_ink_long = 0; | ||||||
|     unsigned int foreground_ink; |     unsigned int foreground_ink; | ||||||
|     const char *mode = NULL; |     const char *mode = NULL; | ||||||
|  | @ -853,7 +854,7 @@ font_render(FontObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     if (!PyArg_ParseTuple( |     if (!PyArg_ParseTuple( | ||||||
|             args, |             args, | ||||||
|             "OO|zzOzfzLffO:render", |             "OO|zzOzfpzL(ff):render", | ||||||
|             &string, |             &string, | ||||||
|             &fill, |             &fill, | ||||||
|             &mode, |             &mode, | ||||||
|  | @ -861,6 +862,7 @@ font_render(FontObject *self, PyObject *args) { | ||||||
|             &features, |             &features, | ||||||
|             &lang, |             &lang, | ||||||
|             &stroke_width, |             &stroke_width, | ||||||
|  |             &stroke_filled, | ||||||
|             &anchor, |             &anchor, | ||||||
|             &foreground_ink_long, |             &foreground_ink_long, | ||||||
|             &x_start, |             &x_start, | ||||||
|  | @ -1005,7 +1007,8 @@ font_render(FontObject *self, PyObject *args) { | ||||||
|         if (stroker != NULL) { |         if (stroker != NULL) { | ||||||
|             error = FT_Get_Glyph(glyph_slot, &glyph); |             error = FT_Get_Glyph(glyph_slot, &glyph); | ||||||
|             if (!error) { |             if (!error) { | ||||||
|                 error = FT_Glyph_Stroke(&glyph, stroker, 1); |                 error = stroke_filled ? FT_Glyph_StrokeBorder(&glyph, stroker, 0, 1) | ||||||
|  |                                       : FT_Glyph_Stroke(&glyph, stroker, 1); | ||||||
|             } |             } | ||||||
|             if (!error) { |             if (!error) { | ||||||
|                 FT_Vector origin = {0, 0}; |                 FT_Vector origin = {0, 0}; | ||||||
|  | @ -1371,8 +1374,7 @@ font_setvarname(FontObject *self, PyObject *args) { | ||||||
|         return geterror(error); |         return geterror(error); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -1426,8 +1428,7 @@ font_setvaraxes(FontObject *self, PyObject *args) { | ||||||
|         return geterror(error); |         return geterror(error); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|  | @ -1629,10 +1630,9 @@ PyInit__imagingft(void) { | ||||||
| 
 | 
 | ||||||
|     static PyModuleDef module_def = { |     static PyModuleDef module_def = { | ||||||
|         PyModuleDef_HEAD_INIT, |         PyModuleDef_HEAD_INIT, | ||||||
|         "_imagingft", /* m_name */ |         .m_name = "_imagingft", | ||||||
|         NULL,         /* m_doc */ |         .m_size = -1, | ||||||
|         -1,           /* m_size */ |         .m_methods = _functions, | ||||||
|         _functions,   /* m_methods */ |  | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     m = PyModule_Create(&module_def); |     m = PyModule_Create(&module_def); | ||||||
|  |  | ||||||
|  | @ -192,8 +192,7 @@ _unop(PyObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     unop(out, im1); |     unop(out, im1); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -226,8 +225,7 @@ _binop(PyObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     binop(out, im1, im2); |     binop(out, im1, im2); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyMethodDef _functions[] = { | static PyMethodDef _functions[] = { | ||||||
|  | @ -310,10 +308,9 @@ PyInit__imagingmath(void) { | ||||||
| 
 | 
 | ||||||
|     static PyModuleDef module_def = { |     static PyModuleDef module_def = { | ||||||
|         PyModuleDef_HEAD_INIT, |         PyModuleDef_HEAD_INIT, | ||||||
|         "_imagingmath", /* m_name */ |         .m_name = "_imagingmath", | ||||||
|         NULL,           /* m_doc */ |         .m_size = -1, | ||||||
|         -1,             /* m_size */ |         .m_methods = _functions, | ||||||
|         _functions,     /* m_methods */ |  | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     m = PyModule_Create(&module_def); |     m = PyModule_Create(&module_def); | ||||||
|  |  | ||||||
|  | @ -252,10 +252,10 @@ PyInit__imagingmorph(void) { | ||||||
| 
 | 
 | ||||||
|     static PyModuleDef module_def = { |     static PyModuleDef module_def = { | ||||||
|         PyModuleDef_HEAD_INIT, |         PyModuleDef_HEAD_INIT, | ||||||
|         "_imagingmorph",                       /* m_name */ |         .m_name = "_imagingmorph", | ||||||
|         "A module for doing image morphology", /* m_doc */ |         .m_doc = "A module for doing image morphology", | ||||||
|         -1,                                    /* m_size */ |         .m_size = -1, | ||||||
|         functions,                             /* m_methods */ |         .m_methods = functions, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     m = PyModule_Create(&module_def); |     m = PyModule_Create(&module_def); | ||||||
|  |  | ||||||
|  | @ -37,8 +37,7 @@ _tkinit(PyObject *self, PyObject *args) { | ||||||
|     /* This will bomb if interp is invalid... */ |     /* This will bomb if interp is invalid... */ | ||||||
|     TkImaging_Init(interp); |     TkImaging_Init(interp); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyMethodDef functions[] = { | static PyMethodDef functions[] = { | ||||||
|  | @ -51,10 +50,9 @@ PyMODINIT_FUNC | ||||||
| PyInit__imagingtk(void) { | PyInit__imagingtk(void) { | ||||||
|     static PyModuleDef module_def = { |     static PyModuleDef module_def = { | ||||||
|         PyModuleDef_HEAD_INIT, |         PyModuleDef_HEAD_INIT, | ||||||
|         "_imagingtk", /* m_name */ |         .m_name = "_imagingtk", | ||||||
|         NULL,         /* m_doc */ |         .m_size = -1, | ||||||
|         -1,           /* m_size */ |         .m_methods = functions, | ||||||
|         functions,    /* m_methods */ |  | ||||||
|     }; |     }; | ||||||
|     PyObject *m; |     PyObject *m; | ||||||
|     m = PyModule_Create(&module_def); |     m = PyModule_Create(&module_def); | ||||||
|  |  | ||||||
|  | @ -164,7 +164,7 @@ _anim_encoder_new(PyObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     if (!PyArg_ParseTuple( |     if (!PyArg_ParseTuple( | ||||||
|             args, |             args, | ||||||
|             "iiIiiiiii", |             "(ii)Iiiiiii", | ||||||
|             &width, |             &width, | ||||||
|             &height, |             &height, | ||||||
|             &bgcolor, |             &bgcolor, | ||||||
|  | @ -835,10 +835,9 @@ PyInit__webp(void) { | ||||||
| 
 | 
 | ||||||
|     static PyModuleDef module_def = { |     static PyModuleDef module_def = { | ||||||
|         PyModuleDef_HEAD_INIT, |         PyModuleDef_HEAD_INIT, | ||||||
|         "_webp",     /* m_name */ |         .m_name = "_webp", | ||||||
|         NULL,        /* m_doc */ |         .m_size = -1, | ||||||
|         -1,          /* m_size */ |         .m_methods = webpMethods, | ||||||
|         webpMethods, /* m_methods */ |  | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     m = PyModule_Create(&module_def); |     m = PyModule_Create(&module_def); | ||||||
|  |  | ||||||
|  | @ -213,8 +213,7 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { | ||||||
|     Py_XDECREF(decoder->lock); |     Py_XDECREF(decoder->lock); | ||||||
|     decoder->lock = op; |     decoder->lock = op; | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -231,8 +230,7 @@ _setfd(ImagingDecoderObject *decoder, PyObject *args) { | ||||||
|     Py_XINCREF(fd); |     Py_XINCREF(fd); | ||||||
|     state->fd = fd; |     state->fd = fd; | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  |  | ||||||
|  | @ -85,8 +85,7 @@ _expose(ImagingDisplayObject *display, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     ImagingExposeDIB(display->dib, hdc); |     ImagingExposeDIB(display->dib, hdc); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -112,8 +111,7 @@ _draw(ImagingDisplayObject *display, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     ImagingDrawDIB(display->dib, hdc, dst, src); |     ImagingDrawDIB(display->dib, hdc, dst, src); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| extern Imaging | extern Imaging | ||||||
|  | @ -143,8 +141,7 @@ _paste(ImagingDisplayObject *display, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     ImagingPasteDIB(display->dib, im, xy); |     ImagingPasteDIB(display->dib, im, xy); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -190,8 +187,7 @@ _releasedc(ImagingDisplayObject *display, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     ReleaseDC(window, dc); |     ReleaseDC(window, dc); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -211,8 +207,7 @@ _frombytes(ImagingDisplayObject *display, PyObject *args) { | ||||||
|     memcpy(display->dib->bits, buffer.buf, buffer.len); |     memcpy(display->dib->bits, buffer.buf, buffer.len); | ||||||
| 
 | 
 | ||||||
|     PyBuffer_Release(&buffer); |     PyBuffer_Release(&buffer); | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -709,8 +704,7 @@ PyImaging_EventLoopWin32(PyObject *self, PyObject *args) { | ||||||
|     } |     } | ||||||
|     Py_END_ALLOW_THREADS; |     Py_END_ALLOW_THREADS; | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* -------------------------------------------------------------------- */ | /* -------------------------------------------------------------------- */ | ||||||
|  |  | ||||||
|  | @ -278,8 +278,7 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { | ||||||
|     Py_XDECREF(encoder->lock); |     Py_XDECREF(encoder->lock); | ||||||
|     encoder->lock = op; |     encoder->lock = op; | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -296,8 +295,7 @@ _setfd(ImagingEncoderObject *encoder, PyObject *args) { | ||||||
|     Py_XINCREF(fd); |     Py_XINCREF(fd); | ||||||
|     state->fd = fd; |     state->fd = fd; | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -1099,7 +1097,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     if (!PyArg_ParseTuple( |     if (!PyArg_ParseTuple( | ||||||
|             args, |             args, | ||||||
|             "ss|nnnnpnnnnnnOz#y#y#", |             "ss|nnnnpn(nn)nnnOz#y#y#", | ||||||
|             &mode, |             &mode, | ||||||
|             &rawmode, |             &rawmode, | ||||||
|             &quality, |             &quality, | ||||||
|  |  | ||||||
|  | @ -1,72 +0,0 @@ | ||||||
| /*
 |  | ||||||
|  * The Python Imaging Library |  | ||||||
|  * $Id$ |  | ||||||
|  * |  | ||||||
|  * default exception handling |  | ||||||
|  * |  | ||||||
|  * This module is usually overridden by application code (e.g. |  | ||||||
|  * _imaging.c for PIL's standard Python bindings).  If you get |  | ||||||
|  * linking errors, remove this file from your project/library. |  | ||||||
|  * |  | ||||||
|  * history: |  | ||||||
|  * 1995-06-15 fl   Created |  | ||||||
|  * 1998-12-29 fl   Minor tweaks |  | ||||||
|  * 2003-09-13 fl   Added ImagingEnter/LeaveSection() |  | ||||||
|  * |  | ||||||
|  * Copyright (c) 1997-2003 by Secret Labs AB. |  | ||||||
|  * Copyright (c) 1995-2003 by Fredrik Lundh. |  | ||||||
|  * |  | ||||||
|  * See the README file for information on usage and redistribution. |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| #include "Imaging.h" |  | ||||||
| 
 |  | ||||||
| /* exception state */ |  | ||||||
| 
 |  | ||||||
| void * |  | ||||||
| ImagingError_OSError(void) { |  | ||||||
|     fprintf(stderr, "*** exception: file access error\n"); |  | ||||||
|     return NULL; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void * |  | ||||||
| ImagingError_MemoryError(void) { |  | ||||||
|     fprintf(stderr, "*** exception: out of memory\n"); |  | ||||||
|     return NULL; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void * |  | ||||||
| ImagingError_ModeError(void) { |  | ||||||
|     return ImagingError_ValueError("bad image mode"); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void * |  | ||||||
| ImagingError_Mismatch(void) { |  | ||||||
|     return ImagingError_ValueError("images don't match"); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void * |  | ||||||
| ImagingError_ValueError(const char *message) { |  | ||||||
|     if (!message) { |  | ||||||
|         message = "exception: bad argument to function"; |  | ||||||
|     } |  | ||||||
|     fprintf(stderr, "*** %s\n", message); |  | ||||||
|     return NULL; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void |  | ||||||
| ImagingError_Clear(void) { |  | ||||||
|     /* nop */; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /* thread state */ |  | ||||||
| 
 |  | ||||||
| void |  | ||||||
| ImagingSectionEnter(ImagingSectionCookie *cookie) { |  | ||||||
|     /* pass */ |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void |  | ||||||
| ImagingSectionLeave(ImagingSectionCookie *cookie) { |  | ||||||
|     /* pass */ |  | ||||||
| } |  | ||||||
|  | @ -609,10 +609,6 @@ ImagingLibTiffDecode( | ||||||
| extern int | extern int | ||||||
| ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); | ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int bytes); | ||||||
| #endif | #endif | ||||||
| #ifdef HAVE_LIBMPEG |  | ||||||
| extern int |  | ||||||
| ImagingMpegDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); |  | ||||||
| #endif |  | ||||||
| extern int | extern int | ||||||
| ImagingMspDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); | ImagingMspDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t bytes); | ||||||
| extern int | extern int | ||||||
|  |  | ||||||
|  | @ -89,8 +89,7 @@ _outline_move(OutlineObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     ImagingOutlineMove(self->outline, x0, y0); |     ImagingOutlineMove(self->outline, x0, y0); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -102,8 +101,7 @@ _outline_line(OutlineObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     ImagingOutlineLine(self->outline, x1, y1); |     ImagingOutlineLine(self->outline, x1, y1); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -115,8 +113,7 @@ _outline_curve(OutlineObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     ImagingOutlineCurve(self->outline, x1, y1, x2, y2, x3, y3); |     ImagingOutlineCurve(self->outline, x1, y1, x2, y2, x3, y3); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -127,8 +124,7 @@ _outline_close(OutlineObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     ImagingOutlineClose(self->outline); |     ImagingOutlineClose(self->outline); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  | @ -140,8 +136,7 @@ _outline_transform(OutlineObject *self, PyObject *args) { | ||||||
| 
 | 
 | ||||||
|     ImagingOutlineTransform(self->outline, a); |     ImagingOutlineTransform(self->outline, a); | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static struct PyMethodDef _outline_methods[] = { | static struct PyMethodDef _outline_methods[] = { | ||||||
|  |  | ||||||
|  | @ -415,8 +415,7 @@ path_map(PyPathObject *self, PyObject *args) { | ||||||
|     } |     } | ||||||
|     self->mapping = 0; |     self->mapping = 0; | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static int | static int | ||||||
|  | @ -528,8 +527,7 @@ path_transform(PyPathObject *self, PyObject *args) { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Py_INCREF(Py_None); |     Py_RETURN_NONE; | ||||||
|     return Py_None; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static struct PyMethodDef methods[] = { | static struct PyMethodDef methods[] = { | ||||||
|  |  | ||||||
|  | @ -11,10 +11,11 @@ For more extensive info, see the [Windows build instructions](build.rst). | ||||||
| * Requires Microsoft Visual Studio 2017 or newer with C++ component. | * Requires Microsoft Visual Studio 2017 or newer with C++ component. | ||||||
| * Requires NASM for libjpeg-turbo, a required dependency when using this script. | * Requires NASM for libjpeg-turbo, a required dependency when using this script. | ||||||
| * Requires CMake 3.15 or newer (available as Visual Studio component). | * Requires CMake 3.15 or newer (available as Visual Studio component). | ||||||
| * Tested on Windows Server 2019 with Visual Studio 2019 Community and Visual Studio 2022 Community (AppVeyor). | * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise and Windows Server | ||||||
| * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). |   2019 with Visual Studio 2019 Enterprise (GitHub Actions). | ||||||
|  | 
 | ||||||
|  | Here's an example script to build on Windows: | ||||||
| 
 | 
 | ||||||
| The following is a simplified version of the script used on AppVeyor: |  | ||||||
| ``` | ``` | ||||||
| set PYTHON=C:\Python39\bin | set PYTHON=C:\Python39\bin | ||||||
| cd /D C:\Pillow\winbuild | cd /D C:\Pillow\winbuild | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ Building Pillow on Windows | ||||||
|           be sufficient. |           be sufficient. | ||||||
| 
 | 
 | ||||||
| This page describes the steps necessary to build Pillow using the same | This page describes the steps necessary to build Pillow using the same | ||||||
| scripts used on GitHub Actions and AppVeyor CIs. | scripts used on GitHub Actions CI. | ||||||
| 
 | 
 | ||||||
| Prerequisites | Prerequisites | ||||||
| ------------- | ------------- | ||||||
|  | @ -112,7 +112,7 @@ directory. | ||||||
| Example | Example | ||||||
| ------- | ------- | ||||||
| 
 | 
 | ||||||
| The following is a simplified version of the script used on AppVeyor:: | Here's an example script to build on Windows:: | ||||||
| 
 | 
 | ||||||
|     set PYTHON=C:\Python39\bin |     set PYTHON=C:\Python39\bin | ||||||
|     cd /D C:\Pillow\winbuild |     cd /D C:\Pillow\winbuild | ||||||
|  |  | ||||||
|  | @ -113,14 +113,14 @@ V = { | ||||||
|     "BROTLI": "1.1.0", |     "BROTLI": "1.1.0", | ||||||
|     "FREETYPE": "2.13.3", |     "FREETYPE": "2.13.3", | ||||||
|     "FRIBIDI": "1.0.16", |     "FRIBIDI": "1.0.16", | ||||||
|     "HARFBUZZ": "10.1.0", |     "HARFBUZZ": "10.2.0", | ||||||
|     "JPEGTURBO": "3.1.0", |     "JPEGTURBO": "3.1.0", | ||||||
|     "LCMS2": "2.16", |     "LCMS2": "2.16", | ||||||
|     "LIBPNG": "1.6.45", |     "LIBPNG": "1.6.46", | ||||||
|     "LIBWEBP": "1.5.0", |     "LIBWEBP": "1.5.0", | ||||||
|     "OPENJPEG": "2.5.3", |     "OPENJPEG": "2.5.3", | ||||||
|     "TIFF": "4.6.0", |     "TIFF": "4.6.0", | ||||||
|     "XZ": "5.6.3", |     "XZ": "5.6.4", | ||||||
|     "ZLIBNG": "2.2.3", |     "ZLIBNG": "2.2.3", | ||||||
| } | } | ||||||
| V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) | V["LIBPNG_XY"] = "".join(V["LIBPNG"].split(".")[:2]) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user