Merge branch 'main' into progress
| 
						 | 
					@ -10,7 +10,7 @@ environment:
 | 
				
			||||||
  TEST_OPTIONS:
 | 
					  TEST_OPTIONS:
 | 
				
			||||||
  DEPLOY: YES
 | 
					  DEPLOY: YES
 | 
				
			||||||
  matrix:
 | 
					  matrix:
 | 
				
			||||||
  - PYTHON: C:/Python311
 | 
					  - PYTHON: C:/Python312
 | 
				
			||||||
    ARCHITECTURE: x86
 | 
					    ARCHITECTURE: x86
 | 
				
			||||||
    APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
 | 
					    APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
 | 
				
			||||||
  - PYTHON: C:/Python38-x64
 | 
					  - PYTHON: C:/Python38-x64
 | 
				
			||||||
| 
						 | 
					@ -43,7 +43,7 @@ build_script:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test_script:
 | 
					test_script:
 | 
				
			||||||
- cd c:\pillow
 | 
					- cd c:\pillow
 | 
				
			||||||
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout'
 | 
					- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma'
 | 
				
			||||||
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
 | 
					- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
 | 
				
			||||||
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
 | 
					- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
 | 
				
			||||||
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
 | 
					- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,8 @@ fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
python3 -m pip install --upgrade pip
 | 
					python3 -m pip install --upgrade pip
 | 
				
			||||||
python3 -m pip install --upgrade wheel
 | 
					python3 -m pip install --upgrade wheel
 | 
				
			||||||
PYTHONOPTIMIZE=0 python3 -m pip install cffi
 | 
					# TODO Update condition when cffi supports 3.13
 | 
				
			||||||
 | 
					if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
 | 
				
			||||||
python3 -m pip install coverage
 | 
					python3 -m pip install coverage
 | 
				
			||||||
python3 -m pip install defusedxml
 | 
					python3 -m pip install defusedxml
 | 
				
			||||||
python3 -m pip install olefile
 | 
					python3 -m pip install olefile
 | 
				
			||||||
| 
						 | 
					@ -38,7 +39,8 @@ python3 -m pip install -U pytest-timeout
 | 
				
			||||||
python3 -m pip install pyroma
 | 
					python3 -m pip install pyroma
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if [[ $(uname) != CYGWIN* ]]; then
 | 
					if [[ $(uname) != CYGWIN* ]]; then
 | 
				
			||||||
    python3 -m pip install numpy
 | 
					    # TODO Update condition when NumPy supports 3.13
 | 
				
			||||||
 | 
					    if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # PyQt6 doesn't support PyPy3
 | 
					    # PyQt6 doesn't support PyPy3
 | 
				
			||||||
    if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
 | 
					    if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
 | 
				
			||||||
| 
						 | 
					@ -46,6 +48,16 @@ if [[ $(uname) != CYGWIN* ]]; then
 | 
				
			||||||
        python3 -m pip install pyqt6
 | 
					        python3 -m pip install pyqt6
 | 
				
			||||||
    fi
 | 
					    fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Pyroma uses non-isolated build and fails with old setuptools
 | 
				
			||||||
 | 
					    if [[
 | 
				
			||||||
 | 
					        $GHA_PYTHON_VERSION == pypy3.9
 | 
				
			||||||
 | 
					        || $GHA_PYTHON_VERSION == 3.8
 | 
				
			||||||
 | 
					        || $GHA_PYTHON_VERSION == 3.9
 | 
				
			||||||
 | 
					    ]]; then
 | 
				
			||||||
 | 
					        # To match pyproject.toml
 | 
				
			||||||
 | 
					        python3 -m pip install "setuptools>=67.8"
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # webp
 | 
					    # webp
 | 
				
			||||||
    pushd depends && ./install_webp.sh && popd
 | 
					    pushd depends && ./install_webp.sh && popd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								.ci/requirements-cibw.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					cibuildwheel==2.16.2
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,7 @@ indent_style = space
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trim_trailing_whitespace = true
 | 
					trim_trailing_whitespace = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[*.yml]
 | 
					[*.{toml,yml}]
 | 
				
			||||||
# Two-space indentation
 | 
					# Two-space indentation
 | 
				
			||||||
indent_size = 2
 | 
					indent_size = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										6
									
								
								.github/workflows/cifuzz.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -2,6 +2,8 @@ name: CIFuzz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
on:
 | 
					on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - "**"
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
      - ".github/workflows/cifuzz.yml"
 | 
					      - ".github/workflows/cifuzz.yml"
 | 
				
			||||||
      - "**.c"
 | 
					      - "**.c"
 | 
				
			||||||
| 
						 | 
					@ -40,13 +42,13 @@ jobs:
 | 
				
			||||||
        language: python
 | 
					        language: python
 | 
				
			||||||
        dry-run: false
 | 
					        dry-run: false
 | 
				
			||||||
    - name: Upload New Crash
 | 
					    - name: Upload New Crash
 | 
				
			||||||
      uses: actions/upload-artifact@v3
 | 
					      uses: actions/upload-artifact@v4
 | 
				
			||||||
      if: failure() && steps.build.outcome == 'success'
 | 
					      if: failure() && steps.build.outcome == 'success'
 | 
				
			||||||
      with:
 | 
					      with:
 | 
				
			||||||
        name: artifacts
 | 
					        name: artifacts
 | 
				
			||||||
        path: ./out/artifacts
 | 
					        path: ./out/artifacts
 | 
				
			||||||
    - name: Upload Legacy Crash
 | 
					    - name: Upload Legacy Crash
 | 
				
			||||||
      uses: actions/upload-artifact@v3
 | 
					      uses: actions/upload-artifact@v4
 | 
				
			||||||
      if: steps.run.outcome == 'success'
 | 
					      if: steps.run.outcome == 'success'
 | 
				
			||||||
      with:
 | 
					      with:
 | 
				
			||||||
        name: crash
 | 
					        name: crash
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										4
									
								
								.github/workflows/docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -2,6 +2,8 @@ name: Docs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
on:
 | 
					on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - "**"
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
      - ".github/workflows/docs.yml"
 | 
					      - ".github/workflows/docs.yml"
 | 
				
			||||||
      - "docs/**"
 | 
					      - "docs/**"
 | 
				
			||||||
| 
						 | 
					@ -31,7 +33,7 @@ jobs:
 | 
				
			||||||
    - uses: actions/checkout@v4
 | 
					    - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - name: Set up Python
 | 
					    - name: Set up Python
 | 
				
			||||||
      uses: actions/setup-python@v4
 | 
					      uses: actions/setup-python@v5
 | 
				
			||||||
      with:
 | 
					      with:
 | 
				
			||||||
        python-version: "3.x"
 | 
					        python-version: "3.x"
 | 
				
			||||||
        cache: pip
 | 
					        cache: pip
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										8
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -2,6 +2,9 @@ name: Lint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
on: [push, pull_request, workflow_dispatch]
 | 
					on: [push, pull_request, workflow_dispatch]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  FORCE_COLOR: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
permissions:
 | 
					permissions:
 | 
				
			||||||
  contents: read
 | 
					  contents: read
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,7 +31,7 @@ jobs:
 | 
				
			||||||
          lint-pre-commit-
 | 
					          lint-pre-commit-
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - name: Set up Python
 | 
					    - name: Set up Python
 | 
				
			||||||
      uses: actions/setup-python@v4
 | 
					      uses: actions/setup-python@v5
 | 
				
			||||||
      with:
 | 
					      with:
 | 
				
			||||||
        python-version: "3.x"
 | 
					        python-version: "3.x"
 | 
				
			||||||
        cache: pip
 | 
					        cache: pip
 | 
				
			||||||
| 
						 | 
					@ -46,3 +49,6 @@ jobs:
 | 
				
			||||||
      run: tox -e lint
 | 
					      run: tox -e lint
 | 
				
			||||||
      env:
 | 
					      env:
 | 
				
			||||||
        PRE_COMMIT_COLOR: always
 | 
					        PRE_COMMIT_COLOR: always
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Mypy
 | 
				
			||||||
 | 
					      run: tox -e mypy
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										7
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -5,7 +5,9 @@ set -e
 | 
				
			||||||
brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
 | 
					brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm
 | 
				
			||||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
 | 
					export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PYTHONOPTIMIZE=0 python3 -m pip install cffi
 | 
					# TODO Update condition when cffi supports 3.13
 | 
				
			||||||
 | 
					if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
python3 -m pip install coverage
 | 
					python3 -m pip install coverage
 | 
				
			||||||
python3 -m pip install defusedxml
 | 
					python3 -m pip install defusedxml
 | 
				
			||||||
python3 -m pip install olefile
 | 
					python3 -m pip install olefile
 | 
				
			||||||
| 
						 | 
					@ -14,7 +16,8 @@ python3 -m pip install -U pytest-cov
 | 
				
			||||||
python3 -m pip install -U pytest-timeout
 | 
					python3 -m pip install -U pytest-timeout
 | 
				
			||||||
python3 -m pip install pyroma
 | 
					python3 -m pip install pyroma
 | 
				
			||||||
 | 
					
 | 
				
			||||||
python3 -m pip install numpy
 | 
					# TODO Update condition when NumPy supports 3.13
 | 
				
			||||||
 | 
					if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# extra test images
 | 
					# extra test images
 | 
				
			||||||
pushd depends && ./install_extra_test_images.sh && popd
 | 
					pushd depends && ./install_extra_test_images.sh && popd
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -20,7 +20,7 @@ jobs:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
    - name: "Check issues"
 | 
					    - name: "Check issues"
 | 
				
			||||||
      uses: actions/stale@v8
 | 
					      uses: actions/stale@v9
 | 
				
			||||||
      with:
 | 
					      with:
 | 
				
			||||||
        repo-token: ${{ secrets.GITHUB_TOKEN }}
 | 
					        repo-token: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
        only-labels: "Awaiting OP Action"
 | 
					        only-labels: "Awaiting OP Action"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										12
									
								
								.github/workflows/test-cygwin.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -2,13 +2,23 @@ name: Test Cygwin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
on:
 | 
					on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - "**"
 | 
				
			||||||
    paths-ignore:
 | 
					    paths-ignore:
 | 
				
			||||||
      - ".github/workflows/docs.yml"
 | 
					      - ".github/workflows/docs.yml"
 | 
				
			||||||
 | 
					      - ".github/workflows/wheels*"
 | 
				
			||||||
 | 
					      - ".gitmodules"
 | 
				
			||||||
 | 
					      - ".travis.yml"
 | 
				
			||||||
      - "docs/**"
 | 
					      - "docs/**"
 | 
				
			||||||
 | 
					      - "wheels/**"
 | 
				
			||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    paths-ignore:
 | 
					    paths-ignore:
 | 
				
			||||||
      - ".github/workflows/docs.yml"
 | 
					      - ".github/workflows/docs.yml"
 | 
				
			||||||
 | 
					      - ".github/workflows/wheels*"
 | 
				
			||||||
 | 
					      - ".gitmodules"
 | 
				
			||||||
 | 
					      - ".travis.yml"
 | 
				
			||||||
      - "docs/**"
 | 
					      - "docs/**"
 | 
				
			||||||
 | 
					      - "wheels/**"
 | 
				
			||||||
  workflow_dispatch:
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
permissions:
 | 
					permissions:
 | 
				
			||||||
| 
						 | 
					@ -122,7 +132,7 @@ jobs:
 | 
				
			||||||
          dash.exe -c "mkdir -p Tests/errors"
 | 
					          dash.exe -c "mkdir -p Tests/errors"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Upload errors
 | 
					      - name: Upload errors
 | 
				
			||||||
        uses: actions/upload-artifact@v3
 | 
					        uses: actions/upload-artifact@v4
 | 
				
			||||||
        if: failure()
 | 
					        if: failure()
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: errors
 | 
					          name: errors
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										12
									
								
								.github/workflows/test-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -2,13 +2,23 @@ name: Test Docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
on:
 | 
					on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - "**"
 | 
				
			||||||
    paths-ignore:
 | 
					    paths-ignore:
 | 
				
			||||||
      - ".github/workflows/docs.yml"
 | 
					      - ".github/workflows/docs.yml"
 | 
				
			||||||
 | 
					      - ".github/workflows/wheels*"
 | 
				
			||||||
 | 
					      - ".gitmodules"
 | 
				
			||||||
 | 
					      - ".travis.yml"
 | 
				
			||||||
      - "docs/**"
 | 
					      - "docs/**"
 | 
				
			||||||
 | 
					      - "wheels/**"
 | 
				
			||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    paths-ignore:
 | 
					    paths-ignore:
 | 
				
			||||||
      - ".github/workflows/docs.yml"
 | 
					      - ".github/workflows/docs.yml"
 | 
				
			||||||
 | 
					      - ".github/workflows/wheels*"
 | 
				
			||||||
 | 
					      - ".gitmodules"
 | 
				
			||||||
 | 
					      - ".travis.yml"
 | 
				
			||||||
      - "docs/**"
 | 
					      - "docs/**"
 | 
				
			||||||
 | 
					      - "wheels/**"
 | 
				
			||||||
  workflow_dispatch:
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
permissions:
 | 
					permissions:
 | 
				
			||||||
| 
						 | 
					@ -41,8 +51,8 @@ jobs:
 | 
				
			||||||
          debian-11-bullseye-amd64,
 | 
					          debian-11-bullseye-amd64,
 | 
				
			||||||
          debian-12-bookworm-x86,
 | 
					          debian-12-bookworm-x86,
 | 
				
			||||||
          debian-12-bookworm-amd64,
 | 
					          debian-12-bookworm-amd64,
 | 
				
			||||||
          fedora-37-amd64,
 | 
					 | 
				
			||||||
          fedora-38-amd64,
 | 
					          fedora-38-amd64,
 | 
				
			||||||
 | 
					          fedora-39-amd64,
 | 
				
			||||||
          gentoo,
 | 
					          gentoo,
 | 
				
			||||||
          ubuntu-20.04-focal-amd64,
 | 
					          ubuntu-20.04-focal-amd64,
 | 
				
			||||||
          ubuntu-22.04-jammy-amd64,
 | 
					          ubuntu-22.04-jammy-amd64,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										10
									
								
								.github/workflows/test-mingw.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -2,13 +2,23 @@ name: Test MinGW
 | 
				
			||||||
 | 
					
 | 
				
			||||||
on:
 | 
					on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - "**"
 | 
				
			||||||
    paths-ignore:
 | 
					    paths-ignore:
 | 
				
			||||||
      - ".github/workflows/docs.yml"
 | 
					      - ".github/workflows/docs.yml"
 | 
				
			||||||
 | 
					      - ".github/workflows/wheels*"
 | 
				
			||||||
 | 
					      - ".gitmodules"
 | 
				
			||||||
 | 
					      - ".travis.yml"
 | 
				
			||||||
      - "docs/**"
 | 
					      - "docs/**"
 | 
				
			||||||
 | 
					      - "wheels/**"
 | 
				
			||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    paths-ignore:
 | 
					    paths-ignore:
 | 
				
			||||||
      - ".github/workflows/docs.yml"
 | 
					      - ".github/workflows/docs.yml"
 | 
				
			||||||
 | 
					      - ".github/workflows/wheels*"
 | 
				
			||||||
 | 
					      - ".gitmodules"
 | 
				
			||||||
 | 
					      - ".travis.yml"
 | 
				
			||||||
      - "docs/**"
 | 
					      - "docs/**"
 | 
				
			||||||
 | 
					      - "wheels/**"
 | 
				
			||||||
  workflow_dispatch:
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
permissions:
 | 
					permissions:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										4
									
								
								.github/workflows/test-valgrind.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -1,9 +1,11 @@
 | 
				
			||||||
name: Test Valgrind
 | 
					name: Test Valgrind
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# like the docker tests, but running valgrind only on *.c/*.h changes.
 | 
					# like the Docker tests, but running valgrind only on *.c/*.h changes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
on:
 | 
					on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - "**"
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
      - ".github/workflows/test-valgrind.yml"
 | 
					      - ".github/workflows/test-valgrind.yml"
 | 
				
			||||||
      - "**.c"
 | 
					      - "**.c"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										67
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -4,11 +4,19 @@ on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    paths-ignore:
 | 
					    paths-ignore:
 | 
				
			||||||
      - ".github/workflows/docs.yml"
 | 
					      - ".github/workflows/docs.yml"
 | 
				
			||||||
 | 
					      - ".github/workflows/wheels*"
 | 
				
			||||||
 | 
					      - ".gitmodules"
 | 
				
			||||||
 | 
					      - ".travis.yml"
 | 
				
			||||||
      - "docs/**"
 | 
					      - "docs/**"
 | 
				
			||||||
 | 
					      - "wheels/**"
 | 
				
			||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    paths-ignore:
 | 
					    paths-ignore:
 | 
				
			||||||
      - ".github/workflows/docs.yml"
 | 
					      - ".github/workflows/docs.yml"
 | 
				
			||||||
 | 
					      - ".github/workflows/wheels*"
 | 
				
			||||||
 | 
					      - ".gitmodules"
 | 
				
			||||||
 | 
					      - ".travis.yml"
 | 
				
			||||||
      - "docs/**"
 | 
					      - "docs/**"
 | 
				
			||||||
 | 
					      - "wheels/**"
 | 
				
			||||||
  workflow_dispatch:
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
permissions:
 | 
					permissions:
 | 
				
			||||||
| 
						 | 
					@ -24,7 +32,7 @@ jobs:
 | 
				
			||||||
    strategy:
 | 
					    strategy:
 | 
				
			||||||
      fail-fast: false
 | 
					      fail-fast: false
 | 
				
			||||||
      matrix:
 | 
					      matrix:
 | 
				
			||||||
        python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12-dev"]
 | 
					        python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    timeout-minutes: 30
 | 
					    timeout-minutes: 30
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,25 +56,26 @@ jobs:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # sets env: pythonLocation
 | 
					    # sets env: pythonLocation
 | 
				
			||||||
    - name: Set up Python
 | 
					    - name: Set up Python
 | 
				
			||||||
      uses: actions/setup-python@v4
 | 
					      uses: actions/setup-python@v5
 | 
				
			||||||
      with:
 | 
					      with:
 | 
				
			||||||
        python-version: ${{ matrix.python-version }}
 | 
					        python-version: ${{ matrix.python-version }}
 | 
				
			||||||
 | 
					        allow-prereleases: true
 | 
				
			||||||
        cache: pip
 | 
					        cache: pip
 | 
				
			||||||
        cache-dependency-path: ".github/workflows/test-windows.yml"
 | 
					        cache-dependency-path: ".github/workflows/test-windows.yml"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - name: Print build system information
 | 
					    - name: Print build system information
 | 
				
			||||||
      run: python3 .github/workflows/system-info.py
 | 
					      run: python3 .github/workflows/system-info.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml
 | 
					    - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
 | 
				
			||||||
      run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml
 | 
					      run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - name: Install dependencies
 | 
					    - name: Install dependencies
 | 
				
			||||||
      id: install
 | 
					      id: install
 | 
				
			||||||
      run: |
 | 
					      run: |
 | 
				
			||||||
        7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\"
 | 
					        choco install nasm --no-progress
 | 
				
			||||||
        echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH
 | 
					        echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        choco install ghostscript --version=10.0.0.20230317
 | 
					        choco install ghostscript --version=10.0.0.20230317 --no-progress
 | 
				
			||||||
        echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
 | 
					        echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Install extra test images
 | 
					        # Install extra test images
 | 
				
			||||||
| 
						 | 
					@ -158,7 +167,6 @@ jobs:
 | 
				
			||||||
    - name: Build Pillow
 | 
					    - name: Build Pillow
 | 
				
			||||||
      run: |
 | 
					      run: |
 | 
				
			||||||
        $FLAGS="-C raqm=vendor -C fribidi=vendor"
 | 
					        $FLAGS="-C raqm=vendor -C fribidi=vendor"
 | 
				
			||||||
        if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS+=" -C imagequant=disable" }
 | 
					 | 
				
			||||||
        cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ."
 | 
					        cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ."
 | 
				
			||||||
        & $env:pythonLocation\python.exe selftest.py --installed
 | 
					        & $env:pythonLocation\python.exe selftest.py --installed
 | 
				
			||||||
      shell: pwsh
 | 
					      shell: pwsh
 | 
				
			||||||
| 
						 | 
					@ -182,7 +190,7 @@ jobs:
 | 
				
			||||||
      shell: bash
 | 
					      shell: bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - name: Upload errors
 | 
					    - name: Upload errors
 | 
				
			||||||
      uses: actions/upload-artifact@v3
 | 
					      uses: actions/upload-artifact@v4
 | 
				
			||||||
      if: failure()
 | 
					      if: failure()
 | 
				
			||||||
      with:
 | 
					      with:
 | 
				
			||||||
        name: errors
 | 
					        name: errors
 | 
				
			||||||
| 
						 | 
					@ -200,47 +208,6 @@ jobs:
 | 
				
			||||||
        flags: GHA_Windows
 | 
					        flags: GHA_Windows
 | 
				
			||||||
        name: ${{ runner.os }} Python ${{ matrix.python-version }}
 | 
					        name: ${{ runner.os }} Python ${{ matrix.python-version }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - name: Build wheel
 | 
					 | 
				
			||||||
      id: wheel
 | 
					 | 
				
			||||||
      if: "github.event_name != 'pull_request'"
 | 
					 | 
				
			||||||
      run: |
 | 
					 | 
				
			||||||
        mkdir fribidi
 | 
					 | 
				
			||||||
        copy winbuild\build\bin\fribidi* fribidi
 | 
					 | 
				
			||||||
        setlocal EnableDelayedExpansion
 | 
					 | 
				
			||||||
        for %%f in (winbuild\build\license\*) do (
 | 
					 | 
				
			||||||
          set x=%%~nf
 | 
					 | 
				
			||||||
          rem Skip FriBiDi license, it is not included in the wheel.
 | 
					 | 
				
			||||||
          set fribidi=!x:~0,7!
 | 
					 | 
				
			||||||
          if NOT !fribidi!==fribidi (
 | 
					 | 
				
			||||||
            rem Skip imagequant license, it is not included in the wheel.
 | 
					 | 
				
			||||||
            set libimagequant=!x:~0,13!
 | 
					 | 
				
			||||||
            if NOT !libimagequant!==libimagequant (
 | 
					 | 
				
			||||||
              echo. >> LICENSE
 | 
					 | 
				
			||||||
              echo ===== %%~nf ===== >> LICENSE
 | 
					 | 
				
			||||||
              echo. >> LICENSE
 | 
					 | 
				
			||||||
              type %%f >> LICENSE
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT%
 | 
					 | 
				
			||||||
        call winbuild\\build\\build_env.cmd
 | 
					 | 
				
			||||||
        %pythonLocation%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor -C imagequant=disable .
 | 
					 | 
				
			||||||
      shell: cmd
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    - name: Upload wheel
 | 
					 | 
				
			||||||
      uses: actions/upload-artifact@v3
 | 
					 | 
				
			||||||
      if: "github.event_name != 'pull_request'"
 | 
					 | 
				
			||||||
      with:
 | 
					 | 
				
			||||||
        name: ${{ steps.wheel.outputs.dist }}
 | 
					 | 
				
			||||||
        path: "*.whl"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    - name: Upload fribidi.dll
 | 
					 | 
				
			||||||
      if: "github.event_name != 'pull_request' && matrix.python-version == 3.11"
 | 
					 | 
				
			||||||
      uses: actions/upload-artifact@v3
 | 
					 | 
				
			||||||
      with:
 | 
					 | 
				
			||||||
        name: fribidi
 | 
					 | 
				
			||||||
        path: fribidi\*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  success:
 | 
					  success:
 | 
				
			||||||
    permissions:
 | 
					    permissions:
 | 
				
			||||||
      contents: none
 | 
					      contents: none
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										18
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -2,13 +2,23 @@ name: Test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
on:
 | 
					on:
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - "**"
 | 
				
			||||||
    paths-ignore:
 | 
					    paths-ignore:
 | 
				
			||||||
      - ".github/workflows/docs.yml"
 | 
					      - ".github/workflows/docs.yml"
 | 
				
			||||||
 | 
					      - ".github/workflows/wheels*"
 | 
				
			||||||
 | 
					      - ".gitmodules"
 | 
				
			||||||
 | 
					      - ".travis.yml"
 | 
				
			||||||
      - "docs/**"
 | 
					      - "docs/**"
 | 
				
			||||||
 | 
					      - "wheels/**"
 | 
				
			||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    paths-ignore:
 | 
					    paths-ignore:
 | 
				
			||||||
      - ".github/workflows/docs.yml"
 | 
					      - ".github/workflows/docs.yml"
 | 
				
			||||||
 | 
					      - ".github/workflows/wheels*"
 | 
				
			||||||
 | 
					      - ".gitmodules"
 | 
				
			||||||
 | 
					      - ".travis.yml"
 | 
				
			||||||
      - "docs/**"
 | 
					      - "docs/**"
 | 
				
			||||||
 | 
					      - "wheels/**"
 | 
				
			||||||
  workflow_dispatch:
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
permissions:
 | 
					permissions:
 | 
				
			||||||
| 
						 | 
					@ -31,7 +41,8 @@ jobs:
 | 
				
			||||||
        python-version: [
 | 
					        python-version: [
 | 
				
			||||||
          "pypy3.10",
 | 
					          "pypy3.10",
 | 
				
			||||||
          "pypy3.9",
 | 
					          "pypy3.9",
 | 
				
			||||||
          "3.12-dev",
 | 
					          "3.13",
 | 
				
			||||||
 | 
					          "3.12",
 | 
				
			||||||
          "3.11",
 | 
					          "3.11",
 | 
				
			||||||
          "3.10",
 | 
					          "3.10",
 | 
				
			||||||
          "3.9",
 | 
					          "3.9",
 | 
				
			||||||
| 
						 | 
					@ -51,9 +62,10 @@ jobs:
 | 
				
			||||||
    - uses: actions/checkout@v4
 | 
					    - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - name: Set up Python ${{ matrix.python-version }}
 | 
					    - name: Set up Python ${{ matrix.python-version }}
 | 
				
			||||||
      uses: actions/setup-python@v4
 | 
					      uses: actions/setup-python@v5
 | 
				
			||||||
      with:
 | 
					      with:
 | 
				
			||||||
        python-version: ${{ matrix.python-version }}
 | 
					        python-version: ${{ matrix.python-version }}
 | 
				
			||||||
 | 
					        allow-prereleases: true
 | 
				
			||||||
        cache: pip
 | 
					        cache: pip
 | 
				
			||||||
        cache-dependency-path: ".ci/*.sh"
 | 
					        cache-dependency-path: ".ci/*.sh"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -100,7 +112,7 @@ jobs:
 | 
				
			||||||
        mkdir -p Tests/errors
 | 
					        mkdir -p Tests/errors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - name: Upload errors
 | 
					    - name: Upload errors
 | 
				
			||||||
      uses: actions/upload-artifact@v3
 | 
					      uses: actions/upload-artifact@v4
 | 
				
			||||||
      if: failure()
 | 
					      if: failure()
 | 
				
			||||||
      with:
 | 
					      with:
 | 
				
			||||||
        name: errors
 | 
					        name: errors
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										151
									
								
								.github/workflows/wheels-dependencies.sh
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,151 @@
 | 
				
			||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					# Define custom utilities
 | 
				
			||||||
 | 
					# Test for macOS with [ -n "$IS_MACOS" ]
 | 
				
			||||||
 | 
					if [ -z "$IS_MACOS" ]; then
 | 
				
			||||||
 | 
					    export MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
 | 
				
			||||||
 | 
					    export MB_ML_VER=${AUDITWHEEL_POLICY:9}
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					export PLAT=$CIBW_ARCHS
 | 
				
			||||||
 | 
					source wheels/multibuild/common_utils.sh
 | 
				
			||||||
 | 
					source wheels/multibuild/library_builders.sh
 | 
				
			||||||
 | 
					if [ -z "$IS_MACOS" ]; then
 | 
				
			||||||
 | 
					    source wheels/multibuild/manylinux_utils.sh
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ARCHIVE_SDIR=pillow-depends-main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Package versions for fresh source builds
 | 
				
			||||||
 | 
					FREETYPE_VERSION=2.13.2
 | 
				
			||||||
 | 
					HARFBUZZ_VERSION=8.3.0
 | 
				
			||||||
 | 
					LIBPNG_VERSION=1.6.40
 | 
				
			||||||
 | 
					JPEGTURBO_VERSION=3.0.1
 | 
				
			||||||
 | 
					OPENJPEG_VERSION=2.5.0
 | 
				
			||||||
 | 
					XZ_VERSION=5.4.5
 | 
				
			||||||
 | 
					TIFF_VERSION=4.6.0
 | 
				
			||||||
 | 
					LCMS2_VERSION=2.16
 | 
				
			||||||
 | 
					if [[ -n "$IS_MACOS" ]]; then
 | 
				
			||||||
 | 
					    GIFLIB_VERSION=5.1.4
 | 
				
			||||||
 | 
					else
 | 
				
			||||||
 | 
					    GIFLIB_VERSION=5.2.1
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
 | 
				
			||||||
 | 
					    ZLIB_VERSION=1.3
 | 
				
			||||||
 | 
					else
 | 
				
			||||||
 | 
					    ZLIB_VERSION=1.2.8
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					LIBWEBP_VERSION=1.3.2
 | 
				
			||||||
 | 
					BZIP2_VERSION=1.0.8
 | 
				
			||||||
 | 
					LIBXCB_VERSION=1.16
 | 
				
			||||||
 | 
					BROTLI_VERSION=1.1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
 | 
				
			||||||
 | 
					    function build_openjpeg {
 | 
				
			||||||
 | 
					        local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-2.5.0.tar.gz)
 | 
				
			||||||
 | 
					        (cd $out_dir \
 | 
				
			||||||
 | 
					            && cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
 | 
				
			||||||
 | 
					            && make install)
 | 
				
			||||||
 | 
					        touch openjpeg-stamp
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function build_brotli {
 | 
				
			||||||
 | 
					    local cmake=$(get_modern_cmake)
 | 
				
			||||||
 | 
					    local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz)
 | 
				
			||||||
 | 
					    (cd $out_dir \
 | 
				
			||||||
 | 
					        && $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
 | 
				
			||||||
 | 
					        && make install)
 | 
				
			||||||
 | 
					    if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
 | 
				
			||||||
 | 
					        cp /usr/local/lib64/libbrotli* /usr/local/lib
 | 
				
			||||||
 | 
					        cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function build {
 | 
				
			||||||
 | 
					    if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
 | 
				
			||||||
 | 
					        export BUILD_PREFIX="/usr/local"
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					    build_xz
 | 
				
			||||||
 | 
					    if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
 | 
				
			||||||
 | 
					        yum remove -y zlib-devel
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					    build_new_zlib
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
 | 
				
			||||||
 | 
					    if [ -n "$IS_MACOS" ]; then
 | 
				
			||||||
 | 
					        if [[ "$CIBW_ARCHS" == "arm64" ]]; then
 | 
				
			||||||
 | 
					            build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
 | 
				
			||||||
 | 
					            build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
 | 
				
			||||||
 | 
					            build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
 | 
				
			||||||
 | 
					            if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then
 | 
				
			||||||
 | 
					                cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
 | 
				
			||||||
 | 
					            fi
 | 
				
			||||||
 | 
					        fi
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					        sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					    build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    build_libjpeg_turbo
 | 
				
			||||||
 | 
					    build_tiff
 | 
				
			||||||
 | 
					    build_libpng
 | 
				
			||||||
 | 
					    build_lcms2
 | 
				
			||||||
 | 
					    if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
 | 
				
			||||||
 | 
					        for dylib in libjpeg.dylib libtiff.dylib liblcms2.dylib; do
 | 
				
			||||||
 | 
					            cp $BUILD_PREFIX/lib/$dylib /opt/arm64-builds/lib
 | 
				
			||||||
 | 
					        done
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					    build_openjpeg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ORIGINAL_CFLAGS=$CFLAGS
 | 
				
			||||||
 | 
					    CFLAGS="$CFLAGS -O3 -DNDEBUG"
 | 
				
			||||||
 | 
					    if [[ -n "$IS_MACOS" ]]; then
 | 
				
			||||||
 | 
					        CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					    build_libwebp
 | 
				
			||||||
 | 
					    CFLAGS=$ORIGINAL_CFLAGS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    build_brotli
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if [ -n "$IS_MACOS" ]; then
 | 
				
			||||||
 | 
					        # Custom freetype build
 | 
				
			||||||
 | 
					        build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					        build_freetype
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if [ -z "$IS_MACOS" ]; then
 | 
				
			||||||
 | 
					        export FREETYPE_LIBS=-lfreetype
 | 
				
			||||||
 | 
					        export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					    build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no
 | 
				
			||||||
 | 
					    if [ -z "$IS_MACOS" ]; then
 | 
				
			||||||
 | 
					        export FREETYPE_LIBS=""
 | 
				
			||||||
 | 
					        export FREETYPE_CFLAGS=""
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Any stuff that you need to do before you start building the wheels
 | 
				
			||||||
 | 
					# Runs in the root directory of this repository.
 | 
				
			||||||
 | 
					curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
 | 
				
			||||||
 | 
					untar pillow-depends-main.zip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [[ -n "$IS_MACOS" ]]; then
 | 
				
			||||||
 | 
					  # webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
 | 
				
			||||||
 | 
					  # libxdmcp causes an issue on macOS < 11
 | 
				
			||||||
 | 
					  # if php is installed, brew tries to reinstall these after installing openblas
 | 
				
			||||||
 | 
					  # remove cairo to fix building harfbuzz on arm64
 | 
				
			||||||
 | 
					  # remove lcms2 and libpng to fix building openjpeg on arm64
 | 
				
			||||||
 | 
					  # remove zstd to avoid inclusion on x86_64
 | 
				
			||||||
 | 
					  # curl from brew requires zstd, use system curl
 | 
				
			||||||
 | 
					  brew remove --ignore-dependencies webp libpng libtiff libxcb libxdmcp curl php cairo lcms2 ghostscript zstd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  brew install pkg-config
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					wrap_wheel_builder build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Append licenses
 | 
				
			||||||
 | 
					for filename in wheels/dependency_licenses/*; do
 | 
				
			||||||
 | 
					  echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE
 | 
				
			||||||
 | 
					  cat $filename >> LICENSE
 | 
				
			||||||
 | 
					done
 | 
				
			||||||
							
								
								
									
										22
									
								
								.github/workflows/wheels-test.ps1
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					param ([string]$venv, [string]$pillow="C:\pillow")
 | 
				
			||||||
 | 
					$ErrorActionPreference  = 'Stop'
 | 
				
			||||||
 | 
					$ProgressPreference = 'SilentlyContinue'
 | 
				
			||||||
 | 
					Set-PSDebug -Trace 1
 | 
				
			||||||
 | 
					if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
 | 
				
			||||||
 | 
					    # unlike CPython, PyPy requires Visual C++ Redistributable to be installed
 | 
				
			||||||
 | 
					    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
 | 
				
			||||||
 | 
					    Invoke-WebRequest -Uri 'https://aka.ms/vs/15/release/vc_redist.x64.exe' -OutFile 'vc_redist.x64.exe'
 | 
				
			||||||
 | 
					    C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					$env:path += ";$pillow\winbuild\build\bin\"
 | 
				
			||||||
 | 
					& "$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
 | 
				
			||||||
 | 
					cd $pillow
 | 
				
			||||||
 | 
					& python -VV
 | 
				
			||||||
 | 
					if (!$?) { exit $LASTEXITCODE }
 | 
				
			||||||
 | 
					& python selftest.py
 | 
				
			||||||
 | 
					if (!$?) { exit $LASTEXITCODE }
 | 
				
			||||||
 | 
					& python -m pytest -vx Tests\check_wheel.py
 | 
				
			||||||
 | 
					if (!$?) { exit $LASTEXITCODE }
 | 
				
			||||||
 | 
					& python -m pytest -vx Tests
 | 
				
			||||||
 | 
					if (!$?) { exit $LASTEXITCODE }
 | 
				
			||||||
							
								
								
									
										25
									
								
								.github/workflows/wheels-test.sh
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [[ "$OSTYPE" == "darwin"* ]]; then
 | 
				
			||||||
 | 
					    brew install fribidi
 | 
				
			||||||
 | 
					    export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
 | 
				
			||||||
 | 
					elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
 | 
				
			||||||
 | 
					    apk add curl fribidi
 | 
				
			||||||
 | 
					else
 | 
				
			||||||
 | 
					    yum install -y fribidi
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
 | 
				
			||||||
 | 
					    python3 -m pip install numpy
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ ! -d "test-images-main" ]; then
 | 
				
			||||||
 | 
					    curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
 | 
				
			||||||
 | 
					    unzip pillow-test-images.zip
 | 
				
			||||||
 | 
					    mv test-images-main/* Tests/images
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Runs tests
 | 
				
			||||||
 | 
					python3 selftest.py
 | 
				
			||||||
 | 
					python3 -m pytest Tests/check_wheel.py
 | 
				
			||||||
 | 
					python3 -m pytest
 | 
				
			||||||
							
								
								
									
										206
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,206 @@
 | 
				
			||||||
 | 
					name: Wheels
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    paths:
 | 
				
			||||||
 | 
					      - ".ci/requirements-cibw.txt"
 | 
				
			||||||
 | 
					      - ".github/workflows/wheel*"
 | 
				
			||||||
 | 
					      - "wheels/*"
 | 
				
			||||||
 | 
					      - "winbuild/build_prepare.py"
 | 
				
			||||||
 | 
					      - "winbuild/fribidi.cmake"
 | 
				
			||||||
 | 
					    tags:
 | 
				
			||||||
 | 
					      - "*"
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					    paths:
 | 
				
			||||||
 | 
					      - ".ci/requirements-cibw.txt"
 | 
				
			||||||
 | 
					      - ".github/workflows/wheel*"
 | 
				
			||||||
 | 
					      - "wheels/*"
 | 
				
			||||||
 | 
					      - "winbuild/build_prepare.py"
 | 
				
			||||||
 | 
					      - "winbuild/fribidi.cmake"
 | 
				
			||||||
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					permissions:
 | 
				
			||||||
 | 
					  contents: read
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					concurrency:
 | 
				
			||||||
 | 
					  group: ${{ github.workflow }}-${{ github.ref }}
 | 
				
			||||||
 | 
					  cancel-in-progress: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  FORCE_COLOR: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  build:
 | 
				
			||||||
 | 
					    name: ${{ matrix.name }}
 | 
				
			||||||
 | 
					    runs-on: ${{ matrix.os }}
 | 
				
			||||||
 | 
					    strategy:
 | 
				
			||||||
 | 
					      fail-fast: false
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        include:
 | 
				
			||||||
 | 
					          - name: "macOS x86_64"
 | 
				
			||||||
 | 
					            os: macos-latest
 | 
				
			||||||
 | 
					            archs: x86_64
 | 
				
			||||||
 | 
					            macosx_deployment_target: "10.10"
 | 
				
			||||||
 | 
					          - name: "macOS arm64"
 | 
				
			||||||
 | 
					            os: macos-latest
 | 
				
			||||||
 | 
					            archs: arm64
 | 
				
			||||||
 | 
					            macosx_deployment_target: "11.0"
 | 
				
			||||||
 | 
					          - name: "manylinux2014 and musllinux x86_64"
 | 
				
			||||||
 | 
					            os: ubuntu-latest
 | 
				
			||||||
 | 
					            archs: x86_64
 | 
				
			||||||
 | 
					          - name: "manylinux_2_28 x86_64"
 | 
				
			||||||
 | 
					            os: ubuntu-latest
 | 
				
			||||||
 | 
					            archs: x86_64
 | 
				
			||||||
 | 
					            build: "*manylinux*"
 | 
				
			||||||
 | 
					            manylinux: "manylinux_2_28"
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          submodules: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - uses: actions/setup-python@v5
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          python-version: "3.x"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Build wheels
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          python3 -m pip install -r .ci/requirements-cibw.txt
 | 
				
			||||||
 | 
					          python3 -m cibuildwheel --output-dir wheelhouse
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          CIBW_ARCHS: ${{ matrix.archs }}
 | 
				
			||||||
 | 
					          CIBW_BUILD: ${{ matrix.build }}
 | 
				
			||||||
 | 
					          CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
 | 
				
			||||||
 | 
					          CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
 | 
				
			||||||
 | 
					          CIBW_SKIP: pp38-*
 | 
				
			||||||
 | 
					          CIBW_TEST_SKIP: "*-macosx_arm64"
 | 
				
			||||||
 | 
					          MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          name: dist
 | 
				
			||||||
 | 
					          path: ./wheelhouse/*.whl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  windows:
 | 
				
			||||||
 | 
					    name: Windows ${{ matrix.arch }}
 | 
				
			||||||
 | 
					    runs-on: windows-latest
 | 
				
			||||||
 | 
					    strategy:
 | 
				
			||||||
 | 
					      fail-fast: false
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        include:
 | 
				
			||||||
 | 
					          - arch: x86
 | 
				
			||||||
 | 
					            cibw_arch: x86
 | 
				
			||||||
 | 
					          - arch: x64
 | 
				
			||||||
 | 
					            cibw_arch: AMD64
 | 
				
			||||||
 | 
					          - arch: ARM64
 | 
				
			||||||
 | 
					            cibw_arch: ARM64
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Checkout extra test images
 | 
				
			||||||
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          repository: python-pillow/test-images
 | 
				
			||||||
 | 
					          path: Tests\test-images
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - uses: actions/setup-python@v5
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          python-version: "3.x"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Prepare for build
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          choco install nasm --no-progress
 | 
				
			||||||
 | 
					          echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          # Install extra test images
 | 
				
			||||||
 | 
					          xcopy /S /Y Tests\test-images\* Tests\images
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          & python.exe -m pip install -r .ci/requirements-cibw.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          # Cannot cross-compile FriBiDi (only used for tests)
 | 
				
			||||||
 | 
					          $FLAGS = ("--no-imagequant", "--architecture=${{ matrix.arch }}")
 | 
				
			||||||
 | 
					          if ('${{ matrix.arch }}' -eq 'ARM64') { $FLAGS += "--no-fribidi" }
 | 
				
			||||||
 | 
					          & python.exe winbuild\build_prepare.py -v @FLAGS
 | 
				
			||||||
 | 
					        shell: pwsh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Build wheels
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          setlocal EnableDelayedExpansion
 | 
				
			||||||
 | 
					          for %%f in (winbuild\build\license\*) do (
 | 
				
			||||||
 | 
					            set x=%%~nf
 | 
				
			||||||
 | 
					            rem Skip FriBiDi license, it is not included in the wheel.
 | 
				
			||||||
 | 
					            set fribidi=!x:~0,7!
 | 
				
			||||||
 | 
					            if NOT !fribidi!==fribidi (
 | 
				
			||||||
 | 
					              rem Skip imagequant license, it is not included in the wheel.
 | 
				
			||||||
 | 
					              set libimagequant=!x:~0,13!
 | 
				
			||||||
 | 
					              if NOT !libimagequant!==libimagequant (
 | 
				
			||||||
 | 
					                echo. >> LICENSE
 | 
				
			||||||
 | 
					                echo ===== %%~nf ===== >> LICENSE
 | 
				
			||||||
 | 
					                echo. >> LICENSE
 | 
				
			||||||
 | 
					                type %%f >> LICENSE
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					          call winbuild\\build\\build_env.cmd
 | 
				
			||||||
 | 
					          %pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          CIBW_ARCHS: ${{ matrix.cibw_arch }}
 | 
				
			||||||
 | 
					          CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
 | 
				
			||||||
 | 
					          CIBW_CACHE_PATH: "C:\\cibw"
 | 
				
			||||||
 | 
					          CIBW_TEST_SKIP: "*-win_arm64"
 | 
				
			||||||
 | 
					          CIBW_TEST_COMMAND: 'docker run --rm
 | 
				
			||||||
 | 
					            -v {project}:C:\pillow
 | 
				
			||||||
 | 
					            -v C:\cibw:C:\cibw
 | 
				
			||||||
 | 
					            -v %CD%\..\venv-test:%CD%\..\venv-test
 | 
				
			||||||
 | 
					            -e CI -e GITHUB_ACTIONS
 | 
				
			||||||
 | 
					            mcr.microsoft.com/windows/servercore:ltsc2022
 | 
				
			||||||
 | 
					            powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test'
 | 
				
			||||||
 | 
					        shell: cmd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Upload wheels
 | 
				
			||||||
 | 
					        uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          name: dist
 | 
				
			||||||
 | 
					          path: ./wheelhouse/*.whl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Prepare to upload FriBiDi
 | 
				
			||||||
 | 
					        if: "matrix.arch != 'ARM64'"
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          mkdir fribidi\${{ matrix.arch }}
 | 
				
			||||||
 | 
					          copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }}
 | 
				
			||||||
 | 
					        shell: cmd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Upload fribidi.dll
 | 
				
			||||||
 | 
					        if: "matrix.arch != 'ARM64'"
 | 
				
			||||||
 | 
					        uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          name: fribidi
 | 
				
			||||||
 | 
					          path: fribidi\*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  sdist:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					    - uses: actions/checkout@v4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - name: Set up Python
 | 
				
			||||||
 | 
					      uses: actions/setup-python@v5
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        python-version: "3.x"
 | 
				
			||||||
 | 
					        cache: pip
 | 
				
			||||||
 | 
					        cache-dependency-path: "Makefile"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - run: make sdist
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        name: dist
 | 
				
			||||||
 | 
					        path: dist/*.tar.gz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  success:
 | 
				
			||||||
 | 
					    permissions:
 | 
				
			||||||
 | 
					      contents: none
 | 
				
			||||||
 | 
					    needs: [build, windows, sdist]
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    name: Wheels Successful
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: Success
 | 
				
			||||||
 | 
					        run: echo Wheels Successful
 | 
				
			||||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					[submodule "multibuild"]
 | 
				
			||||||
 | 
					    path = wheels/multibuild
 | 
				
			||||||
 | 
					    url = https://github.com/multi-build/multibuild.git
 | 
				
			||||||
| 
						 | 
					@ -1,54 +1,35 @@
 | 
				
			||||||
repos:
 | 
					repos:
 | 
				
			||||||
  - repo: https://github.com/asottile/pyupgrade
 | 
					  - repo: https://github.com/astral-sh/ruff-pre-commit
 | 
				
			||||||
    rev: v3.13.0
 | 
					    rev: v0.1.7
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: pyupgrade
 | 
					      - id: ruff
 | 
				
			||||||
        args: [--py38-plus]
 | 
					        args: [--fix, --exit-non-zero-on-fix]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - repo: https://github.com/psf/black-pre-commit-mirror
 | 
					  - repo: https://github.com/psf/black-pre-commit-mirror
 | 
				
			||||||
    rev: 23.9.1
 | 
					    rev: 23.12.0
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: black
 | 
					      - id: black
 | 
				
			||||||
        args: [--target-version=py38]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  - repo: https://github.com/PyCQA/isort
 | 
					 | 
				
			||||||
    rev: 5.12.0
 | 
					 | 
				
			||||||
    hooks:
 | 
					 | 
				
			||||||
      - id: isort
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - repo: https://github.com/PyCQA/bandit
 | 
					  - repo: https://github.com/PyCQA/bandit
 | 
				
			||||||
    rev: 1.7.5
 | 
					    rev: 1.7.6
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
    - id: bandit
 | 
					    - id: bandit
 | 
				
			||||||
      args: [--severity-level=high]
 | 
					      args: [--severity-level=high]
 | 
				
			||||||
      files: ^src/
 | 
					      files: ^src/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - repo: https://github.com/asottile/yesqa
 | 
					 | 
				
			||||||
    rev: v1.5.0
 | 
					 | 
				
			||||||
    hooks:
 | 
					 | 
				
			||||||
      - id: yesqa
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  - repo: https://github.com/Lucas-C/pre-commit-hooks
 | 
					  - repo: https://github.com/Lucas-C/pre-commit-hooks
 | 
				
			||||||
    rev: v1.5.4
 | 
					    rev: v1.5.4
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: remove-tabs
 | 
					      - id: remove-tabs
 | 
				
			||||||
        exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
 | 
					        exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - repo: https://github.com/PyCQA/flake8
 | 
					 | 
				
			||||||
    rev: 6.1.0
 | 
					 | 
				
			||||||
    hooks:
 | 
					 | 
				
			||||||
      - id: flake8
 | 
					 | 
				
			||||||
        additional_dependencies:
 | 
					 | 
				
			||||||
          [flake8-2020, flake8-errmsg, flake8-implicit-str-concat, flake8-logging]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  - repo: https://github.com/pre-commit/pygrep-hooks
 | 
					  - repo: https://github.com/pre-commit/pygrep-hooks
 | 
				
			||||||
    rev: v1.10.0
 | 
					    rev: v1.10.0
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: python-check-blanket-noqa
 | 
					 | 
				
			||||||
      - id: rst-backticks
 | 
					      - id: rst-backticks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - repo: https://github.com/pre-commit/pre-commit-hooks
 | 
					  - repo: https://github.com/pre-commit/pre-commit-hooks
 | 
				
			||||||
    rev: v4.4.0
 | 
					    rev: v4.5.0
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: check-executables-have-shebangs
 | 
					      - id: check-executables-have-shebangs
 | 
				
			||||||
      - id: check-merge-conflict
 | 
					      - id: check-merge-conflict
 | 
				
			||||||
| 
						 | 
					@ -61,17 +42,17 @@ repos:
 | 
				
			||||||
        exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
 | 
					        exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - repo: https://github.com/sphinx-contrib/sphinx-lint
 | 
					  - repo: https://github.com/sphinx-contrib/sphinx-lint
 | 
				
			||||||
    rev: v0.6.8
 | 
					    rev: v0.9.1
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: sphinx-lint
 | 
					      - id: sphinx-lint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - repo: https://github.com/tox-dev/pyproject-fmt
 | 
					  - repo: https://github.com/tox-dev/pyproject-fmt
 | 
				
			||||||
    rev: 1.1.0
 | 
					    rev: 1.5.3
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: pyproject-fmt
 | 
					      - id: pyproject-fmt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - repo: https://github.com/abravalheri/validate-pyproject
 | 
					  - repo: https://github.com/abravalheri/validate-pyproject
 | 
				
			||||||
    rev: v0.14
 | 
					    rev: v0.15
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: validate-pyproject
 | 
					      - id: validate-pyproject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ formats: [pdf]
 | 
				
			||||||
build:
 | 
					build:
 | 
				
			||||||
  os: ubuntu-22.04
 | 
					  os: ubuntu-22.04
 | 
				
			||||||
  tools:
 | 
					  tools:
 | 
				
			||||||
    python: "3.11"
 | 
					    python: "3"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
python:
 | 
					python:
 | 
				
			||||||
  install:
 | 
					  install:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										52
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,52 @@
 | 
				
			||||||
 | 
					if: tag IS present OR type = api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  global:
 | 
				
			||||||
 | 
					    - CIBW_ARCHS=aarch64
 | 
				
			||||||
 | 
					    - CIBW_SKIP=pp38-*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					language: python
 | 
				
			||||||
 | 
					# Default Python version is usually 3.6
 | 
				
			||||||
 | 
					python: "3.12"
 | 
				
			||||||
 | 
					dist: jammy
 | 
				
			||||||
 | 
					services: docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  include:
 | 
				
			||||||
 | 
					    - name: "manylinux2014 aarch64"
 | 
				
			||||||
 | 
					      os: linux
 | 
				
			||||||
 | 
					      arch: arm64
 | 
				
			||||||
 | 
					      env:
 | 
				
			||||||
 | 
					        - CIBW_BUILD="*manylinux*"
 | 
				
			||||||
 | 
					        - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014
 | 
				
			||||||
 | 
					        - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014
 | 
				
			||||||
 | 
					    - name: "manylinux_2_28 aarch64"
 | 
				
			||||||
 | 
					      os: linux
 | 
				
			||||||
 | 
					      arch: arm64
 | 
				
			||||||
 | 
					      env:
 | 
				
			||||||
 | 
					        - CIBW_BUILD="*manylinux*"
 | 
				
			||||||
 | 
					        - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28
 | 
				
			||||||
 | 
					        - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28
 | 
				
			||||||
 | 
					    - name: "musllinux aarch64"
 | 
				
			||||||
 | 
					      os: linux
 | 
				
			||||||
 | 
					      arch: arm64
 | 
				
			||||||
 | 
					      env:
 | 
				
			||||||
 | 
					        - CIBW_BUILD="*musllinux*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					install:
 | 
				
			||||||
 | 
					    - python3 -m pip install -r .ci/requirements-cibw.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					script:
 | 
				
			||||||
 | 
					    - python3 -m cibuildwheel --output-dir wheelhouse
 | 
				
			||||||
 | 
					    - ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Upload wheels to GitHub Releases
 | 
				
			||||||
 | 
					deploy:
 | 
				
			||||||
 | 
					  provider: releases
 | 
				
			||||||
 | 
					  api_key: $GITHUB_RELEASE_TOKEN
 | 
				
			||||||
 | 
					  file_glob: true
 | 
				
			||||||
 | 
					  file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl"
 | 
				
			||||||
 | 
					  on:
 | 
				
			||||||
 | 
					    repo: python-pillow/Pillow
 | 
				
			||||||
 | 
					    tags: true
 | 
				
			||||||
 | 
					  skip_cleanup: true
 | 
				
			||||||
							
								
								
									
										125
									
								
								CHANGES.rst
									
									
									
									
									
								
							
							
						
						| 
						 | 
					@ -2,9 +2,120 @@
 | 
				
			||||||
Changelog (Pillow)
 | 
					Changelog (Pillow)
 | 
				
			||||||
==================
 | 
					==================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
10.1.0 (unreleased)
 | 
					10.2.0 (unreleased)
 | 
				
			||||||
-------------------
 | 
					-------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Attempt memory mapping when tile args is a string #7565
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Fill identical pixels with transparency in subsequent frames when saving GIF #7568
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Corrected duration when combining multiple GIF frames into single frame #7521
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Handle disposing GIF background from outside palette #7515
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Seek past the data when skipping a PSD layer #7483
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Import plugins relative to the module #7576
 | 
				
			||||||
 | 
					  [deliangyang, jaxx0n]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Translate encoder error codes to strings; deprecate ``ImageFile.raise_oserror()`` #7609
 | 
				
			||||||
 | 
					  [bgilbert, radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Support reading BC4U and DX10 BC1 images #6486
 | 
				
			||||||
 | 
					  [REDxEYE, radarhere, hugovk]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Optimize ImageStat.Stat.extrema #7593
 | 
				
			||||||
 | 
					  [florath, radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Handle pathlib.Path in FreeTypeFont #7578
 | 
				
			||||||
 | 
					  [radarhere, hugovk, nulano]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Added support for reading DX10 BC4 DDS images #7603
 | 
				
			||||||
 | 
					  [sambvfx, radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Optimized ImageStat.Stat.count #7599
 | 
				
			||||||
 | 
					  [florath]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Correct PDF palette size when saving #7555
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Fixed closing file pointer with olefile 0.47 #7594
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Raise ValueError when TrueType font size is not greater than zero #7584, #7587
 | 
				
			||||||
 | 
					  [akx, radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- If absent, do not try to close fp when closing image #7557
 | 
				
			||||||
 | 
					  [RaphaelVRossi, radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Allow configuring JPEG restart marker interval on save #7488
 | 
				
			||||||
 | 
					  [bgilbert, radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Decrement reference count for PyObject #7549
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Implement ``streamtype=1`` option for tables-only JPEG encoding #7491
 | 
				
			||||||
 | 
					  [bgilbert, radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- If save_all PNG only has one frame, do not create animated image #7522
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Fixed frombytes() for images with a zero dimension #7493
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					10.1.0 (2023-10-15)
 | 
				
			||||||
 | 
					-------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Added TrueType default font to allow for different sizes #7354
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Fixed invalid argument warning #7442
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Added ImageOps cover method #7412
 | 
				
			||||||
 | 
					  [radarhere, hugovk]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Catch struct.error from truncated EXIF when reading JPEG DPI #7458
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Consider default image when selecting mode for PNG save_all #7437
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Support BGR;15, BGR;16 and BGR;24 access, unpacking and putdata #7303
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Added CMYK to RGB unpacker #7310
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Improved flexibility of XMP parsing #7274
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Support reading 8-bit YCbCr TIFF images #7415
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Allow saving I;16B images as PNG #7302
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Corrected drawing I;16 points and writing I;16 text #7257
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Set blue channel to 128 for BC5S #7413
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Increase flexibility when reading IPTC fields #7319
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Set C palette to be empty by default #7289
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Added gs_binary to control Ghostscript use on all platforms #7392
 | 
				
			||||||
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Read bounding box information from the trailer of EPS files if specified #7382
 | 
					- Read bounding box information from the trailer of EPS files if specified #7382
 | 
				
			||||||
  [nopperl, radarhere]
 | 
					  [nopperl, radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2146,7 +2257,7 @@ Changelog (Pillow)
 | 
				
			||||||
- Cache EXIF information #3498
 | 
					- Cache EXIF information #3498
 | 
				
			||||||
  [Glandos]
 | 
					  [Glandos]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Added transparency for all PNG greyscale modes #3744
 | 
					- Added transparency for all PNG grayscale modes #3744
 | 
				
			||||||
  [radarhere]
 | 
					  [radarhere]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Fix deprecation warnings in Python 3.8 #3749
 | 
					- Fix deprecation warnings in Python 3.8 #3749
 | 
				
			||||||
| 
						 | 
					@ -4648,7 +4759,7 @@ Changelog (Pillow)
 | 
				
			||||||
- Fix Bicubic interpolation #970
 | 
					- Fix Bicubic interpolation #970
 | 
				
			||||||
  [homm]
 | 
					  [homm]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Support for 4-bit greyscale TIFF images #980
 | 
					- Support for 4-bit grayscale TIFF images #980
 | 
				
			||||||
  [hugovk]
 | 
					  [hugovk]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Updated manifest #957
 | 
					- Updated manifest #957
 | 
				
			||||||
| 
						 | 
					@ -6723,7 +6834,7 @@ The test suite includes 750 individual tests.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- You can now convert directly between all modes supported by
 | 
					- You can now convert directly between all modes supported by
 | 
				
			||||||
  PIL.  When converting colour images to "P", PIL defaults to
 | 
					  PIL.  When converting colour images to "P", PIL defaults to
 | 
				
			||||||
  a "web" palette and dithering.  When converting greyscale
 | 
					  a "web" palette and dithering.  When converting grayscale
 | 
				
			||||||
  images to "1", PIL uses a thresholding and dithering.
 | 
					  images to "1", PIL uses a thresholding and dithering.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Added a "dither" option to "convert".  By default, "convert"
 | 
					- Added a "dither" option to "convert".  By default, "convert"
 | 
				
			||||||
| 
						 | 
					@ -6801,13 +6912,13 @@ The test suite includes 530 individual tests.
 | 
				
			||||||
- Fixed "paste" to allow a mask also for mode "F" images.
 | 
					- Fixed "paste" to allow a mask also for mode "F" images.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- The BMP driver now saves mode "1" images.  When loading images, the mode
 | 
					- The BMP driver now saves mode "1" images.  When loading images, the mode
 | 
				
			||||||
  is set to "L" for 8-bit files with greyscale palettes, and to "P" for
 | 
					  is set to "L" for 8-bit files with grayscale palettes, and to "P" for
 | 
				
			||||||
  other 8-bit files.
 | 
					  other 8-bit files.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- The IM driver now reads and saves "1" images (file modes "0 1" or "L 1").
 | 
					- The IM driver now reads and saves "1" images (file modes "0 1" or "L 1").
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- The JPEG and GIF drivers now saves "1" images.  For JPEG, the image
 | 
					- The JPEG and GIF drivers now saves "1" images.  For JPEG, the image
 | 
				
			||||||
  is saved as 8-bit greyscale (it will load as mode "L").  For GIF, the
 | 
					  is saved as 8-bit grayscale (it will load as mode "L").  For GIF, the
 | 
				
			||||||
  image will be loaded as a "P" image.
 | 
					  image will be loaded as a "P" image.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Fixed a potential buffer overrun in the GIF encoder.
 | 
					- Fixed a potential buffer overrun in the GIF encoder.
 | 
				
			||||||
| 
						 | 
					@ -7111,7 +7222,7 @@ The test suite includes 400 individual tests.
 | 
				
			||||||
  drawing capabilities can be used to render vector and metafile
 | 
					  drawing capabilities can be used to render vector and metafile
 | 
				
			||||||
  formats.
 | 
					  formats.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Added restricted drivers for images from Image Tools (greyscale
 | 
					- Added restricted drivers for images from Image Tools (grayscale
 | 
				
			||||||
  only) and LabEye/IFUNC (common interchange modes only).
 | 
					  only) and LabEye/IFUNC (common interchange modes only).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Some minor improvements to the sample scripts provided in the
 | 
					- Some minor improvements to the sample scripts provided in the
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,8 +5,10 @@ include *.md
 | 
				
			||||||
include *.py
 | 
					include *.py
 | 
				
			||||||
include *.rst
 | 
					include *.rst
 | 
				
			||||||
include *.sh
 | 
					include *.sh
 | 
				
			||||||
 | 
					include *.toml
 | 
				
			||||||
include *.txt
 | 
					include *.txt
 | 
				
			||||||
include *.yaml
 | 
					include *.yaml
 | 
				
			||||||
 | 
					include .flake8
 | 
				
			||||||
include LICENSE
 | 
					include LICENSE
 | 
				
			||||||
include Makefile
 | 
					include Makefile
 | 
				
			||||||
include tox.ini
 | 
					include tox.ini
 | 
				
			||||||
| 
						 | 
					@ -29,3 +31,4 @@ global-exclude .git*
 | 
				
			||||||
global-exclude *.pyc
 | 
					global-exclude *.pyc
 | 
				
			||||||
global-exclude *.so
 | 
					global-exclude *.so
 | 
				
			||||||
prune .ci
 | 
					prune .ci
 | 
				
			||||||
 | 
					prune wheels
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										8
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						| 
						 | 
					@ -49,7 +49,7 @@ help:
 | 
				
			||||||
	@echo "  install            make and install"
 | 
						@echo "  install            make and install"
 | 
				
			||||||
	@echo "  install-coverage   make and install with C coverage"
 | 
						@echo "  install-coverage   make and install with C coverage"
 | 
				
			||||||
	@echo "  lint               run the lint checks"
 | 
						@echo "  lint               run the lint checks"
 | 
				
			||||||
	@echo "  lint-fix           run Black and isort to (mostly) fix lint issues"
 | 
						@echo "  lint-fix           run Ruff to (mostly) fix lint issues"
 | 
				
			||||||
	@echo "  release-test       run code and package tests before release"
 | 
						@echo "  release-test       run code and package tests before release"
 | 
				
			||||||
	@echo "  test               run tests on installed Pillow"
 | 
						@echo "  test               run tests on installed Pillow"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -118,6 +118,6 @@ lint:
 | 
				
			||||||
.PHONY: lint-fix
 | 
					.PHONY: lint-fix
 | 
				
			||||||
lint-fix:
 | 
					lint-fix:
 | 
				
			||||||
	python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
 | 
						python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
 | 
				
			||||||
	python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort
 | 
						python3 -m black .
 | 
				
			||||||
	python3 -m black --target-version py38 .
 | 
						python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
 | 
				
			||||||
	python3 -m isort .
 | 
						python3 -m ruff --fix .
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| 
						 | 
					@ -45,12 +45,12 @@ As of 2019, Pillow development is
 | 
				
			||||||
            <a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
 | 
					            <a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
 | 
				
			||||||
                alt="AppVeyor CI build status (Windows)"
 | 
					                alt="AppVeyor CI build status (Windows)"
 | 
				
			||||||
                src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a>
 | 
					                src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a>
 | 
				
			||||||
            <a href="https://github.com/python-pillow/pillow-wheels/actions"><img
 | 
					            <a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
 | 
				
			||||||
                alt="GitHub Actions wheels build status (Wheels)"
 | 
					                alt="GitHub Actions build status (Wheels)"
 | 
				
			||||||
                src="https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg"></a>
 | 
					                src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
 | 
				
			||||||
            <a href="https://app.travis-ci.com/github/python-pillow/pillow-wheels"><img
 | 
					            <a href="https://app.travis-ci.com/github/python-pillow/Pillow"><img
 | 
				
			||||||
                alt="Travis CI wheels build status (aarch64)"
 | 
					                alt="Travis CI wheels build status (aarch64)"
 | 
				
			||||||
                src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels"></a>
 | 
					                src="https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels"></a>
 | 
				
			||||||
            <a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
 | 
					            <a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
 | 
				
			||||||
                alt="Code coverage"
 | 
					                alt="Code coverage"
 | 
				
			||||||
                src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
 | 
					                src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
 | 
				
			||||||
| 
						 | 
					@ -74,9 +74,9 @@ As of 2019, Pillow development is
 | 
				
			||||||
            <a href="https://pypi.org/project/Pillow/"><img
 | 
					            <a href="https://pypi.org/project/Pillow/"><img
 | 
				
			||||||
                alt="Number of PyPI downloads"
 | 
					                alt="Number of PyPI downloads"
 | 
				
			||||||
                src="https://img.shields.io/pypi/dm/pillow.svg"></a>
 | 
					                src="https://img.shields.io/pypi/dm/pillow.svg"></a>
 | 
				
			||||||
            <a href="https://bestpractices.coreinfrastructure.org/projects/6331"><img
 | 
					            <a href="https://www.bestpractices.dev/projects/6331"><img
 | 
				
			||||||
                alt="OpenSSF Best Practices"
 | 
					                alt="OpenSSF Best Practices"
 | 
				
			||||||
                src="https://bestpractices.coreinfrastructure.org/projects/6331/badge"></a>
 | 
					                src="https://www.bestpractices.dev/projects/6331/badge"></a>
 | 
				
			||||||
        </td>
 | 
					        </td>
 | 
				
			||||||
    </tr>
 | 
					    </tr>
 | 
				
			||||||
    <tr>
 | 
					    <tr>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										38
									
								
								RELEASING.md
									
									
									
									
									
								
							
							
						
						| 
						 | 
					@ -10,7 +10,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) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
 | 
				
			||||||
* [ ] Check that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI and GitHub Actions.
 | 
					* [ ] Check that all of the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) 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`
 | 
				
			||||||
* [ ] Update `CHANGES.rst`.
 | 
					* [ ] Update `CHANGES.rst`.
 | 
				
			||||||
* [ ] 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.
 | 
				
			||||||
| 
						 | 
					@ -20,12 +20,8 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
 | 
				
			||||||
  git tag 5.2.0
 | 
					  git tag 5.2.0
 | 
				
			||||||
  git push --tags
 | 
					  git push --tags
 | 
				
			||||||
  ```
 | 
					  ```
 | 
				
			||||||
* [ ] Create and check source distribution:
 | 
					* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
 | 
				
			||||||
  ```bash
 | 
					* [ ] Check and upload all source and binary distributions e.g.:
 | 
				
			||||||
  make sdist
 | 
					 | 
				
			||||||
  ```
 | 
					 | 
				
			||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
 | 
					 | 
				
			||||||
* [ ] Check and upload all binaries and source distributions e.g.:
 | 
					 | 
				
			||||||
  ```bash
 | 
					  ```bash
 | 
				
			||||||
  python3 -m twine check --strict dist/*
 | 
					  python3 -m twine check --strict dist/*
 | 
				
			||||||
  python3 -m twine upload dist/Pillow-5.2.0*
 | 
					  python3 -m twine upload dist/Pillow-5.2.0*
 | 
				
			||||||
| 
						 | 
					@ -59,8 +55,8 @@ Released as needed for security, installation or critical bug fixes.
 | 
				
			||||||
  ```bash
 | 
					  ```bash
 | 
				
			||||||
  make sdist
 | 
					  make sdist
 | 
				
			||||||
  ```
 | 
					  ```
 | 
				
			||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
 | 
					* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
 | 
				
			||||||
* [ ] Check and upload all binaries and source distributions e.g.:
 | 
					* [ ] Check and upload all source and binary distributions e.g.:
 | 
				
			||||||
  ```bash
 | 
					  ```bash
 | 
				
			||||||
  python3 -m twine check --strict dist/*
 | 
					  python3 -m twine check --strict dist/*
 | 
				
			||||||
  python3 -m twine upload dist/Pillow-5.2.1*
 | 
					  python3 -m twine upload dist/Pillow-5.2.1*
 | 
				
			||||||
| 
						 | 
					@ -90,34 +86,22 @@ Released as needed privately to individual vendors for critical security-related
 | 
				
			||||||
  ```bash
 | 
					  ```bash
 | 
				
			||||||
  make sdist
 | 
					  make sdist
 | 
				
			||||||
  ```
 | 
					  ```
 | 
				
			||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
 | 
					* [ ] Create [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
 | 
				
			||||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
 | 
					* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
 | 
				
			||||||
  ```bash
 | 
					  ```bash
 | 
				
			||||||
  git push origin 2.5.x
 | 
					  git push origin 2.5.x
 | 
				
			||||||
  ```
 | 
					  ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Binary Distributions
 | 
					## Source and Binary Distributions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### macOS and Linux
 | 
					* [ ] Download sdist and wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
 | 
				
			||||||
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):
 | 
					 | 
				
			||||||
  ```bash
 | 
					 | 
				
			||||||
  git clone https://github.com/python-pillow/pillow-wheels
 | 
					 | 
				
			||||||
  cd pillow-wheels
 | 
					 | 
				
			||||||
  ./update-pillow-tag.sh [[release tag]]
 | 
					 | 
				
			||||||
  ```
 | 
					 | 
				
			||||||
* [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases)
 | 
					 | 
				
			||||||
  and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli) from the main repo:
 | 
					 | 
				
			||||||
  ```bash
 | 
					 | 
				
			||||||
  gh release download --dir dist --pattern "*.whl" --repo python-pillow/pillow-wheels
 | 
					 | 
				
			||||||
  ```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Windows
 | 
					 | 
				
			||||||
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
 | 
					 | 
				
			||||||
  and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
 | 
					  and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
 | 
				
			||||||
  ```bash
 | 
					  ```bash
 | 
				
			||||||
  gh run download --dir dist
 | 
					  gh run download --dir dist
 | 
				
			||||||
  # select dist-x.y.z
 | 
					  # select dist
 | 
				
			||||||
  ```
 | 
					  ```
 | 
				
			||||||
 | 
					* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
 | 
				
			||||||
 | 
					  and copy into `dist`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Publicize Release
 | 
					## Publicize Release
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,7 +45,7 @@ def test_direct():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assert caccess[(0, 0)] == access[(0, 0)]
 | 
					    assert caccess[(0, 0)] == access[(0, 0)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    print("Size: %sx%s" % im.size)
 | 
					    print(f"Size: {im.width}x{im.height}")
 | 
				
			||||||
    timer(iterate_get, "PyAccess - get", im.size, access)
 | 
					    timer(iterate_get, "PyAccess - get", im.size, access)
 | 
				
			||||||
    timer(iterate_set, "PyAccess - set", im.size, access)
 | 
					    timer(iterate_set, "PyAccess - set", im.size, access)
 | 
				
			||||||
    timer(iterate_get, "C-api - get", im.size, caccess)
 | 
					    timer(iterate_get, "C-api - get", im.size, caccess)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										41
									
								
								Tests/check_wheel.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,41 @@
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from PIL import features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_wheel_modules():
 | 
				
			||||||
 | 
					    expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # tkinter is not available in cibuildwheel installed CPython on Windows
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        import tkinter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert tkinter
 | 
				
			||||||
 | 
					    except ImportError:
 | 
				
			||||||
 | 
					        expected_modules.remove("tkinter")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert set(features.get_supported_modules()) == expected_modules
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_wheel_codecs():
 | 
				
			||||||
 | 
					    expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert set(features.get_supported_codecs()) == expected_codecs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_wheel_features():
 | 
				
			||||||
 | 
					    expected_features = {
 | 
				
			||||||
 | 
					        "webp_anim",
 | 
				
			||||||
 | 
					        "webp_mux",
 | 
				
			||||||
 | 
					        "transp_webp",
 | 
				
			||||||
 | 
					        "raqm",
 | 
				
			||||||
 | 
					        "fribidi",
 | 
				
			||||||
 | 
					        "harfbuzz",
 | 
				
			||||||
 | 
					        "libjpeg_turbo",
 | 
				
			||||||
 | 
					        "xcb",
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if sys.platform == "win32":
 | 
				
			||||||
 | 
					        expected_features.remove("xcb")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert set(features.get_supported_features()) == expected_features
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@ Helper functions.
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import shutil
 | 
					import shutil
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
import sysconfig
 | 
					import sysconfig
 | 
				
			||||||
import tempfile
 | 
					import tempfile
 | 
				
			||||||
| 
						 | 
					@ -95,7 +96,7 @@ def assert_image_equal(a, b, msg=None):
 | 
				
			||||||
            except Exception:
 | 
					            except Exception:
 | 
				
			||||||
                pass
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert False, msg or "got different content"
 | 
					        pytest.fail(msg or "got different content")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def assert_image_equal_tofile(a, filename, msg=None, mode=None):
 | 
					def assert_image_equal_tofile(a, filename, msg=None, mode=None):
 | 
				
			||||||
| 
						 | 
					@ -258,11 +259,21 @@ def hopper(mode=None, cache={}):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def djpeg_available():
 | 
					def djpeg_available():
 | 
				
			||||||
    return bool(shutil.which("djpeg"))
 | 
					    if shutil.which("djpeg"):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            subprocess.check_call(["djpeg", "-version"])
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        except subprocess.CalledProcessError:  # pragma: no cover
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def cjpeg_available():
 | 
					def cjpeg_available():
 | 
				
			||||||
    return bool(shutil.which("cjpeg"))
 | 
					    if shutil.which("cjpeg"):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            subprocess.check_call(["cjpeg", "-version"])
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        except subprocess.CalledProcessError:  # pragma: no cover
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def netpbm_available():
 | 
					def netpbm_available():
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 331 B After Width: | Height: | Size: 331 B  | 
| 
		 Before Width: | Height: | Size: 668 B After Width: | Height: | Size: 668 B  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/background_outside_palette.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 82 B  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bc1.dds
									
									
									
									
									
										Executable file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bc1_typeless.dds
									
									
									
									
									
										Executable file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bc4_typeless.dds
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bc4_unorm.dds
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bc4_unorm.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 982 B  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/bc4u.dds
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/default_font_freetype.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/five_channels.psd
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/imagedraw_default_font_size.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.0 KiB  | 
| 
		 Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 180 B  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/truncated_exif_dpi.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 7.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/unsupported_bitcount_luminance.dds
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/unsupported_bitcount_rgb.dds
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/xmp_no_prefix.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 788 B  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/xmp_padded.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 778 B  | 
| 
						 | 
					@ -15,7 +15,7 @@
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
################################################################################
 | 
					################################################################################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
python3 setup.py build --build-base=/tmp/build install
 | 
					python3 -m pip install .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Build fuzzers in $OUT.
 | 
					# Build fuzzers in $OUT.
 | 
				
			||||||
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
 | 
					for fuzzer in $(find $SRC -name 'fuzz_*.py'); do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -233,13 +233,13 @@ def test_apng_mode():
 | 
				
			||||||
        assert im.getpixel((0, 0)) == (0, 0, 128, 191)
 | 
					        assert im.getpixel((0, 0)) == (0, 0, 128, 191)
 | 
				
			||||||
        assert im.getpixel((64, 32)) == (0, 0, 128, 191)
 | 
					        assert im.getpixel((64, 32)) == (0, 0, 128, 191)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with Image.open("Tests/images/apng/mode_greyscale.png") as im:
 | 
					    with Image.open("Tests/images/apng/mode_grayscale.png") as im:
 | 
				
			||||||
        assert im.mode == "L"
 | 
					        assert im.mode == "L"
 | 
				
			||||||
        im.seek(im.n_frames - 1)
 | 
					        im.seek(im.n_frames - 1)
 | 
				
			||||||
        assert im.getpixel((0, 0)) == 128
 | 
					        assert im.getpixel((0, 0)) == 128
 | 
				
			||||||
        assert im.getpixel((64, 32)) == 255
 | 
					        assert im.getpixel((64, 32)) == 255
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with Image.open("Tests/images/apng/mode_greyscale_alpha.png") as im:
 | 
					    with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
 | 
				
			||||||
        assert im.mode == "LA"
 | 
					        assert im.mode == "LA"
 | 
				
			||||||
        im.seek(im.n_frames - 1)
 | 
					        im.seek(im.n_frames - 1)
 | 
				
			||||||
        assert im.getpixel((0, 0)) == (128, 191)
 | 
					        assert im.getpixel((0, 0)) == (128, 191)
 | 
				
			||||||
| 
						 | 
					@ -352,15 +352,13 @@ def test_apng_save(tmp_path):
 | 
				
			||||||
        im.load()
 | 
					        im.load()
 | 
				
			||||||
        assert not im.is_animated
 | 
					        assert not im.is_animated
 | 
				
			||||||
        assert im.n_frames == 1
 | 
					        assert im.n_frames == 1
 | 
				
			||||||
        assert im.get_format_mimetype() == "image/apng"
 | 
					        assert im.get_format_mimetype() == "image/png"
 | 
				
			||||||
        assert im.info.get("default_image") is None
 | 
					        assert im.info.get("default_image") is None
 | 
				
			||||||
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
 | 
					        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
 | 
				
			||||||
        assert im.getpixel((64, 32)) == (0, 255, 0, 255)
 | 
					        assert im.getpixel((64, 32)) == (0, 255, 0, 255)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with Image.open("Tests/images/apng/single_frame_default.png") as im:
 | 
					    with Image.open("Tests/images/apng/single_frame_default.png") as im:
 | 
				
			||||||
        frames = []
 | 
					        frames = [frame_im.copy() for frame_im in ImageSequence.Iterator(im)]
 | 
				
			||||||
        for frame_im in ImageSequence.Iterator(im):
 | 
					 | 
				
			||||||
            frames.append(frame_im.copy())
 | 
					 | 
				
			||||||
        frames[0].save(
 | 
					        frames[0].save(
 | 
				
			||||||
            test_file, save_all=True, default_image=True, append_images=frames[1:]
 | 
					            test_file, save_all=True, default_image=True, append_images=frames[1:]
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
| 
						 | 
					@ -452,26 +450,29 @@ def test_apng_save_duration_loop(tmp_path):
 | 
				
			||||||
        test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
 | 
					        test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    with Image.open(test_file) as im:
 | 
					    with Image.open(test_file) as im:
 | 
				
			||||||
        im.load()
 | 
					 | 
				
			||||||
        assert im.n_frames == 1
 | 
					        assert im.n_frames == 1
 | 
				
			||||||
        assert im.info.get("duration") == 750
 | 
					        assert "duration" not in im.info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    different_frame = Image.new("RGBA", (128, 64))
 | 
				
			||||||
 | 
					    frame.save(
 | 
				
			||||||
 | 
					        test_file,
 | 
				
			||||||
 | 
					        save_all=True,
 | 
				
			||||||
 | 
					        append_images=[frame, different_frame],
 | 
				
			||||||
 | 
					        duration=[500, 100, 150],
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    with Image.open(test_file) as im:
 | 
				
			||||||
 | 
					        assert im.n_frames == 2
 | 
				
			||||||
 | 
					        assert im.info["duration"] == 600
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        im.seek(1)
 | 
				
			||||||
 | 
					        assert im.info["duration"] == 150
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # test info duration
 | 
					    # test info duration
 | 
				
			||||||
    frame.info["duration"] = 750
 | 
					    frame.info["duration"] = 300
 | 
				
			||||||
    frame.save(test_file, save_all=True)
 | 
					    frame.save(test_file, save_all=True, append_images=[frame, different_frame])
 | 
				
			||||||
    with Image.open(test_file) as im:
 | 
					    with Image.open(test_file) as im:
 | 
				
			||||||
        assert im.info.get("duration") == 750
 | 
					        assert im.n_frames == 2
 | 
				
			||||||
 | 
					        assert im.info["duration"] == 600
 | 
				
			||||||
 | 
					 | 
				
			||||||
def test_apng_save_duplicate_duration(tmp_path):
 | 
					 | 
				
			||||||
    test_file = str(tmp_path / "temp.png")
 | 
					 | 
				
			||||||
    frame = Image.new("RGB", (1, 1))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Test a single duration is correctly combined across duplicate frames
 | 
					 | 
				
			||||||
    frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500)
 | 
					 | 
				
			||||||
    with Image.open(test_file) as im:
 | 
					 | 
				
			||||||
        assert im.n_frames == 1
 | 
					 | 
				
			||||||
        assert im.info.get("duration") == 1500
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_apng_save_disposal(tmp_path):
 | 
					def test_apng_save_disposal(tmp_path):
 | 
				
			||||||
| 
						 | 
					@ -723,10 +724,17 @@ def test_seek_after_close():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
 | 
					@pytest.mark.parametrize("mode", ("RGBA", "RGB", "P"))
 | 
				
			||||||
def test_different_modes_in_later_frames(mode, tmp_path):
 | 
					@pytest.mark.parametrize("default_image", (True, False))
 | 
				
			||||||
 | 
					@pytest.mark.parametrize("duplicate", (True, False))
 | 
				
			||||||
 | 
					def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path):
 | 
				
			||||||
    test_file = str(tmp_path / "temp.png")
 | 
					    test_file = str(tmp_path / "temp.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    im = Image.new("L", (1, 1))
 | 
					    im = Image.new("L", (1, 1))
 | 
				
			||||||
    im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))])
 | 
					    im.save(
 | 
				
			||||||
 | 
					        test_file,
 | 
				
			||||||
 | 
					        save_all=True,
 | 
				
			||||||
 | 
					        default_image=default_image,
 | 
				
			||||||
 | 
					        append_images=[im.convert(mode) if duplicate else Image.new(mode, (1, 1), 1)],
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    with Image.open(test_file) as reloaded:
 | 
					    with Image.open(test_file) as reloaded:
 | 
				
			||||||
        assert reloaded.mode == mode
 | 
					        assert reloaded.mode == mode
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -159,7 +159,7 @@ def test_rle8():
 | 
				
			||||||
    with Image.open("Tests/images/hopper_rle8.bmp") as im:
 | 
					    with Image.open("Tests/images/hopper_rle8.bmp") as im:
 | 
				
			||||||
        assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
 | 
					        assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with Image.open("Tests/images/hopper_rle8_greyscale.bmp") as im:
 | 
					    with Image.open("Tests/images/hopper_rle8_grayscale.bmp") as im:
 | 
				
			||||||
        assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
 | 
					        assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # This test image has been manually hexedited
 | 
					    # This test image has been manually hexedited
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,9 +12,14 @@ TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
 | 
				
			||||||
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
 | 
					TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
 | 
				
			||||||
TEST_FILE_ATI1 = "Tests/images/ati1.dds"
 | 
					TEST_FILE_ATI1 = "Tests/images/ati1.dds"
 | 
				
			||||||
TEST_FILE_ATI2 = "Tests/images/ati2.dds"
 | 
					TEST_FILE_ATI2 = "Tests/images/ati2.dds"
 | 
				
			||||||
 | 
					TEST_FILE_DX10_BC4_TYPELESS = "Tests/images/bc4_typeless.dds"
 | 
				
			||||||
 | 
					TEST_FILE_DX10_BC4_UNORM = "Tests/images/bc4_unorm.dds"
 | 
				
			||||||
TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
 | 
					TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
 | 
				
			||||||
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
 | 
					TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
 | 
				
			||||||
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
 | 
					TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
 | 
				
			||||||
 | 
					TEST_FILE_DX10_BC1 = "Tests/images/bc1.dds"
 | 
				
			||||||
 | 
					TEST_FILE_DX10_BC1_TYPELESS = "Tests/images/bc1_typeless.dds"
 | 
				
			||||||
 | 
					TEST_FILE_BC4U = "Tests/images/bc4u.dds"
 | 
				
			||||||
TEST_FILE_BC5S = "Tests/images/bc5s.dds"
 | 
					TEST_FILE_BC5S = "Tests/images/bc5s.dds"
 | 
				
			||||||
TEST_FILE_BC5U = "Tests/images/bc5u.dds"
 | 
					TEST_FILE_BC5U = "Tests/images/bc5u.dds"
 | 
				
			||||||
TEST_FILE_BC6H = "Tests/images/bc6h.dds"
 | 
					TEST_FILE_BC6H = "Tests/images/bc6h.dds"
 | 
				
			||||||
| 
						 | 
					@ -29,11 +34,20 @@ TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
 | 
				
			||||||
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
 | 
					TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_sanity_dxt1():
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
    """Check DXT1 images can be opened"""
 | 
					    "image_path",
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        TEST_FILE_DXT1,
 | 
				
			||||||
 | 
					        # hexeditted to use DX10 FourCC
 | 
				
			||||||
 | 
					        TEST_FILE_DX10_BC1,
 | 
				
			||||||
 | 
					        TEST_FILE_DX10_BC1_TYPELESS,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_sanity_dxt1_bc1(image_path):
 | 
				
			||||||
 | 
					    """Check DXT1 and BC1 images can be opened"""
 | 
				
			||||||
    with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
 | 
					    with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target:
 | 
				
			||||||
        target = target.convert("RGBA")
 | 
					        target = target.convert("RGBA")
 | 
				
			||||||
    with Image.open(TEST_FILE_DXT1) as im:
 | 
					    with Image.open(image_path) as im:
 | 
				
			||||||
        im.load()
 | 
					        im.load()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert im.format == "DDS"
 | 
					        assert im.format == "DDS"
 | 
				
			||||||
| 
						 | 
					@ -69,10 +83,18 @@ def test_sanity_dxt5():
 | 
				
			||||||
    assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
 | 
					    assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_sanity_ati1():
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
    """Check ATI1 images can be opened"""
 | 
					    "image_path",
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        TEST_FILE_ATI1,
 | 
				
			||||||
 | 
					        # hexeditted to use BC4U FourCC
 | 
				
			||||||
 | 
					        TEST_FILE_BC4U,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_sanity_ati1_bc4u(image_path):
 | 
				
			||||||
 | 
					    """Check ATI1 and BC4U images can be opened"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with Image.open(TEST_FILE_ATI1) as im:
 | 
					    with Image.open(image_path) as im:
 | 
				
			||||||
        im.load()
 | 
					        im.load()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert im.format == "DDS"
 | 
					        assert im.format == "DDS"
 | 
				
			||||||
| 
						 | 
					@ -82,6 +104,27 @@ def test_sanity_ati1():
 | 
				
			||||||
        assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
 | 
					        assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
 | 
					    "image_path",
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        TEST_FILE_DX10_BC4_UNORM,
 | 
				
			||||||
 | 
					        # hexeditted to be typeless
 | 
				
			||||||
 | 
					        TEST_FILE_DX10_BC4_TYPELESS,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_dx10_bc4(image_path):
 | 
				
			||||||
 | 
					    """Check DX10 BC4 images can be opened"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with Image.open(image_path) as im:
 | 
				
			||||||
 | 
					        im.load()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert im.format == "DDS"
 | 
				
			||||||
 | 
					        assert im.mode == "L"
 | 
				
			||||||
 | 
					        assert im.size == (64, 64)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_image_equal_tofile(im, TEST_FILE_DX10_BC4_UNORM.replace(".dds", ".png"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize(
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
    "image_path",
 | 
					    "image_path",
 | 
				
			||||||
    (
 | 
					    (
 | 
				
			||||||
| 
						 | 
					@ -199,12 +242,6 @@ def test_dx10_r8g8b8a8_unorm_srgb():
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_unimplemented_dxgi_format():
 | 
					 | 
				
			||||||
    with pytest.raises(NotImplementedError):
 | 
					 | 
				
			||||||
        with Image.open("Tests/images/unimplemented_dxgi_format.dds"):
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@pytest.mark.parametrize(
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
    ("mode", "size", "test_file"),
 | 
					    ("mode", "size", "test_file"),
 | 
				
			||||||
    [
 | 
					    [
 | 
				
			||||||
| 
						 | 
					@ -303,9 +340,29 @@ def test_palette():
 | 
				
			||||||
        assert_image_equal_tofile(im, "Tests/images/transparent.gif")
 | 
					        assert_image_equal_tofile(im, "Tests/images/transparent.gif")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_unimplemented_pixel_format():
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
 | 
					    "test_file",
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        "Tests/images/unsupported_bitcount_rgb.dds",
 | 
				
			||||||
 | 
					        "Tests/images/unsupported_bitcount_luminance.dds",
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_unsupported_bitcount(test_file):
 | 
				
			||||||
 | 
					    with pytest.raises(OSError):
 | 
				
			||||||
 | 
					        with Image.open(test_file):
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
 | 
					    "test_file",
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        "Tests/images/unimplemented_dxgi_format.dds",
 | 
				
			||||||
 | 
					        "Tests/images/unimplemented_pfflags.dds",
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_not_implemented(test_file):
 | 
				
			||||||
    with pytest.raises(NotImplementedError):
 | 
					    with pytest.raises(NotImplementedError):
 | 
				
			||||||
        with Image.open("Tests/images/unimplemented_pixel_format.dds"):
 | 
					        with Image.open(test_file):
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@ from .helper import (
 | 
				
			||||||
    assert_image_similar,
 | 
					    assert_image_similar,
 | 
				
			||||||
    assert_image_similar_tofile,
 | 
					    assert_image_similar_tofile,
 | 
				
			||||||
    hopper,
 | 
					    hopper,
 | 
				
			||||||
 | 
					    is_win32,
 | 
				
			||||||
    mark_if_feature_version,
 | 
					    mark_if_feature_version,
 | 
				
			||||||
    skip_unless_feature,
 | 
					    skip_unless_feature,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -98,6 +99,20 @@ def test_load():
 | 
				
			||||||
        assert im.load()[0, 0] == (255, 255, 255)
 | 
					        assert im.load()[0, 0] == (255, 255, 255)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_binary():
 | 
				
			||||||
 | 
					    if HAS_GHOSTSCRIPT:
 | 
				
			||||||
 | 
					        assert EpsImagePlugin.gs_binary is not None
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        assert EpsImagePlugin.gs_binary is False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not is_win32():
 | 
				
			||||||
 | 
					        assert EpsImagePlugin.gs_windows_binary is None
 | 
				
			||||||
 | 
					    elif not HAS_GHOSTSCRIPT:
 | 
				
			||||||
 | 
					        assert EpsImagePlugin.gs_windows_binary is False
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        assert EpsImagePlugin.gs_windows_binary is not None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_invalid_file():
 | 
					def test_invalid_file():
 | 
				
			||||||
    invalid_file = "Tests/images/flower.jpg"
 | 
					    invalid_file = "Tests/images/flower.jpg"
 | 
				
			||||||
    with pytest.raises(SyntaxError):
 | 
					    with pytest.raises(SyntaxError):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -205,18 +205,39 @@ def test_optimize_full_l():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_optimize_if_palette_can_be_reduced_by_half():
 | 
					def test_optimize_if_palette_can_be_reduced_by_half():
 | 
				
			||||||
    with Image.open("Tests/images/test.colors.gif") as im:
 | 
					    im = Image.new("P", (8, 1))
 | 
				
			||||||
        # Reduce dimensions because original is too big for _get_optimize()
 | 
					    im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150))
 | 
				
			||||||
        im = im.resize((591, 443))
 | 
					    for i in range(8):
 | 
				
			||||||
    im_rgb = im.convert("RGB")
 | 
					        im.putpixel((i, 0), (i + 1, 0, 0))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for optimize, colors in ((False, 256), (True, 8)):
 | 
					    for optimize, colors in ((False, 256), (True, 8)):
 | 
				
			||||||
        out = BytesIO()
 | 
					        out = BytesIO()
 | 
				
			||||||
        im_rgb.save(out, "GIF", optimize=optimize)
 | 
					        im.save(out, "GIF", optimize=optimize)
 | 
				
			||||||
        with Image.open(out) as reloaded:
 | 
					        with Image.open(out) as reloaded:
 | 
				
			||||||
            assert len(reloaded.palette.palette) // 3 == colors
 | 
					            assert len(reloaded.palette.palette) // 3 == colors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_full_palette_second_frame(tmp_path):
 | 
				
			||||||
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
 | 
					    im = Image.new("P", (1, 256))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    full_palette_im = Image.new("P", (1, 256))
 | 
				
			||||||
 | 
					    for i in range(256):
 | 
				
			||||||
 | 
					        full_palette_im.putpixel((0, i), i)
 | 
				
			||||||
 | 
					    full_palette_im.palette = ImagePalette.ImagePalette(
 | 
				
			||||||
 | 
					        "RGB", bytearray(i // 3 for i in range(768))
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    full_palette_im.palette.dirty = 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    im.save(out, save_all=True, append_images=[full_palette_im])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with Image.open(out) as reloaded:
 | 
				
			||||||
 | 
					        reloaded.seek(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for i in range(256):
 | 
				
			||||||
 | 
					            reloaded.getpixel((0, i)) == i
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_roundtrip(tmp_path):
 | 
					def test_roundtrip(tmp_path):
 | 
				
			||||||
    out = str(tmp_path / "temp.gif")
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
    im = hopper()
 | 
					    im = hopper()
 | 
				
			||||||
| 
						 | 
					@ -285,14 +306,14 @@ def test_save_all_progress():
 | 
				
			||||||
    out = BytesIO()
 | 
					    out = BytesIO()
 | 
				
			||||||
    progress = []
 | 
					    progress = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with Image.open("Tests/images/hopper.gif") as im:
 | 
					 | 
				
			||||||
    with Image.open("Tests/images/chi.gif") as im2:
 | 
					    with Image.open("Tests/images/chi.gif") as im2:
 | 
				
			||||||
 | 
					        im = Image.new("RGB", im2.size)
 | 
				
			||||||
        im.save(out, "GIF", save_all=True, append_images=[im2], progress=callback)
 | 
					        im.save(out, "GIF", save_all=True, append_images=[im2], progress=callback)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expected = [
 | 
					    expected = [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            "image_index": 0,
 | 
					            "image_index": 0,
 | 
				
			||||||
            "image_filename": "Tests/images/hopper.gif",
 | 
					            "image_filename": None,
 | 
				
			||||||
            "completed_frames": 1,
 | 
					            "completed_frames": 1,
 | 
				
			||||||
            "total_frames": 32,
 | 
					            "total_frames": 32,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -634,7 +655,7 @@ def test_save_dispose(tmp_path):
 | 
				
			||||||
def test_dispose2_palette(tmp_path):
 | 
					def test_dispose2_palette(tmp_path):
 | 
				
			||||||
    out = str(tmp_path / "temp.gif")
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Four colors: white, grey, black, red
 | 
					    # Four colors: white, gray, black, red
 | 
				
			||||||
    circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
 | 
					    circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    im_list = []
 | 
					    im_list = []
 | 
				
			||||||
| 
						 | 
					@ -900,7 +921,14 @@ def test_identical_frames(tmp_path):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize(
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
    "duration", ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500)
 | 
					    "duration",
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        [1000, 1500, 2000],
 | 
				
			||||||
 | 
					        (1000, 1500, 2000),
 | 
				
			||||||
 | 
					        # One more duration than the number of frames
 | 
				
			||||||
 | 
					        [1000, 1500, 2000, 4000],
 | 
				
			||||||
 | 
					        1500,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
def test_identical_frames_to_single_frame(duration, tmp_path):
 | 
					def test_identical_frames_to_single_frame(duration, tmp_path):
 | 
				
			||||||
    out = str(tmp_path / "temp.gif")
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
| 
						 | 
					@ -916,7 +944,7 @@ def test_identical_frames_to_single_frame(duration, tmp_path):
 | 
				
			||||||
        assert reread.n_frames == 1
 | 
					        assert reread.n_frames == 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Assert that the new duration is the total of the identical frames
 | 
					        # Assert that the new duration is the total of the identical frames
 | 
				
			||||||
        assert reread.info["duration"] == 8500
 | 
					        assert reread.info["duration"] == 4500
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_loop_none(tmp_path):
 | 
					def test_loop_none(tmp_path):
 | 
				
			||||||
| 
						 | 
					@ -1186,6 +1214,12 @@ def test_rgba_transparency(tmp_path):
 | 
				
			||||||
        assert_image_equal(hopper("P").convert("RGB"), reloaded)
 | 
					        assert_image_equal(hopper("P").convert("RGB"), reloaded)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_background_outside_palettte(tmp_path):
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/background_outside_palette.gif") as im:
 | 
				
			||||||
 | 
					        im.seek(1)
 | 
				
			||||||
 | 
					        assert im.info["background"] == 255
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_bbox(tmp_path):
 | 
					def test_bbox(tmp_path):
 | 
				
			||||||
    out = str(tmp_path / "temp.gif")
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1224,18 +1258,17 @@ def test_palette_save_L(tmp_path):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_palette_save_P(tmp_path):
 | 
					def test_palette_save_P(tmp_path):
 | 
				
			||||||
    # Pass in a different palette, then construct what the image would look like.
 | 
					    im = Image.new("P", (1, 2))
 | 
				
			||||||
    # Forcing a non-straight grayscale palette.
 | 
					    im.putpixel((0, 1), 1)
 | 
				
			||||||
 | 
					 | 
				
			||||||
    im = hopper("P")
 | 
					 | 
				
			||||||
    palette = bytes(255 - i // 3 for i in range(768))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    out = str(tmp_path / "temp.gif")
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
    im.save(out, palette=palette)
 | 
					    im.save(out, palette=bytes((1, 2, 3, 4, 5, 6)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with Image.open(out) as reloaded:
 | 
					    with Image.open(out) as reloaded:
 | 
				
			||||||
        im.putpalette(palette)
 | 
					        reloaded_rgb = reloaded.convert("RGB")
 | 
				
			||||||
        assert_image_equal(reloaded, im)
 | 
					
 | 
				
			||||||
 | 
					        assert reloaded_rgb.getpixel((0, 0)) == (1, 2, 3)
 | 
				
			||||||
 | 
					        assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_palette_save_duplicate_entries(tmp_path):
 | 
					def test_palette_save_duplicate_entries(tmp_path):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,7 @@
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
from io import StringIO
 | 
					from io import BytesIO, StringIO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import Image, IptcImagePlugin
 | 
					from PIL import Image, IptcImagePlugin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,6 +32,36 @@ def test_getiptcinfo_jpg_found():
 | 
				
			||||||
    assert iptc[(2, 101)] == b"Hungary"
 | 
					    assert iptc[(2, 101)] == b"Hungary"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_getiptcinfo_fotostation():
 | 
				
			||||||
 | 
					    # Arrange
 | 
				
			||||||
 | 
					    with open(TEST_FILE, "rb") as fp:
 | 
				
			||||||
 | 
					        data = bytearray(fp.read())
 | 
				
			||||||
 | 
					    data[86] = 240
 | 
				
			||||||
 | 
					    f = BytesIO(data)
 | 
				
			||||||
 | 
					    with Image.open(f) as im:
 | 
				
			||||||
 | 
					        # Act
 | 
				
			||||||
 | 
					        iptc = IptcImagePlugin.getiptcinfo(im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Assert
 | 
				
			||||||
 | 
					    for tag in iptc.keys():
 | 
				
			||||||
 | 
					        if tag[0] == 240:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					    pytest.fail("FotoStation tag not found")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_getiptcinfo_zero_padding():
 | 
				
			||||||
 | 
					    # Arrange
 | 
				
			||||||
 | 
					    with Image.open(TEST_FILE) as im:
 | 
				
			||||||
 | 
					        im.info["photoshop"][0x0404] += b"\x00\x00\x00"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Act
 | 
				
			||||||
 | 
					        iptc = IptcImagePlugin.getiptcinfo(im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Assert
 | 
				
			||||||
 | 
					    assert isinstance(iptc, dict)
 | 
				
			||||||
 | 
					    assert len(iptc) == 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_getiptcinfo_tiff_none():
 | 
					def test_getiptcinfo_tiff_none():
 | 
				
			||||||
    # Arrange
 | 
					    # Arrange
 | 
				
			||||||
    with Image.open("Tests/images/hopper.tif") as im:
 | 
					    with Image.open("Tests/images/hopper.tif") as im:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -643,6 +643,23 @@ class TestFileJpeg:
 | 
				
			||||||
            assert max(im2.quantization[0]) <= 255
 | 
					            assert max(im2.quantization[0]) <= 255
 | 
				
			||||||
            assert max(im2.quantization[1]) <= 255
 | 
					            assert max(im2.quantization[1]) <= 255
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @pytest.mark.parametrize(
 | 
				
			||||||
 | 
					        "blocks, rows, markers",
 | 
				
			||||||
 | 
					        ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    def test_restart_markers(self, blocks, rows, markers):
 | 
				
			||||||
 | 
					        im = Image.new("RGB", (32, 32))  # 16 MCUs
 | 
				
			||||||
 | 
					        out = BytesIO()
 | 
				
			||||||
 | 
					        im.save(
 | 
				
			||||||
 | 
					            out,
 | 
				
			||||||
 | 
					            format="JPEG",
 | 
				
			||||||
 | 
					            restart_marker_blocks=blocks,
 | 
				
			||||||
 | 
					            restart_marker_rows=rows,
 | 
				
			||||||
 | 
					            # force 8x8 pixel MCUs
 | 
				
			||||||
 | 
					            subsampling=0,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
 | 
					    @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
 | 
				
			||||||
    def test_load_djpeg(self):
 | 
					    def test_load_djpeg(self):
 | 
				
			||||||
        with Image.open(TEST_FILE) as img:
 | 
					        with Image.open(TEST_FILE) as img:
 | 
				
			||||||
| 
						 | 
					@ -767,6 +784,13 @@ class TestFileJpeg:
 | 
				
			||||||
            # This should return the default
 | 
					            # This should return the default
 | 
				
			||||||
            assert im.info.get("dpi") == (72, 72)
 | 
					            assert im.info.get("dpi") == (72, 72)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_dpi_exif_truncated(self):
 | 
				
			||||||
 | 
					        # Arrange
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/truncated_exif_dpi.jpg") as im:
 | 
				
			||||||
 | 
					            # Act / Assert
 | 
				
			||||||
 | 
					            # This should return the default
 | 
				
			||||||
 | 
					            assert im.info.get("dpi") == (72, 72)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_no_dpi_in_exif(self):
 | 
					    def test_no_dpi_in_exif(self):
 | 
				
			||||||
        # Arrange
 | 
					        # Arrange
 | 
				
			||||||
        # This is photoshop-200dpi.jpg with resolution removed from EXIF:
 | 
					        # This is photoshop-200dpi.jpg with resolution removed from EXIF:
 | 
				
			||||||
| 
						 | 
					@ -882,7 +906,10 @@ class TestFileJpeg:
 | 
				
			||||||
    def test_getxmp(self):
 | 
					    def test_getxmp(self):
 | 
				
			||||||
        with Image.open("Tests/images/xmp_test.jpg") as im:
 | 
					        with Image.open("Tests/images/xmp_test.jpg") as im:
 | 
				
			||||||
            if ElementTree is None:
 | 
					            if ElementTree is None:
 | 
				
			||||||
                with pytest.warns(UserWarning):
 | 
					                with pytest.warns(
 | 
				
			||||||
 | 
					                    UserWarning,
 | 
				
			||||||
 | 
					                    match="XMP data cannot be read without defusedxml dependency",
 | 
				
			||||||
 | 
					                ):
 | 
				
			||||||
                    assert im.getxmp() == {}
 | 
					                    assert im.getxmp() == {}
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                xmp = im.getxmp()
 | 
					                xmp = im.getxmp()
 | 
				
			||||||
| 
						 | 
					@ -905,6 +932,28 @@ class TestFileJpeg:
 | 
				
			||||||
            with Image.open("Tests/images/hopper.jpg") as im:
 | 
					            with Image.open("Tests/images/hopper.jpg") as im:
 | 
				
			||||||
                assert im.getxmp() == {}
 | 
					                assert im.getxmp() == {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_getxmp_no_prefix(self):
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/xmp_no_prefix.jpg") as im:
 | 
				
			||||||
 | 
					            if ElementTree is None:
 | 
				
			||||||
 | 
					                with pytest.warns(
 | 
				
			||||||
 | 
					                    UserWarning,
 | 
				
			||||||
 | 
					                    match="XMP data cannot be read without defusedxml dependency",
 | 
				
			||||||
 | 
					                ):
 | 
				
			||||||
 | 
					                    assert im.getxmp() == {}
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                assert im.getxmp() == {"xmpmeta": {"key": "value"}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_getxmp_padded(self):
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/xmp_padded.jpg") as im:
 | 
				
			||||||
 | 
					            if ElementTree is None:
 | 
				
			||||||
 | 
					                with pytest.warns(
 | 
				
			||||||
 | 
					                    UserWarning,
 | 
				
			||||||
 | 
					                    match="XMP data cannot be read without defusedxml dependency",
 | 
				
			||||||
 | 
					                ):
 | 
				
			||||||
 | 
					                    assert im.getxmp() == {}
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                assert im.getxmp() == {"xmpmeta": None}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @pytest.mark.timeout(timeout=1)
 | 
					    @pytest.mark.timeout(timeout=1)
 | 
				
			||||||
    def test_eof(self):
 | 
					    def test_eof(self):
 | 
				
			||||||
        # Even though this decoder never says that it is finished
 | 
					        # Even though this decoder never says that it is finished
 | 
				
			||||||
| 
						 | 
					@ -929,6 +978,28 @@ class TestFileJpeg:
 | 
				
			||||||
            im.load()
 | 
					            im.load()
 | 
				
			||||||
            ImageFile.LOAD_TRUNCATED_IMAGES = False
 | 
					            ImageFile.LOAD_TRUNCATED_IMAGES = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_separate_tables(self):
 | 
				
			||||||
 | 
					        im = hopper()
 | 
				
			||||||
 | 
					        data = []  # [interchange, tables-only, image-only]
 | 
				
			||||||
 | 
					        for streamtype in range(3):
 | 
				
			||||||
 | 
					            out = BytesIO()
 | 
				
			||||||
 | 
					            im.save(out, format="JPEG", streamtype=streamtype)
 | 
				
			||||||
 | 
					            data.append(out.getvalue())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # SOI, EOI
 | 
				
			||||||
 | 
					        for marker in b"\xff\xd8", b"\xff\xd9":
 | 
				
			||||||
 | 
					            assert marker in data[1] and marker in data[2]
 | 
				
			||||||
 | 
					        # DHT, DQT
 | 
				
			||||||
 | 
					        for marker in b"\xff\xc4", b"\xff\xdb":
 | 
				
			||||||
 | 
					            assert marker in data[1] and marker not in data[2]
 | 
				
			||||||
 | 
					        # SOF0, SOS, APP0 (JFIF header)
 | 
				
			||||||
 | 
					        for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0":
 | 
				
			||||||
 | 
					            assert marker not in data[1] and marker in data[2]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with Image.open(BytesIO(data[0])) as interchange_im:
 | 
				
			||||||
 | 
					            with Image.open(BytesIO(data[1] + data[2])) as combined_im:
 | 
				
			||||||
 | 
					                assert_image_equal(interchange_im, combined_im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_repr_jpeg(self):
 | 
					    def test_repr_jpeg(self):
 | 
				
			||||||
        im = hopper()
 | 
					        im = hopper()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -416,7 +416,7 @@ def test_plt_marker():
 | 
				
			||||||
    while True:
 | 
					    while True:
 | 
				
			||||||
        marker = out.read(2)
 | 
					        marker = out.read(2)
 | 
				
			||||||
        if not marker:
 | 
					        if not marker:
 | 
				
			||||||
            assert False, "End of stream without PLT"
 | 
					            pytest.fail("End of stream without PLT")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        jp2_boxid = _binary.i16be(marker)
 | 
					        jp2_boxid = _binary.i16be(marker)
 | 
				
			||||||
        if jp2_boxid == 0xFF4F:
 | 
					        if jp2_boxid == 0xFF4F:
 | 
				
			||||||
| 
						 | 
					@ -426,7 +426,7 @@ def test_plt_marker():
 | 
				
			||||||
            # PLT
 | 
					            # PLT
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        elif jp2_boxid == 0xFF93:
 | 
					        elif jp2_boxid == 0xFF93:
 | 
				
			||||||
            assert False, "SOD without finding PLT first"
 | 
					            pytest.fail("SOD without finding PLT first")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        hdr = out.read(2)
 | 
					        hdr = out.read(2)
 | 
				
			||||||
        length = _binary.i16be(hdr)
 | 
					        length = _binary.i16be(hdr)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,8 +26,7 @@ def open_with_magick(magick, tmp_path, f):
 | 
				
			||||||
    rc = subprocess.call(
 | 
					    rc = subprocess.call(
 | 
				
			||||||
        magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
 | 
					        magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    if rc:
 | 
					    assert not rc
 | 
				
			||||||
        raise OSError
 | 
					 | 
				
			||||||
    return Image.open(outfile)
 | 
					    return Image.open(outfile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -92,11 +92,11 @@ class TestFilePng:
 | 
				
			||||||
            assert im.format == "PNG"
 | 
					            assert im.format == "PNG"
 | 
				
			||||||
            assert im.get_format_mimetype() == "image/png"
 | 
					            assert im.get_format_mimetype() == "image/png"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for mode in ["1", "L", "P", "RGB", "I", "I;16"]:
 | 
					        for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]:
 | 
				
			||||||
            im = hopper(mode)
 | 
					            im = hopper(mode)
 | 
				
			||||||
            im.save(test_file)
 | 
					            im.save(test_file)
 | 
				
			||||||
            with Image.open(test_file) as reloaded:
 | 
					            with Image.open(test_file) as reloaded:
 | 
				
			||||||
                if mode == "I;16":
 | 
					                if mode in ("I;16", "I;16B"):
 | 
				
			||||||
                    reloaded = reloaded.convert(mode)
 | 
					                    reloaded = reloaded.convert(mode)
 | 
				
			||||||
                assert_image_equal(reloaded, im)
 | 
					                assert_image_equal(reloaded, im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -297,7 +297,7 @@ class TestFilePng:
 | 
				
			||||||
        assert_image(im, "RGBA", (10, 10))
 | 
					        assert_image(im, "RGBA", (10, 10))
 | 
				
			||||||
        assert im.getcolors() == [(100, (0, 0, 0, 0))]
 | 
					        assert im.getcolors() == [(100, (0, 0, 0, 0))]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_save_greyscale_transparency(self, tmp_path):
 | 
					    def test_save_grayscale_transparency(self, tmp_path):
 | 
				
			||||||
        for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items():
 | 
					        for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items():
 | 
				
			||||||
            in_file = "Tests/images/" + mode.lower() + "_trns.png"
 | 
					            in_file = "Tests/images/" + mode.lower() + "_trns.png"
 | 
				
			||||||
            with Image.open(in_file) as im:
 | 
					            with Image.open(in_file) as im:
 | 
				
			||||||
| 
						 | 
					@ -665,7 +665,10 @@ class TestFilePng:
 | 
				
			||||||
    def test_getxmp(self):
 | 
					    def test_getxmp(self):
 | 
				
			||||||
        with Image.open("Tests/images/color_snakes.png") as im:
 | 
					        with Image.open("Tests/images/color_snakes.png") as im:
 | 
				
			||||||
            if ElementTree is None:
 | 
					            if ElementTree is None:
 | 
				
			||||||
                with pytest.warns(UserWarning):
 | 
					                with pytest.warns(
 | 
				
			||||||
 | 
					                    UserWarning,
 | 
				
			||||||
 | 
					                    match="XMP data cannot be read without defusedxml dependency",
 | 
				
			||||||
 | 
					                ):
 | 
				
			||||||
                    assert im.getxmp() == {}
 | 
					                    assert im.getxmp() == {}
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                xmp = im.getxmp()
 | 
					                xmp = im.getxmp()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -111,6 +111,11 @@ def test_rgba():
 | 
				
			||||||
        assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png")
 | 
					        assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_layer_skip():
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/five_channels.psd") as im:
 | 
				
			||||||
 | 
					        assert im.n_frames == 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_icc_profile():
 | 
					def test_icc_profile():
 | 
				
			||||||
    with Image.open(test_file) as im:
 | 
					    with Image.open(test_file) as im:
 | 
				
			||||||
        assert "icc_profile" in im.info
 | 
					        assert "icc_profile" in im.info
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -779,7 +779,10 @@ class TestFileTiff:
 | 
				
			||||||
    def test_getxmp(self):
 | 
					    def test_getxmp(self):
 | 
				
			||||||
        with Image.open("Tests/images/lab.tif") as im:
 | 
					        with Image.open("Tests/images/lab.tif") as im:
 | 
				
			||||||
            if ElementTree is None:
 | 
					            if ElementTree is None:
 | 
				
			||||||
                with pytest.warns(UserWarning):
 | 
					                with pytest.warns(
 | 
				
			||||||
 | 
					                    UserWarning,
 | 
				
			||||||
 | 
					                    match="XMP data cannot be read without defusedxml dependency",
 | 
				
			||||||
 | 
					                ):
 | 
				
			||||||
                    assert im.getxmp() == {}
 | 
					                    assert im.getxmp() == {}
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                xmp = im.getxmp()
 | 
					                xmp = im.getxmp()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -118,7 +118,10 @@ def test_getxmp():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with Image.open("Tests/images/flower2.webp") as im:
 | 
					    with Image.open("Tests/images/flower2.webp") as im:
 | 
				
			||||||
        if ElementTree is None:
 | 
					        if ElementTree is None:
 | 
				
			||||||
            with pytest.warns(UserWarning):
 | 
					            with pytest.warns(
 | 
				
			||||||
 | 
					                UserWarning,
 | 
				
			||||||
 | 
					                match="XMP data cannot be read without defusedxml dependency",
 | 
				
			||||||
 | 
					            ):
 | 
				
			||||||
                assert im.getxmp() == {}
 | 
					                assert im.getxmp() == {}
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            assert (
 | 
					            assert (
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import io
 | 
					import io
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import shutil
 | 
					import shutil
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
| 
						 | 
					@ -638,8 +639,8 @@ class TestImage:
 | 
				
			||||||
                im.remap_palette(None)
 | 
					                im.remap_palette(None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_remap_palette_transparency(self):
 | 
					    def test_remap_palette_transparency(self):
 | 
				
			||||||
        im = Image.new("P", (1, 2))
 | 
					        im = Image.new("P", (1, 2), (0, 0, 0))
 | 
				
			||||||
        im.putpixel((0, 1), 1)
 | 
					        im.putpixel((0, 1), (255, 0, 0))
 | 
				
			||||||
        im.info["transparency"] = 0
 | 
					        im.info["transparency"] = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        im_remapped = im.remap_palette([1, 0])
 | 
					        im_remapped = im.remap_palette([1, 0])
 | 
				
			||||||
| 
						 | 
					@ -906,6 +907,13 @@ class TestImage:
 | 
				
			||||||
        im = Image.new("RGB", size)
 | 
					        im = Image.new("RGB", size)
 | 
				
			||||||
        assert im.tobytes() == b""
 | 
					        assert im.tobytes() == b""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
 | 
				
			||||||
 | 
					    def test_zero_frombytes(self, size):
 | 
				
			||||||
 | 
					        Image.frombytes("RGB", size, b"")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        im = Image.new("RGB", size)
 | 
				
			||||||
 | 
					        im.frombytes(b"")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_has_transparency_data(self):
 | 
					    def test_has_transparency_data(self):
 | 
				
			||||||
        for mode in ("1", "L", "P", "RGB"):
 | 
					        for mode in ("1", "L", "P", "RGB"):
 | 
				
			||||||
            im = Image.new(mode, (1, 1))
 | 
					            im = Image.new(mode, (1, 1))
 | 
				
			||||||
| 
						 | 
					@ -992,7 +1000,7 @@ class TestImage:
 | 
				
			||||||
        with Image.open(os.path.join("Tests/images", path)) as im:
 | 
					        with Image.open(os.path.join("Tests/images", path)) as im:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                im.load()
 | 
					                im.load()
 | 
				
			||||||
                assert False
 | 
					                pytest.fail()
 | 
				
			||||||
            except OSError as e:
 | 
					            except OSError as e:
 | 
				
			||||||
                buffer_overrun = str(e) == "buffer overrun when reading image file"
 | 
					                buffer_overrun = str(e) == "buffer overrun when reading image file"
 | 
				
			||||||
                truncated = "image file is truncated" in str(e)
 | 
					                truncated = "image file is truncated" in str(e)
 | 
				
			||||||
| 
						 | 
					@ -1003,10 +1011,19 @@ class TestImage:
 | 
				
			||||||
        with Image.open("Tests/images/fli_overrun2.bin") as im:
 | 
					        with Image.open("Tests/images/fli_overrun2.bin") as im:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                im.seek(1)
 | 
					                im.seek(1)
 | 
				
			||||||
                assert False
 | 
					                pytest.fail()
 | 
				
			||||||
            except OSError as e:
 | 
					            except OSError as e:
 | 
				
			||||||
                assert str(e) == "buffer overrun when reading image file"
 | 
					                assert str(e) == "buffer overrun when reading image file"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_close_graceful(self, caplog):
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/hopper.jpg") as im:
 | 
				
			||||||
 | 
					            copy = im.copy()
 | 
				
			||||||
 | 
					            with caplog.at_level(logging.DEBUG):
 | 
				
			||||||
 | 
					                im.close()
 | 
				
			||||||
 | 
					                copy.close()
 | 
				
			||||||
 | 
					            assert len(caplog.records) == 0
 | 
				
			||||||
 | 
					            assert im.fp is None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MockEncoder:
 | 
					class MockEncoder:
 | 
				
			||||||
    pass
 | 
					    pass
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -130,9 +130,16 @@ class TestImageGetPixel(AccessTest):
 | 
				
			||||||
        bands = Image.getmodebands(mode)
 | 
					        bands = Image.getmodebands(mode)
 | 
				
			||||||
        if bands == 1:
 | 
					        if bands == 1:
 | 
				
			||||||
            return 1
 | 
					            return 1
 | 
				
			||||||
 | 
					        if mode in ("BGR;15", "BGR;16"):
 | 
				
			||||||
 | 
					            # These modes have less than 8 bits per band
 | 
				
			||||||
 | 
					            # So (1, 2, 3) cannot be roundtripped
 | 
				
			||||||
 | 
					            return (16, 32, 49)
 | 
				
			||||||
        return tuple(range(1, bands + 1))
 | 
					        return tuple(range(1, bands + 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check(self, mode, expected_color=None):
 | 
					    def check(self, mode, expected_color=None):
 | 
				
			||||||
 | 
					        if self._need_cffi_access and mode.startswith("BGR;"):
 | 
				
			||||||
 | 
					            pytest.skip("Support not added to deprecated module for BGR;* modes")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not expected_color:
 | 
					        if not expected_color:
 | 
				
			||||||
            expected_color = self.color(mode)
 | 
					            expected_color = self.color(mode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -203,6 +210,9 @@ class TestImageGetPixel(AccessTest):
 | 
				
			||||||
            "F",
 | 
					            "F",
 | 
				
			||||||
            "P",
 | 
					            "P",
 | 
				
			||||||
            "PA",
 | 
					            "PA",
 | 
				
			||||||
 | 
					            "BGR;15",
 | 
				
			||||||
 | 
					            "BGR;16",
 | 
				
			||||||
 | 
					            "BGR;24",
 | 
				
			||||||
            "RGB",
 | 
					            "RGB",
 | 
				
			||||||
            "RGBA",
 | 
					            "RGBA",
 | 
				
			||||||
            "RGBX",
 | 
					            "RGBX",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -117,11 +117,11 @@ def test_trns_p(tmp_path):
 | 
				
			||||||
    f = str(tmp_path / "temp.png")
 | 
					    f = str(tmp_path / "temp.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    im_l = im.convert("L")
 | 
					    im_l = im.convert("L")
 | 
				
			||||||
    assert im_l.info["transparency"] == 1  # undone
 | 
					    assert im_l.info["transparency"] == 0
 | 
				
			||||||
    im_l.save(f)
 | 
					    im_l.save(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    im_rgb = im.convert("RGB")
 | 
					    im_rgb = im.convert("RGB")
 | 
				
			||||||
    assert im_rgb.info["transparency"] == (0, 1, 2)  # undone
 | 
					    assert im_rgb.info["transparency"] == (0, 0, 0)
 | 
				
			||||||
    im_rgb.save(f)
 | 
					    im_rgb.save(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -76,6 +76,15 @@ def test_mode_F():
 | 
				
			||||||
    assert list(im.getdata()) == target
 | 
					    assert list(im.getdata()) == target
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
 | 
				
			||||||
 | 
					def test_mode_BGR(mode):
 | 
				
			||||||
 | 
					    data = [(16, 32, 49), (32, 32, 98)]
 | 
				
			||||||
 | 
					    im = Image.new(mode, (1, 2))
 | 
				
			||||||
 | 
					    im.putdata(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert list(im.getdata()) == data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_array_B():
 | 
					def test_array_B():
 | 
				
			||||||
    # shouldn't segfault
 | 
					    # shouldn't segfault
 | 
				
			||||||
    # see https://github.com/python-pillow/Pillow/issues/1008
 | 
					    # see https://github.com/python-pillow/Pillow/issues/1008
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -84,3 +84,14 @@ def test_rgba_palette(mode, palette):
 | 
				
			||||||
    im.putpalette(palette, mode)
 | 
					    im.putpalette(palette, mode)
 | 
				
			||||||
    assert im.getpalette() == [1, 2, 3]
 | 
					    assert im.getpalette() == [1, 2, 3]
 | 
				
			||||||
    assert im.palette.colors == {(1, 2, 3, 4): 0}
 | 
					    assert im.palette.colors == {(1, 2, 3, 4): 0}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_empty_palette():
 | 
				
			||||||
 | 
					    im = Image.new("P", (1, 1))
 | 
				
			||||||
 | 
					    assert im.getpalette() == []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_undefined_palette_index():
 | 
				
			||||||
 | 
					    im = Image.new("P", (1, 1), 3)
 | 
				
			||||||
 | 
					    im.putpalette((1, 2, 3))
 | 
				
			||||||
 | 
					    assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,7 +67,7 @@ def test_quantize_no_dither():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_quantize_no_dither2():
 | 
					def test_quantize_no_dither2():
 | 
				
			||||||
    im = Image.new("RGB", (9, 1))
 | 
					    im = Image.new("RGB", (9, 1))
 | 
				
			||||||
    im.putdata(list((p,) * 3 for p in range(0, 36, 4)))
 | 
					    im.putdata([(p,) * 3 for p in range(0, 36, 4)])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    palette = Image.new("P", (1, 1))
 | 
					    palette = Image.new("P", (1, 1))
 | 
				
			||||||
    data = (0, 0, 0, 32, 32, 32)
 | 
					    data = (0, 0, 0, 32, 32, 32)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -195,7 +195,7 @@ class TestReducingGapResize:
 | 
				
			||||||
            (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
 | 
					            (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with pytest.raises(AssertionError):
 | 
					        with pytest.raises(pytest.fail.Exception):
 | 
				
			||||||
            assert_image_equal(ref, im)
 | 
					            assert_image_equal(ref, im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert_image_similar(ref, im, epsilon)
 | 
					        assert_image_similar(ref, im, epsilon)
 | 
				
			||||||
| 
						 | 
					@ -210,7 +210,7 @@ class TestReducingGapResize:
 | 
				
			||||||
            (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
 | 
					            (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with pytest.raises(AssertionError):
 | 
					        with pytest.raises(pytest.fail.Exception):
 | 
				
			||||||
            assert_image_equal(ref, im)
 | 
					            assert_image_equal(ref, im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert_image_similar(ref, im, epsilon)
 | 
					        assert_image_similar(ref, im, epsilon)
 | 
				
			||||||
| 
						 | 
					@ -225,7 +225,7 @@ class TestReducingGapResize:
 | 
				
			||||||
            (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
 | 
					            (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with pytest.raises(AssertionError):
 | 
					        with pytest.raises(pytest.fail.Exception):
 | 
				
			||||||
            assert_image_equal(ref, im)
 | 
					            assert_image_equal(ref, im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert_image_similar(ref, im, epsilon)
 | 
					        assert_image_similar(ref, im, epsilon)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -147,7 +147,7 @@ def test_reducing_gap_values():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ref = hopper()
 | 
					    ref = hopper()
 | 
				
			||||||
    ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None)
 | 
					    ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None)
 | 
				
			||||||
    with pytest.raises(AssertionError):
 | 
					    with pytest.raises(pytest.fail.Exception):
 | 
				
			||||||
        assert_image_equal(ref, im)
 | 
					        assert_image_equal(ref, im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assert_image_similar(ref, im, 3.5)
 | 
					    assert_image_similar(ref, im, 3.5)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@ GREEN = (0, 255, 0)
 | 
				
			||||||
ORANGE = (255, 128, 0)
 | 
					ORANGE = (255, 128, 0)
 | 
				
			||||||
WHITE = (255, 255, 255)
 | 
					WHITE = (255, 255, 255)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
GREY = 128
 | 
					GRAY = 128
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_sanity():
 | 
					def test_sanity():
 | 
				
			||||||
| 
						 | 
					@ -121,12 +121,12 @@ def test_constant():
 | 
				
			||||||
    im = Image.new("RGB", (20, 10))
 | 
					    im = Image.new("RGB", (20, 10))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Act
 | 
					    # Act
 | 
				
			||||||
    new = ImageChops.constant(im, GREY)
 | 
					    new = ImageChops.constant(im, GRAY)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Assert
 | 
					    # Assert
 | 
				
			||||||
    assert new.size == im.size
 | 
					    assert new.size == im.size
 | 
				
			||||||
    assert new.getpixel((0, 0)) == GREY
 | 
					    assert new.getpixel((0, 0)) == GRAY
 | 
				
			||||||
    assert new.getpixel((19, 9)) == GREY
 | 
					    assert new.getpixel((19, 9)) == GRAY
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_darker_image():
 | 
					def test_darker_image():
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,9 @@
 | 
				
			||||||
 | 
					import contextlib
 | 
				
			||||||
import os.path
 | 
					import os.path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import Image, ImageColor, ImageDraw, ImageFont
 | 
					from PIL import Image, ImageColor, ImageDraw, ImageFont, features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .helper import (
 | 
					from .helper import (
 | 
				
			||||||
    assert_image_equal,
 | 
					    assert_image_equal,
 | 
				
			||||||
| 
						 | 
					@ -586,6 +587,18 @@ def test_point(points):
 | 
				
			||||||
    assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
 | 
					    assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_point_I16():
 | 
				
			||||||
 | 
					    # Arrange
 | 
				
			||||||
 | 
					    im = Image.new("I;16", (1, 1))
 | 
				
			||||||
 | 
					    draw = ImageDraw.Draw(im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Act
 | 
				
			||||||
 | 
					    draw.point((0, 0), fill=0x1234)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Assert
 | 
				
			||||||
 | 
					    assert im.getpixel((0, 0)) == 0x1234
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize("points", POINTS)
 | 
					@pytest.mark.parametrize("points", POINTS)
 | 
				
			||||||
def test_polygon(points):
 | 
					def test_polygon(points):
 | 
				
			||||||
    # Arrange
 | 
					    # Arrange
 | 
				
			||||||
| 
						 | 
					@ -732,7 +745,7 @@ def test_rectangle_I16(bbox):
 | 
				
			||||||
    draw = ImageDraw.Draw(im)
 | 
					    draw = ImageDraw.Draw(im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Act
 | 
					    # Act
 | 
				
			||||||
    draw.rectangle(bbox, fill="black", outline="green")
 | 
					    draw.rectangle(bbox, outline=0xFFFF)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Assert
 | 
					    # Assert
 | 
				
			||||||
    assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")
 | 
					    assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png")
 | 
				
			||||||
| 
						 | 
					@ -1341,7 +1354,33 @@ def test_setting_default_font():
 | 
				
			||||||
        assert draw.getfont() == font
 | 
					        assert draw.getfont() == font
 | 
				
			||||||
    finally:
 | 
					    finally:
 | 
				
			||||||
        ImageDraw.ImageDraw.font = None
 | 
					        ImageDraw.ImageDraw.font = None
 | 
				
			||||||
        assert isinstance(draw.getfont(), ImageFont.ImageFont)
 | 
					        assert isinstance(draw.getfont(), ImageFont.load_default().__class__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_default_font_size():
 | 
				
			||||||
 | 
					    freetype_support = features.check_module("freetype2")
 | 
				
			||||||
 | 
					    text = "Default font at a specific size."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    im = Image.new("RGB", (220, 25))
 | 
				
			||||||
 | 
					    draw = ImageDraw.Draw(im)
 | 
				
			||||||
 | 
					    with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
 | 
				
			||||||
 | 
					        draw.text((0, 0), text, font_size=16)
 | 
				
			||||||
 | 
					        assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
 | 
				
			||||||
 | 
					        assert draw.textlength(text, font_size=16) == 216
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
 | 
				
			||||||
 | 
					        assert draw.textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    im = Image.new("RGB", (220, 25))
 | 
				
			||||||
 | 
					    draw = ImageDraw.Draw(im)
 | 
				
			||||||
 | 
					    with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
 | 
				
			||||||
 | 
					        draw.multiline_text((0, 0), text, font_size=16)
 | 
				
			||||||
 | 
					        assert_image_equal_tofile(im, "Tests/images/imagedraw_default_font_size.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with contextlib.nullcontext() if freetype_support else pytest.raises(ImportError):
 | 
				
			||||||
 | 
					        assert draw.multiline_textbbox((0, 0), text, font_size=16) == (0, 3, 216, 19)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize("bbox", BBOX)
 | 
					@pytest.mark.parametrize("bbox", BBOX)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -115,6 +115,7 @@ class TestImageFile:
 | 
				
			||||||
        assert_image_equal(im1, im2)
 | 
					        assert_image_equal(im1, im2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_raise_oserror(self):
 | 
					    def test_raise_oserror(self):
 | 
				
			||||||
 | 
					        with pytest.warns(DeprecationWarning):
 | 
				
			||||||
            with pytest.raises(OSError):
 | 
					            with pytest.raises(OSError):
 | 
				
			||||||
                ImageFile.raise_oserror(1)
 | 
					                ImageFile.raise_oserror(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import re
 | 
				
			||||||
import shutil
 | 
					import shutil
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
from io import BytesIO
 | 
					from io import BytesIO
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
from packaging.version import parse as parse_version
 | 
					from packaging.version import parse as parse_version
 | 
				
			||||||
| 
						 | 
					@ -76,8 +77,9 @@ def _render(font, layout_engine):
 | 
				
			||||||
    return img
 | 
					    return img
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_font_with_name(layout_engine):
 | 
					@pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH)))
 | 
				
			||||||
    _render(FONT_PATH, layout_engine)
 | 
					def test_font_with_name(layout_engine, font):
 | 
				
			||||||
 | 
					    _render(font, layout_engine)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_font_with_filelike(layout_engine):
 | 
					def test_font_with_filelike(layout_engine):
 | 
				
			||||||
| 
						 | 
					@ -141,7 +143,9 @@ def test_I16(font):
 | 
				
			||||||
    draw = ImageDraw.Draw(im)
 | 
					    draw = ImageDraw.Draw(im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    txt = "Hello World!"
 | 
					    txt = "Hello World!"
 | 
				
			||||||
    draw.text((10, 10), txt, font=font)
 | 
					    draw.text((10, 10), txt, fill=0xFFFE, font=font)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert im.getpixel((12, 14)) == 0xFFFE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    target = "Tests/images/transparent_background_text_L.png"
 | 
					    target = "Tests/images/transparent_background_text_L.png"
 | 
				
			||||||
    assert_image_similar_tofile(im.convert("L"), target, 0.01)
 | 
					    assert_image_similar_tofile(im.convert("L"), target, 0.01)
 | 
				
			||||||
| 
						 | 
					@ -301,8 +305,8 @@ def test_multiline_spacing(font):
 | 
				
			||||||
    "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
 | 
					    "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270)
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
def test_rotated_transposed_font(font, orientation):
 | 
					def test_rotated_transposed_font(font, orientation):
 | 
				
			||||||
    img_grey = Image.new("L", (100, 100))
 | 
					    img_gray = Image.new("L", (100, 100))
 | 
				
			||||||
    draw = ImageDraw.Draw(img_grey)
 | 
					    draw = ImageDraw.Draw(img_gray)
 | 
				
			||||||
    word = "testing"
 | 
					    word = "testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
 | 
					    transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
 | 
				
			||||||
| 
						 | 
					@ -342,8 +346,8 @@ def test_rotated_transposed_font(font, orientation):
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
def test_unrotated_transposed_font(font, orientation):
 | 
					def test_unrotated_transposed_font(font, orientation):
 | 
				
			||||||
    img_grey = Image.new("L", (100, 100))
 | 
					    img_gray = Image.new("L", (100, 100))
 | 
				
			||||||
    draw = ImageDraw.Draw(img_grey)
 | 
					    draw = ImageDraw.Draw(img_gray)
 | 
				
			||||||
    word = "testing"
 | 
					    word = "testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
 | 
					    transposed_font = ImageFont.TransposedFont(font, orientation=orientation)
 | 
				
			||||||
| 
						 | 
					@ -451,7 +455,7 @@ def test_load_non_font_bytes():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_default_font():
 | 
					def test_default_font():
 | 
				
			||||||
    # Arrange
 | 
					    # Arrange
 | 
				
			||||||
    txt = 'This is a "better than nothing" default font.'
 | 
					    txt = "This is a default font using FreeType support."
 | 
				
			||||||
    im = Image.new(mode="RGB", size=(300, 100))
 | 
					    im = Image.new(mode="RGB", size=(300, 100))
 | 
				
			||||||
    draw = ImageDraw.Draw(im)
 | 
					    draw = ImageDraw.Draw(im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -459,8 +463,11 @@ def test_default_font():
 | 
				
			||||||
    default_font = ImageFont.load_default()
 | 
					    default_font = ImageFont.load_default()
 | 
				
			||||||
    draw.text((10, 10), txt, font=default_font)
 | 
					    draw.text((10, 10), txt, font=default_font)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    larger_default_font = ImageFont.load_default(size=14)
 | 
				
			||||||
 | 
					    draw.text((10, 60), txt, font=larger_default_font)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Assert
 | 
					    # Assert
 | 
				
			||||||
    assert_image_equal_tofile(im, "Tests/images/default_font.png")
 | 
					    assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
 | 
					@pytest.mark.parametrize("mode", (None, "1", "RGBA"))
 | 
				
			||||||
| 
						 | 
					@ -483,14 +490,6 @@ def test_render_empty(font):
 | 
				
			||||||
    assert_image_equal(im, target)
 | 
					    assert_image_equal(im, target)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_unicode_pilfont():
 | 
					 | 
				
			||||||
    # should not segfault, should return UnicodeDecodeError
 | 
					 | 
				
			||||||
    # issue #2826
 | 
					 | 
				
			||||||
    font = ImageFont.load_default()
 | 
					 | 
				
			||||||
    with pytest.raises(UnicodeEncodeError):
 | 
					 | 
				
			||||||
        font.getbbox("’")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def test_unicode_extended(layout_engine):
 | 
					def test_unicode_extended(layout_engine):
 | 
				
			||||||
    # issue #3777
 | 
					    # issue #3777
 | 
				
			||||||
    text = "A\u278A\U0001F12B"
 | 
					    text = "A\u278A\U0001F12B"
 | 
				
			||||||
| 
						 | 
					@ -720,14 +719,6 @@ def test_variation_set_by_axes(font):
 | 
				
			||||||
    _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)
 | 
					    _check_text(font, "Tests/images/variation_tiny_axes.png", 32.5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_textbbox_non_freetypefont():
 | 
					 | 
				
			||||||
    im = Image.new("RGB", (200, 200))
 | 
					 | 
				
			||||||
    d = ImageDraw.Draw(im)
 | 
					 | 
				
			||||||
    default_font = ImageFont.load_default()
 | 
					 | 
				
			||||||
    assert d.textlength("test", font=default_font) == 24
 | 
					 | 
				
			||||||
    assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@pytest.mark.parametrize(
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
    "anchor, left, top",
 | 
					    "anchor, left, top",
 | 
				
			||||||
    (
 | 
					    (
 | 
				
			||||||
| 
						 | 
					@ -1082,3 +1073,9 @@ def test_raqm_missing_warning(monkeypatch):
 | 
				
			||||||
        "Raqm layout was requested, but Raqm is not available. "
 | 
					        "Raqm layout was requested, but Raqm is not available. "
 | 
				
			||||||
        "Falling back to basic layout."
 | 
					        "Falling back to basic layout."
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize("size", [-1, 0])
 | 
				
			||||||
 | 
					def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size):
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					        ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										45
									
								
								Tests/test_imagefontpil.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,45 @@
 | 
				
			||||||
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from PIL import Image, ImageDraw, ImageFont, features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .helper import assert_image_equal_tofile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pytestmark = pytest.mark.skipif(
 | 
				
			||||||
 | 
					    features.check_module("freetype2"),
 | 
				
			||||||
 | 
					    reason="PILfont superseded if FreeType is supported",
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_default_font():
 | 
				
			||||||
 | 
					    # Arrange
 | 
				
			||||||
 | 
					    txt = 'This is a "better than nothing" default font.'
 | 
				
			||||||
 | 
					    im = Image.new(mode="RGB", size=(300, 100))
 | 
				
			||||||
 | 
					    draw = ImageDraw.Draw(im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Act
 | 
				
			||||||
 | 
					    default_font = ImageFont.load_default()
 | 
				
			||||||
 | 
					    draw.text((10, 10), txt, font=default_font)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Assert
 | 
				
			||||||
 | 
					    assert_image_equal_tofile(im, "Tests/images/default_font.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_size_without_freetype():
 | 
				
			||||||
 | 
					    with pytest.raises(ImportError):
 | 
				
			||||||
 | 
					        ImageFont.load_default(size=14)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_unicode():
 | 
				
			||||||
 | 
					    # should not segfault, should return UnicodeDecodeError
 | 
				
			||||||
 | 
					    # issue #2826
 | 
				
			||||||
 | 
					    font = ImageFont.load_default()
 | 
				
			||||||
 | 
					    with pytest.raises(UnicodeEncodeError):
 | 
				
			||||||
 | 
					        font.getbbox("’")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_textbbox():
 | 
				
			||||||
 | 
					    im = Image.new("RGB", (200, 200))
 | 
				
			||||||
 | 
					    d = ImageDraw.Draw(im)
 | 
				
			||||||
 | 
					    default_font = ImageFont.load_default()
 | 
				
			||||||
 | 
					    assert d.textlength("test", font=default_font) == 24
 | 
				
			||||||
 | 
					    assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11)
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,10 @@ from .helper import assert_image_equal_tofile, skip_unless_feature
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestImageGrab:
 | 
					class TestImageGrab:
 | 
				
			||||||
 | 
					    @pytest.mark.skipif(
 | 
				
			||||||
 | 
					        os.environ.get("USERNAME") == "ContainerAdministrator",
 | 
				
			||||||
 | 
					        reason="can't grab screen when running in Docker",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    @pytest.mark.skipif(
 | 
					    @pytest.mark.skipif(
 | 
				
			||||||
        sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS"
 | 
					        sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,6 +39,9 @@ def test_sanity():
 | 
				
			||||||
    ImageOps.contain(hopper("L"), (128, 128))
 | 
					    ImageOps.contain(hopper("L"), (128, 128))
 | 
				
			||||||
    ImageOps.contain(hopper("RGB"), (128, 128))
 | 
					    ImageOps.contain(hopper("RGB"), (128, 128))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ImageOps.cover(hopper("L"), (128, 128))
 | 
				
			||||||
 | 
					    ImageOps.cover(hopper("RGB"), (128, 128))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ImageOps.crop(hopper("L"), 1)
 | 
					    ImageOps.crop(hopper("L"), 1)
 | 
				
			||||||
    ImageOps.crop(hopper("RGB"), 1)
 | 
					    ImageOps.crop(hopper("RGB"), 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -119,6 +122,20 @@ def test_contain_round():
 | 
				
			||||||
    assert new_im.height == 5
 | 
					    assert new_im.height == 5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
 | 
					    "image_name, expected_size",
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        ("colr_bungee.png", (1024, 256)),  # landscape
 | 
				
			||||||
 | 
					        ("imagedraw_stroke_multiline.png", (256, 640)),  # portrait
 | 
				
			||||||
 | 
					        ("hopper.png", (256, 256)),  # square
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_cover(image_name, expected_size):
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/" + image_name) as im:
 | 
				
			||||||
 | 
					        new_im = ImageOps.cover(im, (256, 256))
 | 
				
			||||||
 | 
					        assert new_im.size == expected_size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_pad():
 | 
					def test_pad():
 | 
				
			||||||
    # Same ratio
 | 
					    # Same ratio
 | 
				
			||||||
    im = hopper()
 | 
					    im = hopper()
 | 
				
			||||||
| 
						 | 
					@ -416,6 +433,12 @@ def test_exif_transpose_in_place():
 | 
				
			||||||
        assert_image_equal(im, expected)
 | 
					        assert_image_equal(im, expected)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_autocontrast_unsupported_mode():
 | 
				
			||||||
 | 
					    im = Image.new("RGBA", (1, 1))
 | 
				
			||||||
 | 
					    with pytest.raises(OSError):
 | 
				
			||||||
 | 
					        ImageOps.autocontrast(im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_autocontrast_cutoff():
 | 
					def test_autocontrast_cutoff():
 | 
				
			||||||
    # Test the cutoff argument of autocontrast
 | 
					    # Test the cutoff argument of autocontrast
 | 
				
			||||||
    with Image.open("Tests/images/bw_gradient.png") as img:
 | 
					    with Image.open("Tests/images/bw_gradient.png") as img:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -85,7 +85,7 @@ def test_ipythonviewer():
 | 
				
			||||||
            test_viewer = viewer
 | 
					            test_viewer = viewer
 | 
				
			||||||
            break
 | 
					            break
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        assert False
 | 
					        pytest.fail()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    im = hopper()
 | 
					    im = hopper()
 | 
				
			||||||
    assert test_viewer.show(im) == 1
 | 
					    assert test_viewer.show(im) == 1
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -340,6 +340,17 @@ class TestLibUnpack:
 | 
				
			||||||
            self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0))
 | 
					            self.assert_unpack("RGB", "G;16N", 2, (0, 1, 0), (0, 3, 0), (0, 5, 0))
 | 
				
			||||||
            self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5))
 | 
					            self.assert_unpack("RGB", "B;16N", 2, (0, 0, 1), (0, 0, 3), (0, 0, 5))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assert_unpack(
 | 
				
			||||||
 | 
					            "RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_BGR(self):
 | 
				
			||||||
 | 
					        self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8))
 | 
				
			||||||
 | 
					        self.assert_unpack(
 | 
				
			||||||
 | 
					            "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_RGBA(self):
 | 
					    def test_RGBA(self):
 | 
				
			||||||
        self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))
 | 
					        self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6))
 | 
				
			||||||
        self.assert_unpack(
 | 
					        self.assert_unpack(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from setuptools.build_meta import *  # noqa: F401, F403
 | 
					from setuptools.build_meta import *  # noqa: F403
 | 
				
			||||||
from setuptools.build_meta import build_wheel
 | 
					from setuptools.build_meta import build_wheel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
backend_class = build_wheel.__self__.__class__
 | 
					backend_class = build_wheel.__self__.__class__
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
#!/bin/bash
 | 
					#!/bin/bash
 | 
				
			||||||
# install libimagequant
 | 
					# install libimagequant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
archive=libimagequant-4.2.1
 | 
					archive=libimagequant-4.2.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
 | 
					./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,7 @@ The fork author's goal is to foster and support active development of PIL throug
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. _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
 | 
					.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
 | 
				
			||||||
.. _Travis CI: https://app.travis-ci.com/github/python-pillow/pillow-wheels
 | 
					.. _Travis CI: https://app.travis-ci.com/github/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/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										25
									
								
								docs/conf.py
									
									
									
									
									
								
							
							
						
						| 
						 | 
					@ -166,6 +166,12 @@ html_static_path = ["resources"]
 | 
				
			||||||
# directly to the root of the documentation.
 | 
					# directly to the root of the documentation.
 | 
				
			||||||
# html_extra_path = []
 | 
					# html_extra_path = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html_css_files = ["css/dark.css"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html_js_files = [
 | 
				
			||||||
 | 
					    "js/activate_tab.js",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 | 
					# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 | 
				
			||||||
# using the given strftime format.
 | 
					# using the given strftime format.
 | 
				
			||||||
# html_last_updated_fmt = '%b %d, %Y'
 | 
					# html_last_updated_fmt = '%b %d, %Y'
 | 
				
			||||||
| 
						 | 
					@ -313,19 +319,15 @@ texinfo_documents = [
 | 
				
			||||||
# texinfo_no_detailmenu = False
 | 
					# texinfo_no_detailmenu = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def setup(app):
 | 
					 | 
				
			||||||
    app.add_css_file("css/dark.css")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
linkcheck_allowed_redirects = {
 | 
					linkcheck_allowed_redirects = {
 | 
				
			||||||
    r"https://bestpractices.coreinfrastructure.org/projects/6331": r"https://bestpractices.coreinfrastructure.org/en/.*",  # noqa: E501
 | 
					    r"https://www.bestpractices.dev/projects/6331": r"https://www.bestpractices.dev/en/.*",
 | 
				
			||||||
    r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg",  # noqa: E501
 | 
					    r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg",
 | 
				
			||||||
    r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*",  # noqa: E501
 | 
					    r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*",
 | 
				
			||||||
    r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest",  # noqa: E501
 | 
					    r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest",
 | 
				
			||||||
    r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/",
 | 
					    r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/",
 | 
				
			||||||
    r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*",  # noqa: E501
 | 
					    r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*",
 | 
				
			||||||
    r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg",  # noqa: E501
 | 
					    r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg",
 | 
				
			||||||
    r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+",  # noqa: E501
 | 
					    r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# sphinx.ext.extlinks
 | 
					# sphinx.ext.extlinks
 | 
				
			||||||
| 
						 | 
					@ -338,6 +340,7 @@ extlinks = {
 | 
				
			||||||
    "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
 | 
					    "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"),
 | 
				
			||||||
    "issue": (_repo + "issues/%s", "#%s"),
 | 
					    "issue": (_repo + "issues/%s", "#%s"),
 | 
				
			||||||
    "pr": (_repo + "pull/%s", "#%s"),
 | 
					    "pr": (_repo + "pull/%s", "#%s"),
 | 
				
			||||||
 | 
					    "pypi": ("https://pypi.org/project/%s/", "%s"),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# sphinxext.opengraph
 | 
					# sphinxext.opengraph
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@ Deprecated features
 | 
				
			||||||
-------------------
 | 
					-------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Below are features which are considered deprecated. Where appropriate,
 | 
					Below are features which are considered deprecated. Where appropriate,
 | 
				
			||||||
a ``DeprecationWarning`` is issued.
 | 
					a :py:exc:`DeprecationWarning` is issued.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PSFile
 | 
					PSFile
 | 
				
			||||||
~~~~~~
 | 
					~~~~~~
 | 
				
			||||||
| 
						 | 
					@ -34,6 +34,16 @@ Since Pillow's C API is now faster than PyAccess on PyPy,
 | 
				
			||||||
``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is
 | 
					``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is
 | 
				
			||||||
similarly deprecated.
 | 
					similarly deprecated.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ImageFile.raise_oserror
 | 
				
			||||||
 | 
					~~~~~~~~~~~~~~~~~~~~~~~
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. deprecated:: 10.2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					``ImageFile.raise_oserror()`` has been deprecated and will be removed in Pillow
 | 
				
			||||||
 | 
					12.0.0 (2025-10-15). The function is undocumented and is only useful for translating
 | 
				
			||||||
 | 
					error codes returned by a codec's ``decode()`` method, which ImageFile already does
 | 
				
			||||||
 | 
					automatically.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Removed features
 | 
					Removed features
 | 
				
			||||||
----------------
 | 
					----------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -267,7 +277,7 @@ ImageFile.raise_ioerror
 | 
				
			||||||
.. deprecated:: 7.2.0
 | 
					.. deprecated:: 7.2.0
 | 
				
			||||||
.. versionremoved:: 9.0.0
 | 
					.. versionremoved:: 9.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
``IOError`` was merged into ``OSError`` in Python 3.3.
 | 
					:py:exc:`IOError` was merged into :py:exc:`OSError` in Python 3.3.
 | 
				
			||||||
So, ``ImageFile.raise_ioerror`` has been removed.
 | 
					So, ``ImageFile.raise_ioerror`` has been removed.
 | 
				
			||||||
Use ``ImageFile.raise_oserror`` instead.
 | 
					Use ``ImageFile.raise_oserror`` instead.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -293,9 +303,9 @@ im.offset
 | 
				
			||||||
``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead.
 | 
					``im.offset()`` has been removed, call :py:func:`.ImageChops.offset()` instead.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
It was documented as deprecated in PIL 1.1.2,
 | 
					It was documented as deprecated in PIL 1.1.2,
 | 
				
			||||||
raised a ``DeprecationWarning`` since 1.1.5,
 | 
					raised a :py:exc:`DeprecationWarning` since 1.1.5,
 | 
				
			||||||
an ``Exception`` since Pillow 3.0.0
 | 
					an :py:exc:`Exception` since Pillow 3.0.0
 | 
				
			||||||
and ``NotImplementedError`` since 3.3.0.
 | 
					and :py:exc:`NotImplementedError` since 3.3.0.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Image.fromstring, im.fromstring and im.tostring
 | 
					Image.fromstring, im.fromstring and im.tostring
 | 
				
			||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | 
					~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | 
				
			||||||
| 
						 | 
					@ -307,9 +317,9 @@ Image.fromstring, im.fromstring and im.tostring
 | 
				
			||||||
* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead.
 | 
					* ``im.fromstring()`` has been removed, call :py:meth:`~PIL.Image.Image.frombytes()` instead.
 | 
				
			||||||
* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead.
 | 
					* ``im.tostring()`` has been removed, call :py:meth:`~PIL.Image.Image.tobytes()` instead.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
They issued a ``DeprecationWarning`` since 2.0.0,
 | 
					They issued a :py:exc:`DeprecationWarning` since 2.0.0,
 | 
				
			||||||
an ``Exception`` since 3.0.0
 | 
					an :py:exc:`Exception` since 3.0.0
 | 
				
			||||||
and ``NotImplementedError`` since 3.3.0.
 | 
					and :py:exc:`NotImplementedError` since 3.3.0.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ImageCms.CmsProfile attributes
 | 
					ImageCms.CmsProfile attributes
 | 
				
			||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | 
					~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | 
				
			||||||
| 
						 | 
					@ -318,7 +328,7 @@ ImageCms.CmsProfile attributes
 | 
				
			||||||
.. versionremoved:: 8.0.0
 | 
					.. versionremoved:: 8.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0,
 | 
					Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0,
 | 
				
			||||||
they issued a ``DeprecationWarning``:
 | 
					they issued a :py:exc:`DeprecationWarning`:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
========================  ===================================================
 | 
					========================  ===================================================
 | 
				
			||||||
Removed                   Use instead
 | 
					Removed                   Use instead
 | 
				
			||||||
| 
						 | 
					@ -442,7 +452,7 @@ PIL.OleFileIO
 | 
				
			||||||
.. deprecated:: 4.0.0
 | 
					.. deprecated:: 4.0.0
 | 
				
			||||||
.. versionremoved:: 6.0.0
 | 
					.. versionremoved:: 6.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PIL.OleFileIO was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of
 | 
					``PIL.OleFileIO`` was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of
 | 
				
			||||||
the upstream olefile Python package, and replaced with an ``ImportError`` in 5.0.0
 | 
					the upstream :pypi:`olefile` Python package, and replaced with an :py:exc:`ImportError` in 5.0.0
 | 
				
			||||||
(2018-01). The deprecated file has now been removed from Pillow. If needed, install from
 | 
					(2018-01). The deprecated file has now been removed from Pillow. If needed, install from
 | 
				
			||||||
PyPI (eg. ``python3 -m pip install olefile``).
 | 
					PyPI (eg. ``python3 -m pip install olefile``).
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								docs/example/image_thumbnail.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 19 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/example/imageops_contain.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 19 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/example/imageops_cover.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 38 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/example/imageops_fit.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 28 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/example/imageops_pad.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 19 KiB  | 
| 
						 | 
					@ -115,8 +115,13 @@ in ``L``, ``RGB`` and ``CMYK`` modes.
 | 
				
			||||||
Loading
 | 
					Loading
 | 
				
			||||||
~~~~~~~
 | 
					~~~~~~~
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To use Ghostscript, Pillow searches for the "gs" executable. On Windows, it
 | 
				
			||||||
 | 
					also searches for "gswin32c" and "gswin64c". To customise this behaviour,
 | 
				
			||||||
 | 
					``EpsImagePlugin.gs_binary = "gswin64"`` will set the name of the executable to
 | 
				
			||||||
 | 
					use. ``EpsImagePlugin.gs_binary = False`` will prevent Ghostscript use.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
 | 
					If Ghostscript is available, you can call the :py:meth:`~PIL.Image.Image.load`
 | 
				
			||||||
method with the following parameters to affect how Ghostscript renders the EPS
 | 
					method with the following parameters to affect how Ghostscript renders the EPS.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**scale**
 | 
					**scale**
 | 
				
			||||||
    Affects the scale of the resultant rasterized image. If the EPS suggests
 | 
					    Affects the scale of the resultant rasterized image. If the EPS suggests
 | 
				
			||||||
| 
						 | 
					@ -261,9 +266,13 @@ following options are available::
 | 
				
			||||||
    :py:class:`PIL.ImagePalette.ImagePalette` object.
 | 
					    :py:class:`PIL.ImagePalette.ImagePalette` object.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**optimize**
 | 
					**optimize**
 | 
				
			||||||
    If present and true, attempt to compress the palette by
 | 
					    Whether to attempt to compress the palette by eliminating unused colors
 | 
				
			||||||
    eliminating unused colors. This is only useful if the palette can
 | 
					    (this is only useful if the palette can be compressed to the next smaller
 | 
				
			||||||
    be compressed to the next smaller power of 2 elements.
 | 
					    power of 2 elements) and whether to mark all pixels that are not new in the
 | 
				
			||||||
 | 
					    next frame as transparent.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This is attempted by default, unless a palette is specified as an option or
 | 
				
			||||||
 | 
					    as part of the first image's :py:attr:`~PIL.Image.Image.info` dictionary.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Note that if the image you are saving comes from an existing GIF, it may have
 | 
					Note that if the image you are saving comes from an existing GIF, it may have
 | 
				
			||||||
the following properties in its :py:attr:`~PIL.Image.Image.info` dictionary.
 | 
					the following properties in its :py:attr:`~PIL.Image.Image.info` dictionary.
 | 
				
			||||||
| 
						 | 
					@ -489,6 +498,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    If absent, the setting will be determined by libjpeg or libjpeg-turbo.
 | 
					    If absent, the setting will be determined by libjpeg or libjpeg-turbo.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**restart_marker_blocks**
 | 
				
			||||||
 | 
					    If present, emit a restart marker whenever the specified number of MCU
 | 
				
			||||||
 | 
					    blocks has been produced.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. versionadded:: 10.2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**restart_marker_rows**
 | 
				
			||||||
 | 
					    If present, emit a restart marker whenever the specified number of MCU
 | 
				
			||||||
 | 
					    rows has been produced.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. versionadded:: 10.2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**qtables**
 | 
					**qtables**
 | 
				
			||||||
    If present, sets the qtables for the encoder. This is listed as an
 | 
					    If present, sets the qtables for the encoder. This is listed as an
 | 
				
			||||||
    advanced option for wizards in the JPEG documentation. Use with
 | 
					    advanced option for wizards in the JPEG documentation. Use with
 | 
				
			||||||
| 
						 | 
					@ -501,6 +522,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .. versionadded:: 2.5.0
 | 
					    .. versionadded:: 2.5.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**streamtype**
 | 
				
			||||||
 | 
					    Allows storing images without quantization and Huffman tables, or with
 | 
				
			||||||
 | 
					    these tables but without image data.  This is useful for container formats
 | 
				
			||||||
 | 
					    or network protocols that handle tables separately and share them between
 | 
				
			||||||
 | 
					    images.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * ``0`` (default): interchange datastream, with tables and image data
 | 
				
			||||||
 | 
					    * ``1``: abbreviated table specification (tables-only) datastream
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .. versionadded:: 10.2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    * ``2``: abbreviated image (image-only) datastream
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**comment**
 | 
					**comment**
 | 
				
			||||||
    A comment about the image.
 | 
					    A comment about the image.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1291,6 +1325,8 @@ Pillow reads Kodak FlashPix files. In the current version, only the highest
 | 
				
			||||||
resolution image is read from the file, and the viewing transform is not taken
 | 
					resolution image is read from the file, and the viewing transform is not taken
 | 
				
			||||||
into account.
 | 
					into account.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To enable FPX support, you must install :pypi:`olefile`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. note::
 | 
					.. note::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    To enable full FlashPix support, you need to build and install the IJG JPEG
 | 
					    To enable full FlashPix support, you need to build and install the IJG JPEG
 | 
				
			||||||
| 
						 | 
					@ -1367,6 +1403,8 @@ the first sprite in the file is loaded. You can use :py:meth:`~PIL.Image.Image.s
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Note that there may be an embedded gamma of 2.2 in MIC files.
 | 
					Note that there may be an embedded gamma of 2.2 in MIC files.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To enable MIC support, you must install :pypi:`olefile`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
MPO
 | 
					MPO
 | 
				
			||||||
^^^
 | 
					^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,7 @@ image. If the image was not read from a file, it is set to None. The size
 | 
				
			||||||
attribute is a 2-tuple containing width and height (in pixels). The
 | 
					attribute is a 2-tuple containing width and height (in pixels). The
 | 
				
			||||||
:py:attr:`~PIL.Image.Image.mode` attribute defines the number and names of the
 | 
					:py:attr:`~PIL.Image.Image.mode` attribute defines the number and names of the
 | 
				
			||||||
bands in the image, and also the pixel type and depth. Common modes are “L”
 | 
					bands in the image, and also the pixel type and depth. Common modes are “L”
 | 
				
			||||||
(luminance) for greyscale images, “RGB” for true color images, and “CMYK” for
 | 
					(luminance) for grayscale images, “RGB” for true color images, and “CMYK” for
 | 
				
			||||||
pre-press images.
 | 
					pre-press images.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
If the file cannot be opened, an :py:exc:`OSError` exception is raised.
 | 
					If the file cannot be opened, an :py:exc:`OSError` exception is raised.
 | 
				
			||||||
| 
						 | 
					@ -268,6 +268,37 @@ true, to provide for the same changes to the image's size.
 | 
				
			||||||
A more general form of image transformations can be carried out via the
 | 
					A more general form of image transformations can be carried out via the
 | 
				
			||||||
:py:meth:`~PIL.Image.Image.transform` method.
 | 
					:py:meth:`~PIL.Image.Image.transform` method.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Relative resizing
 | 
				
			||||||
 | 
					^^^^^^^^^^^^^^^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Instead of calculating the size of the new image when resizing, you can also
 | 
				
			||||||
 | 
					choose to resize relative to a given size.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from PIL import Image, ImageOps
 | 
				
			||||||
 | 
					    size = (100, 150)
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/hopper.png") as im:
 | 
				
			||||||
 | 
					        ImageOps.contain(im, size).save("imageops_contain.png")
 | 
				
			||||||
 | 
					        ImageOps.cover(im, size).save("imageops_cover.png")
 | 
				
			||||||
 | 
					        ImageOps.fit(im, size).save("imageops_fit.png")
 | 
				
			||||||
 | 
					        ImageOps.pad(im, size, color="#f00").save("imageops_pad.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # thumbnail() can also be used,
 | 
				
			||||||
 | 
					        # but will modify the image object in place
 | 
				
			||||||
 | 
					        im.thumbnail(size)
 | 
				
			||||||
 | 
					        im.save("imageops_thumbnail.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
 | 
				
			||||||
 | 
					|                | :py:meth:`~PIL.Image.Image.thumbnail`     | :py:meth:`~PIL.ImageOps.contain`           | :py:meth:`~PIL.ImageOps.cover`           | :py:meth:`~PIL.ImageOps.fit`           | :py:meth:`~PIL.ImageOps.pad`           |
 | 
				
			||||||
 | 
					+================+===========================================+============================================+==========================================+========================================+========================================+
 | 
				
			||||||
 | 
					|Given size      | ``(100, 150)``                            | ``(100, 150)``                             | ``(100, 150)``                           | ``(100, 150)``                         | ``(100, 150)``                         |
 | 
				
			||||||
 | 
					+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
 | 
				
			||||||
 | 
					|Resulting image | .. image:: ../example/image_thumbnail.png | .. image:: ../example/imageops_contain.png | .. image:: ../example/imageops_cover.png | .. image:: ../example/imageops_fit.png | .. image:: ../example/imageops_pad.png |
 | 
				
			||||||
 | 
					+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
 | 
				
			||||||
 | 
					|Resulting size  | ``100×100``                               | ``100×100``                                | ``150×150``                              | ``100×150``                            | ``100×150``                            |
 | 
				
			||||||
 | 
					+----------------+-------------------------------------------+--------------------------------------------+------------------------------------------+----------------------------------------+----------------------------------------+
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. _color-transforms:
 | 
					.. _color-transforms:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Color transforms
 | 
					Color transforms
 | 
				
			||||||
| 
						 | 
					@ -568,7 +599,7 @@ Controlling the decoder
 | 
				
			||||||
Some decoders allow you to manipulate the image while reading it from a file.
 | 
					Some decoders allow you to manipulate the image while reading it from a file.
 | 
				
			||||||
This can often be used to speed up decoding when creating thumbnails (when
 | 
					This can often be used to speed up decoding when creating thumbnails (when
 | 
				
			||||||
speed is usually more important than quality) and printing to a monochrome
 | 
					speed is usually more important than quality) and printing to a monochrome
 | 
				
			||||||
laser printer (when only a greyscale version of the image is needed).
 | 
					laser printer (when only a grayscale version of the image is needed).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The :py:meth:`~PIL.Image.Image.draft` method manipulates an opened but not yet
 | 
					The :py:meth:`~PIL.Image.Image.draft` method manipulates an opened but not yet
 | 
				
			||||||
loaded image so it as closely as possible matches the given mode and size. This
 | 
					loaded image so it as closely as possible matches the given mode and size. This
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,14 +26,14 @@ Pillow decodes files in two stages:
 | 
				
			||||||
   it.
 | 
					   it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
An image plugin should contain a format handler derived from the
 | 
					An image plugin should contain a format handler derived from the
 | 
				
			||||||
:py:class:`PIL.ImageFile.ImageFile` base class. This class should
 | 
					:py:class:`PIL.ImageFile.ImageFile` base class. This class should provide an
 | 
				
			||||||
provide an ``_open`` method, which reads the file header and
 | 
					``_open`` method, which reads the file header and set at least the internal
 | 
				
			||||||
sets up at least the :py:attr:`~PIL.Image.Image.mode` and
 | 
					``_size`` and ``_mode`` attributes so that :py:attr:`~PIL.Image.Image.mode` and
 | 
				
			||||||
:py:attr:`~PIL.Image.Image.size` attributes. To be able to load the
 | 
					:py:attr:`~PIL.Image.Image.size` are populated. To be able to load the file,
 | 
				
			||||||
file, the method must also create a list of ``tile`` descriptors,
 | 
					the method must also create a list of ``tile`` descriptors, which contain a
 | 
				
			||||||
which contain a decoder name, extents of the tile, and
 | 
					decoder name, extents of the tile, and any decoder-specific data. The format
 | 
				
			||||||
any decoder-specific data. The format handler class must be explicitly
 | 
					handler class must be explicitly registered, via a call to the
 | 
				
			||||||
registered, via a call to the :py:mod:`~PIL.Image` module.
 | 
					:py:mod:`~PIL.Image` module.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. note:: For performance reasons, it is important that the
 | 
					.. note:: For performance reasons, it is important that the
 | 
				
			||||||
  ``_open`` method quickly rejects files that do not have the
 | 
					  ``_open`` method quickly rejects files that do not have the
 | 
				
			||||||
| 
						 | 
					@ -45,7 +45,7 @@ Example
 | 
				
			||||||
The following plugin supports a simple format, which has a 128-byte header
 | 
					The following plugin supports a simple format, which has a 128-byte header
 | 
				
			||||||
consisting of the words “SPAM” followed by the width, height, and pixel size in
 | 
					consisting of the words “SPAM” followed by the width, height, and pixel size in
 | 
				
			||||||
bits. The header fields are separated by spaces. The image data follows
 | 
					bits. The header fields are separated by spaces. The image data follows
 | 
				
			||||||
directly after the header, and can be either bi-level, greyscale, or 24-bit
 | 
					directly after the header, and can be either bi-level, grayscale, or 24-bit
 | 
				
			||||||
true color.
 | 
					true color.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**SpamImagePlugin.py**::
 | 
					**SpamImagePlugin.py**::
 | 
				
			||||||
| 
						 | 
					@ -96,13 +96,13 @@ true color.
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The format handler must always set the
 | 
					The format handler must always set the internal ``_size`` and ``_mode``
 | 
				
			||||||
:py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.Image.Image.mode`
 | 
					attributes so that :py:attr:`~PIL.Image.Image.size` and
 | 
				
			||||||
attributes. If these are not set, the file cannot be opened. To
 | 
					:py:attr:`~PIL.Image.Image.mode` are populated. If these are not set, the file
 | 
				
			||||||
simplify the plugin, the calling code considers exceptions like
 | 
					cannot be opened. To simplify the plugin, the calling code considers exceptions
 | 
				
			||||||
:py:exc:`SyntaxError`, :py:exc:`KeyError`, :py:exc:`IndexError`,
 | 
					like :py:exc:`SyntaxError`, :py:exc:`KeyError`, :py:exc:`IndexError`,
 | 
				
			||||||
:py:exc:`EOFError` and :py:exc:`struct.error` as a failure to identify
 | 
					:py:exc:`EOFError` and :py:exc:`struct.error` as a failure to identify the
 | 
				
			||||||
the file.
 | 
					file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Note that the image plugin must be explicitly registered using
 | 
					Note that the image plugin must be explicitly registered using
 | 
				
			||||||
:py:func:`PIL.Image.register_open`. Although not required, it is also a good
 | 
					:py:func:`PIL.Image.register_open`. Although not required, it is also a good
 | 
				
			||||||
| 
						 | 
					@ -211,9 +211,9 @@ table describes some commonly used **raw modes**:
 | 
				
			||||||
| ``1;R``   | | 1-bit reversed bilevel, stored with the leftmost pixel in the   |
 | 
					| ``1;R``   | | 1-bit reversed bilevel, stored with the leftmost pixel in the   |
 | 
				
			||||||
|           | | least significant bit. 0 means black, 1 means white.            |
 | 
					|           | | least significant bit. 0 means black, 1 means white.            |
 | 
				
			||||||
+-----------+-------------------------------------------------------------------+
 | 
					+-----------+-------------------------------------------------------------------+
 | 
				
			||||||
| ``L``     | 8-bit greyscale. 0 means black, 255 means white.                  |
 | 
					| ``L``     | 8-bit grayscale. 0 means black, 255 means white.                  |
 | 
				
			||||||
+-----------+-------------------------------------------------------------------+
 | 
					+-----------+-------------------------------------------------------------------+
 | 
				
			||||||
| ``L;I``   | 8-bit inverted greyscale. 0 means white, 255 means black.         |
 | 
					| ``L;I``   | 8-bit inverted grayscale. 0 means white, 255 means black.         |
 | 
				
			||||||
+-----------+-------------------------------------------------------------------+
 | 
					+-----------+-------------------------------------------------------------------+
 | 
				
			||||||
| ``P``     | 8-bit palette-mapped image.                                       |
 | 
					| ``P``     | 8-bit palette-mapped image.                                       |
 | 
				
			||||||
+-----------+-------------------------------------------------------------------+
 | 
					+-----------+-------------------------------------------------------------------+
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,12 +37,12 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
 | 
				
			||||||
   :target: https://ci.appveyor.com/project/python-pillow/Pillow
 | 
					   :target: https://ci.appveyor.com/project/python-pillow/Pillow
 | 
				
			||||||
   :alt: AppVeyor CI build status (Windows)
 | 
					   :alt: AppVeyor CI build status (Windows)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. image:: https://github.com/python-pillow/pillow-wheels/workflows/Wheels/badge.svg
 | 
					.. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
 | 
				
			||||||
   :target: https://github.com/python-pillow/pillow-wheels/actions
 | 
					   :target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
 | 
				
			||||||
   :alt: GitHub Actions wheels build status (Wheels)
 | 
					   :alt: GitHub Actions build status (Wheels)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. image:: https://img.shields.io/travis/com/python-pillow/pillow-wheels/main.svg?label=aarch64%20wheels
 | 
					.. image:: https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels
 | 
				
			||||||
   :target: https://app.travis-ci.com/github/python-pillow/pillow-wheels
 | 
					   :target: https://app.travis-ci.com/github/python-pillow/Pillow
 | 
				
			||||||
   :alt: Travis CI wheels build status (aarch64)
 | 
					   :alt: Travis CI wheels build status (aarch64)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg
 | 
					.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg
 | 
				
			||||||
| 
						 | 
					@ -69,8 +69,8 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
 | 
				
			||||||
   :target: https://pypi.org/project/Pillow/
 | 
					   :target: https://pypi.org/project/Pillow/
 | 
				
			||||||
   :alt: Number of PyPI downloads
 | 
					   :alt: Number of PyPI downloads
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. image:: https://bestpractices.coreinfrastructure.org/projects/6331/badge
 | 
					.. image:: https://www.bestpractices.dev/projects/6331/badge
 | 
				
			||||||
   :target: https://bestpractices.coreinfrastructure.org/projects/6331
 | 
					   :target: https://www.bestpractices.dev/projects/6331
 | 
				
			||||||
   :alt: OpenSSF Best Practices
 | 
					   :alt: OpenSSF Best Practices
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. image:: https://badges.gitter.im/python-pillow/Pillow.svg
 | 
					.. image:: https://badges.gitter.im/python-pillow/Pillow.svg
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,14 @@
 | 
				
			||||||
Installation
 | 
					Installation
 | 
				
			||||||
============
 | 
					============
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. raw:: html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script>
 | 
				
			||||||
 | 
					    document.addEventListener('DOMContentLoaded', function() {
 | 
				
			||||||
 | 
					      activateTab(getOS());
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    </script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Warnings
 | 
					Warnings
 | 
				
			||||||
--------
 | 
					--------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,6 +50,11 @@ Install Pillow with :command:`pip`::
 | 
				
			||||||
    python3 -m pip install --upgrade pip
 | 
					    python3 -m pip install --upgrade pip
 | 
				
			||||||
    python3 -m pip install --upgrade Pillow
 | 
					    python3 -m pip install --upgrade Pillow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Optionally, install :pypi:`defusedxml` for Pillow to read XMP data,
 | 
				
			||||||
 | 
					and :pypi:`olefile` for Pillow to read FPX and MIC images::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    python3 -m pip install --upgrade defusedxml olefile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. tab:: Linux
 | 
					.. tab:: Linux
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,9 +95,10 @@ Install Pillow with :command:`pip`::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. tab:: Windows
 | 
					.. tab:: Windows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    We provide Pillow binaries for Windows compiled for the matrix of
 | 
					    We provide Pillow binaries for Windows compiled for the matrix of supported
 | 
				
			||||||
    supported Pythons in 64-bit versions in the wheel format. These binaries include
 | 
					    Pythons in the wheel format. These include x86, x86-64 and arm64 versions
 | 
				
			||||||
    support for all optional libraries except libimagequant and libxcb. Raqm support
 | 
					    (with the exception of Python 3.8 on arm64). These binaries include support
 | 
				
			||||||
 | 
					    for all optional libraries except libimagequant and libxcb. Raqm support
 | 
				
			||||||
    requires FriBiDi to be installed separately::
 | 
					    requires FriBiDi to be installed separately::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        python3 -m pip install --upgrade pip
 | 
					        python3 -m pip install --upgrade pip
 | 
				
			||||||
| 
						 | 
					@ -144,24 +158,24 @@ Many of Pillow's features require external libraries:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and
 | 
					  * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9d** and
 | 
				
			||||||
    libjpeg-turbo version **8**.
 | 
					    libjpeg-turbo version **8**.
 | 
				
			||||||
  * Starting with Pillow 3.0.0, libjpeg is required by default, but
 | 
					  * Starting with Pillow 3.0.0, libjpeg is required by default. It can be
 | 
				
			||||||
    may be disabled with the ``--disable-jpeg`` flag.
 | 
					    disabled with the ``-C jpeg=disable`` flag.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* **zlib** provides access to compressed PNGs
 | 
					* **zlib** provides access to compressed PNGs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  * Starting with Pillow 3.0.0, zlib is required by default, but may
 | 
					  * Starting with Pillow 3.0.0, zlib is required by default. It can be
 | 
				
			||||||
    be disabled with the ``--disable-zlib`` flag.
 | 
					    disabled with the ``-C zlib=disable`` flag.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* **libtiff** provides compressed TIFF functionality
 | 
					* **libtiff** provides compressed TIFF functionality
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5.1**
 | 
					  * Pillow has been tested with libtiff versions **3.x** and **4.0-4.6.0**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* **libfreetype** provides type related services
 | 
					* **libfreetype** provides type related services
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* **littlecms** provides color management
 | 
					* **littlecms** provides color management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
 | 
					  * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
 | 
				
			||||||
    above uses liblcms2. Tested with **1.19** and **2.7-2.15**.
 | 
					    above uses liblcms2. Tested with **1.19** and **2.7-2.16**.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* **libwebp** provides the WebP format.
 | 
					* **libwebp** provides the WebP format.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -169,8 +183,6 @@ Many of Pillow's features require external libraries:
 | 
				
			||||||
    transparent WebP files. Versions **0.3.0** and above support
 | 
					    transparent WebP files. Versions **0.3.0** and above support
 | 
				
			||||||
    transparency.
 | 
					    transparency.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* **tcl/tk** provides support for tkinter bitmap and photo images.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* **openjpeg** provides JPEG 2000 functionality.
 | 
					* **openjpeg** provides JPEG 2000 functionality.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
 | 
					  * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**,
 | 
				
			||||||
| 
						 | 
					@ -180,7 +192,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.2.1**
 | 
					  * Pillow has been tested with libimagequant **2.6-4.2.2**
 | 
				
			||||||
  * 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.
 | 
				
			||||||
| 
						 | 
					@ -354,7 +366,7 @@ for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no
 | 
				
			||||||
additional configuration should be required. If they are installed in
 | 
					additional configuration should be required. If they are installed in
 | 
				
			||||||
a non-standard location, you may need to configure setuptools to use
 | 
					a non-standard location, you may need to configure setuptools to use
 | 
				
			||||||
those locations by editing :file:`setup.py` or
 | 
					those locations by editing :file:`setup.py` or
 | 
				
			||||||
:file:`setup.cfg`, or by adding environment variables on the command
 | 
					:file:`pyproject.toml`, or by adding environment variables on the command
 | 
				
			||||||
line::
 | 
					line::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
 | 
					    CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all:
 | 
				
			||||||
| 
						 | 
					@ -454,10 +466,10 @@ These platforms are built and tested for every change.
 | 
				
			||||||
+----------------------------------+----------------------------+---------------------+
 | 
					+----------------------------------+----------------------------+---------------------+
 | 
				
			||||||
| Debian 12 Bookworm               | 3.11                       | x86, x86-64         |
 | 
					| Debian 12 Bookworm               | 3.11                       | x86, x86-64         |
 | 
				
			||||||
+----------------------------------+----------------------------+---------------------+
 | 
					+----------------------------------+----------------------------+---------------------+
 | 
				
			||||||
| Fedora 37                        | 3.11                       | x86-64              |
 | 
					 | 
				
			||||||
+----------------------------------+----------------------------+---------------------+
 | 
					 | 
				
			||||||
| Fedora 38                        | 3.11                       | x86-64              |
 | 
					| Fedora 38                        | 3.11                       | x86-64              |
 | 
				
			||||||
+----------------------------------+----------------------------+---------------------+
 | 
					+----------------------------------+----------------------------+---------------------+
 | 
				
			||||||
 | 
					| Fedora 39                        | 3.12                       | x86-64              |
 | 
				
			||||||
 | 
					+----------------------------------+----------------------------+---------------------+
 | 
				
			||||||
| Gentoo                           | 3.9                        | x86-64              |
 | 
					| Gentoo                           | 3.9                        | x86-64              |
 | 
				
			||||||
+----------------------------------+----------------------------+---------------------+
 | 
					+----------------------------------+----------------------------+---------------------+
 | 
				
			||||||
| macOS 12 Monterey                | 3.8, 3.9, 3.10, 3.11,      | x86-64              |
 | 
					| macOS 12 Monterey                | 3.8, 3.9, 3.10, 3.11,      | x86-64              |
 | 
				
			||||||
| 
						 | 
					@ -476,7 +488,7 @@ These platforms are built and tested for every change.
 | 
				
			||||||
| Windows Server 2022              | 3.8, 3.9, 3.10, 3.11,      | x86-64              |
 | 
					| Windows Server 2022              | 3.8, 3.9, 3.10, 3.11,      | x86-64              |
 | 
				
			||||||
|                                  | 3.12, PyPy3                |                     |
 | 
					|                                  | 3.12, PyPy3                |                     |
 | 
				
			||||||
|                                  +----------------------------+---------------------+
 | 
					|                                  +----------------------------+---------------------+
 | 
				
			||||||
|                                  | 3.11                       | x86                 |
 | 
					|                                  | 3.12                       | x86                 |
 | 
				
			||||||
|                                  +----------------------------+---------------------+
 | 
					|                                  +----------------------------+---------------------+
 | 
				
			||||||
|                                  | 3.9 (MinGW)                | x86-64              |
 | 
					|                                  | 3.9 (MinGW)                | x86-64              |
 | 
				
			||||||
|                                  +----------------------------+---------------------+
 | 
					|                                  +----------------------------+---------------------+
 | 
				
			||||||
| 
						 | 
					@ -494,93 +506,97 @@ These platforms have been reported to work at the versions mentioned.
 | 
				
			||||||
    Contributors please test Pillow on your platform then update this
 | 
					    Contributors please test Pillow on your platform then update this
 | 
				
			||||||
    document and send a pull request.
 | 
					    document and send a pull request.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Operating system                 | | Tested Python            | | Latest tested  | | Tested     |
 | 
					| Operating system                 | | Tested Python            | | Latest tested  | | Tested     |
 | 
				
			||||||
|                                  | | versions                 | | Pillow version | | processors |
 | 
					|                                  | | versions                 | | Pillow version | | processors |
 | 
				
			||||||
+==================================+===========================+==================+==============+
 | 
					+==================================+============================+==================+==============+
 | 
				
			||||||
| macOS 14 Sonoma                  | 3.8, 3.9, 3.10, 3.11      | 10.0.1           |arm           |
 | 
					| macOS 14 Sonoma                  | 3.8, 3.9, 3.10, 3.11, 3.12 | 10.1.0           |arm           |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| macOS 13 Ventura                 | 3.8, 3.9, 3.10, 3.11       | 10.0.1           |arm           |
 | 
					| macOS 13 Ventura                 | 3.8, 3.9, 3.10, 3.11       | 10.0.1           |arm           |
 | 
				
			||||||
|                                  +---------------------------+------------------+              |
 | 
					|                                  +----------------------------+------------------+              |
 | 
				
			||||||
|                                  | 3.7                        | 9.5.0            |              |
 | 
					|                                  | 3.7                        | 9.5.0            |              |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| macOS 12 Monterey                | 3.7, 3.8, 3.9, 3.10, 3.11  | 9.3.0            |arm           |
 | 
					| macOS 12 Monterey                | 3.7, 3.8, 3.9, 3.10, 3.11  | 9.3.0            |arm           |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| macOS 11 Big Sur                 | 3.7, 3.8, 3.9, 3.10        | 8.4.0            |arm           |
 | 
					| macOS 11 Big Sur                 | 3.7, 3.8, 3.9, 3.10        | 8.4.0            |arm           |
 | 
				
			||||||
|                                  +---------------------------+------------------+--------------+
 | 
					|                                  +----------------------------+------------------+--------------+
 | 
				
			||||||
|                                  | 3.7, 3.8, 3.9, 3.10, 3.11  | 9.4.0            |x86-64        |
 | 
					|                                  | 3.7, 3.8, 3.9, 3.10, 3.11  | 9.4.0            |x86-64        |
 | 
				
			||||||
|                                  +---------------------------+------------------+              |
 | 
					|                                  +----------------------------+------------------+              |
 | 
				
			||||||
|                                  | 3.6                        | 8.4.0            |              |
 | 
					|                                  | 3.6                        | 8.4.0            |              |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| macOS 10.15 Catalina             | 3.6, 3.7, 3.8, 3.9         | 8.3.2            |x86-64        |
 | 
					| macOS 10.15 Catalina             | 3.6, 3.7, 3.8, 3.9         | 8.3.2            |x86-64        |
 | 
				
			||||||
|                                  +---------------------------+------------------+              |
 | 
					|                                  +----------------------------+------------------+              |
 | 
				
			||||||
|                                  | 3.5                        | 7.2.0            |              |
 | 
					|                                  | 3.5                        | 7.2.0            |              |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| macOS 10.14 Mojave               | 3.5, 3.6, 3.7, 3.8         | 7.2.0            |x86-64        |
 | 
					| macOS 10.14 Mojave               | 3.5, 3.6, 3.7, 3.8         | 7.2.0            |x86-64        |
 | 
				
			||||||
|                                  +---------------------------+------------------+              |
 | 
					|                                  +----------------------------+------------------+              |
 | 
				
			||||||
|                                  | 2.7                        | 6.0.0            |              |
 | 
					|                                  | 2.7                        | 6.0.0            |              |
 | 
				
			||||||
|                                  +---------------------------+------------------+              |
 | 
					|                                  +----------------------------+------------------+              |
 | 
				
			||||||
|                                  | 3.4                        | 5.4.1            |              |
 | 
					|                                  | 3.4                        | 5.4.1            |              |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| macOS 10.13 High Sierra          | 2.7, 3.4, 3.5, 3.6         | 4.2.1            |x86-64        |
 | 
					| macOS 10.13 High Sierra          | 2.7, 3.4, 3.5, 3.6         | 4.2.1            |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| macOS 10.12 Sierra               | 2.7, 3.4, 3.5, 3.6         | 4.1.1            |x86-64        |
 | 
					| macOS 10.12 Sierra               | 2.7, 3.4, 3.5, 3.6         | 4.1.1            |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Mac OS X 10.11 El Capitan        | 2.7, 3.4, 3.5, 3.6, 3.7    | 5.4.1            |x86-64        |
 | 
					| Mac OS X 10.11 El Capitan        | 2.7, 3.4, 3.5, 3.6, 3.7    | 5.4.1            |x86-64        |
 | 
				
			||||||
|                                  +---------------------------+------------------+              |
 | 
					|                                  +----------------------------+------------------+              |
 | 
				
			||||||
|                                  | 3.3                        | 4.1.0            |              |
 | 
					|                                  | 3.3                        | 4.1.0            |              |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Mac OS X 10.9 Mavericks          | 2.7, 3.2, 3.3, 3.4         | 3.0.0            |x86-64        |
 | 
					| Mac OS X 10.9 Mavericks          | 2.7, 3.2, 3.3, 3.4         | 3.0.0            |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Mac OS X 10.8 Mountain Lion      | 2.6, 2.7, 3.2, 3.3         |                  |x86-64        |
 | 
					| Mac OS X 10.8 Mountain Lion      | 2.6, 2.7, 3.2, 3.3         |                  |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Redhat Linux 6                   | 2.6                        |                  |x86           |
 | 
					| Redhat Linux 6                   | 2.6                        |                  |x86           |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| CentOS 6.3                       | 2.7, 3.3                   |                  |x86           |
 | 
					| CentOS 6.3                       | 2.7, 3.3                   |                  |x86           |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| CentOS 8                         | 3.9                        | 9.0.0            |x86-64        |
 | 
					| CentOS 8                         | 3.9                        | 9.0.0            |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Fedora 23                        | 2.7, 3.4                   | 3.1.0            |x86-64        |
 | 
					| Fedora 23                        | 2.7, 3.4                   | 3.1.0            |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5  | 3.4.1            |x86,x86-64    |
 | 
					| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5  | 3.4.1            |x86,x86-64    |
 | 
				
			||||||
|                                  | | PyPy5.3.1, PyPy3 v2.4.0  |                  |              |
 | 
					|                                  | | PyPy5.3.1, PyPy3 v2.4.0  |                  |              |
 | 
				
			||||||
|                                  +---------------------------+------------------+--------------+
 | 
					|                                  +----------------------------+------------------+--------------+
 | 
				
			||||||
|                                  | 2.7                        | 4.3.0            |x86-64        |
 | 
					|                                  | 2.7                        | 4.3.0            |x86-64        |
 | 
				
			||||||
|                                  +---------------------------+------------------+--------------+
 | 
					|                                  +----------------------------+------------------+--------------+
 | 
				
			||||||
|                                  | 2.7, 3.2                   | 3.4.1            |ppc           |
 | 
					|                                  | 2.7, 3.2                   | 3.4.1            |ppc           |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Ubuntu Linux 10.04 LTS (Lucid)   | 2.6                        | 2.3.0            |x86,x86-64    |
 | 
					| Ubuntu Linux 10.04 LTS (Lucid)   | 2.6                        | 2.3.0            |x86,x86-64    |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Debian 8.2 Jessie                | 2.7, 3.4                   | 3.1.0            |x86-64        |
 | 
					| Debian 8.2 Jessie                | 2.7, 3.4                   | 3.1.0            |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Raspbian Jessie                  | 2.7, 3.4                   | 3.1.0            |arm           |
 | 
					| Raspbian Jessie                  | 2.7, 3.4                   | 3.1.0            |arm           |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Raspbian Stretch                 | 2.7, 3.5                   | 4.0.0            |arm           |
 | 
					| Raspbian Stretch                 | 2.7, 3.5                   | 4.0.0            |arm           |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Raspberry Pi OS                  | 3.6, 3.7, 3.8, 3.9         | 8.2.0            |arm           |
 | 
					| Raspberry Pi OS                  | 3.6, 3.7, 3.8, 3.9         | 8.2.0            |arm           |
 | 
				
			||||||
|                                  +---------------------------+------------------+              |
 | 
					|                                  +----------------------------+------------------+              |
 | 
				
			||||||
|                                  | 2.7                        | 6.2.2            |              |
 | 
					|                                  | 2.7                        | 6.2.2            |              |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Gentoo Linux                     | 2.7, 3.2                   | 2.1.0            |x86-64        |
 | 
					| Gentoo Linux                     | 2.7, 3.2                   | 2.1.0            |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| FreeBSD 11.1                     | 2.7, 3.4, 3.5, 3.6         | 4.3.0            |x86-64        |
 | 
					| FreeBSD 11.1                     | 2.7, 3.4, 3.5, 3.6         | 4.3.0            |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| FreeBSD 10.3                     | 2.7, 3.4, 3.5              | 4.2.0            |x86-64        |
 | 
					| FreeBSD 10.3                     | 2.7, 3.4, 3.5              | 4.2.0            |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| FreeBSD 10.2                     | 2.7, 3.4                   | 3.1.0            |x86-64        |
 | 
					| FreeBSD 10.2                     | 2.7, 3.4                   | 3.1.0            |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
 | 
					| Windows 11                       | 3.9, 3.10, 3.11, 3.12      | 10.1.0           |arm64         |
 | 
				
			||||||
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
 | 
					| Windows 11 Pro                   | 3.11, 3.12                 | 10.1.0           |x86-64        |
 | 
				
			||||||
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Windows 10                       | 3.7                        | 7.1.0            |x86-64        |
 | 
					| Windows 10                       | 3.7                        | 7.1.0            |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Windows 10/Cygwin 3.3            | 3.6, 3.7, 3.8, 3.9         | 8.4.0            |x86-64        |
 | 
					| Windows 10/Cygwin 3.3            | 3.6, 3.7, 3.8, 3.9         | 8.4.0            |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Windows 8.1 Pro                  | 2.6, 2.7, 3.2, 3.3, 3.4    | 2.4.0            |x86,x86-64    |
 | 
					| Windows 8.1 Pro                  | 2.6, 2.7, 3.2, 3.3, 3.4    | 2.4.0            |x86,x86-64    |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Windows 8 Pro                    | 2.6, 2.7, 3.2, 3.3, 3.4a3  | 2.2.0            |x86,x86-64    |
 | 
					| Windows 8 Pro                    | 2.6, 2.7, 3.2, 3.3, 3.4a3  | 2.2.0            |x86,x86-64    |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Windows 7 Professional           | 3.7                        | 7.0.0            |x86,x86-64    |
 | 
					| Windows 7 Professional           | 3.7                        | 7.0.0            |x86,x86-64    |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
| Windows Server 2008 R2 Enterprise| 3.3                        |                  |x86-64        |
 | 
					| Windows Server 2008 R2 Enterprise| 3.3                        |                  |x86-64        |
 | 
				
			||||||
+----------------------------------+---------------------------+------------------+--------------+
 | 
					+----------------------------------+----------------------------+------------------+--------------+
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Old Versions
 | 
					Old Versions
 | 
				
			||||||
------------
 | 
					------------
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,7 +59,7 @@ Functions
 | 
				
			||||||
.. py:method:: getcolor(color, mode)
 | 
					.. py:method:: getcolor(color, mode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a
 | 
					    Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a
 | 
				
			||||||
    greyscale value if the mode is not color or a palette image. If the string
 | 
					    grayscale value if the mode is not color or a palette image. If the string
 | 
				
			||||||
    cannot be parsed, this function raises a :py:exc:`ValueError` exception.
 | 
					    cannot be parsed, this function raises a :py:exc:`ValueError` exception.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .. versionadded:: 1.1.4
 | 
					    .. versionadded:: 1.1.4
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -351,7 +351,7 @@ Methods
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Draw a shape.
 | 
					    Draw a shape.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False)
 | 
					.. py:method:: ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, font_size=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Draws the string at the given position.
 | 
					    Draws the string at the given position.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -362,9 +362,10 @@ Methods
 | 
				
			||||||
    :param fill: Color to use for the text.
 | 
					    :param fill: Color to use for the text.
 | 
				
			||||||
    :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
 | 
					    :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
 | 
				
			||||||
    :param anchor: The text anchor alignment. Determines the relative location of
 | 
					    :param anchor: The text anchor alignment. Determines the relative location of
 | 
				
			||||||
                   the anchor to the text. The default alignment is top left.
 | 
					                   the anchor to the text. The default alignment is top left,
 | 
				
			||||||
                   See :ref:`text-anchors` for valid values. This parameter is
 | 
					                   specifically ``la`` for horizontal text and ``lt`` for
 | 
				
			||||||
                   ignored for non-TrueType fonts.
 | 
					                   vertical text. See :ref:`text-anchors` for details.
 | 
				
			||||||
 | 
					                   This parameter is ignored for non-TrueType fonts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    .. note:: This parameter was present in earlier versions
 | 
					                    .. note:: This parameter was present in earlier versions
 | 
				
			||||||
                              of Pillow, but implemented only in version 8.0.0.
 | 
					                              of Pillow, but implemented only in version 8.0.0.
 | 
				
			||||||
| 
						 | 
					@ -416,8 +417,14 @@ Methods
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    .. versionadded:: 8.0.0
 | 
					                    .. versionadded:: 8.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param font_size: If ``font`` is not provided, then the size to use for the default
 | 
				
			||||||
 | 
					                      font.
 | 
				
			||||||
 | 
					                      Keyword-only argument.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False)
 | 
					                    .. versionadded:: 10.1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. py:method:: ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None, embedded_color=False, font_size=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Draws the string at the given position.
 | 
					    Draws the string at the given position.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -427,9 +434,10 @@ Methods
 | 
				
			||||||
    :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
 | 
					    :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param anchor: The text anchor alignment. Determines the relative location of
 | 
					    :param anchor: The text anchor alignment. Determines the relative location of
 | 
				
			||||||
                   the anchor to the text. The default alignment is top left.
 | 
					                   the anchor to the text. The default alignment is top left,
 | 
				
			||||||
                   See :ref:`text-anchors` for valid values. This parameter is
 | 
					                   specifically ``la`` for horizontal text and ``lt`` for
 | 
				
			||||||
                   ignored for non-TrueType fonts.
 | 
					                   vertical text. See :ref:`text-anchors` for details.
 | 
				
			||||||
 | 
					                   This parameter is ignored for non-TrueType fonts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    .. note:: This parameter was present in earlier versions
 | 
					                    .. note:: This parameter was present in earlier versions
 | 
				
			||||||
                              of Pillow, but implemented only in version 8.0.0.
 | 
					                              of Pillow, but implemented only in version 8.0.0.
 | 
				
			||||||
| 
						 | 
					@ -477,7 +485,13 @@ Methods
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                     .. versionadded:: 8.0.0
 | 
					                     .. versionadded:: 8.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False)
 | 
					    :param font_size: If ``font`` is not provided, then the size to use for the default
 | 
				
			||||||
 | 
					                      font.
 | 
				
			||||||
 | 
					                      Keyword-only argument.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    .. versionadded:: 10.1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False, font_size=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Returns length (in pixels with 1/64 precision) of given text when rendered
 | 
					    Returns length (in pixels with 1/64 precision) of given text when rendered
 | 
				
			||||||
    in font with provided direction, features, and language.
 | 
					    in font with provided direction, features, and language.
 | 
				
			||||||
| 
						 | 
					@ -538,9 +552,15 @@ Methods
 | 
				
			||||||
                     It should be a `BCP 47 language code`_.
 | 
					                     It should be a `BCP 47 language code`_.
 | 
				
			||||||
                     Requires libraqm.
 | 
					                     Requires libraqm.
 | 
				
			||||||
    :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
 | 
					    :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
 | 
				
			||||||
 | 
					    :param font_size: If ``font`` is not provided, then the size to use for the default
 | 
				
			||||||
 | 
					                      font.
 | 
				
			||||||
 | 
					                      Keyword-only argument.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    .. versionadded:: 10.1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :return: Either width for horizontal text, or height for vertical text.
 | 
					    :return: Either width for horizontal text, or height for vertical text.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
 | 
					.. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False, font_size=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Returns bounding box (in pixels) of given text relative to given anchor
 | 
					    Returns bounding box (in pixels) of given text relative to given anchor
 | 
				
			||||||
    when rendered in font with provided direction, features, and language.
 | 
					    when rendered in font with provided direction, features, and language.
 | 
				
			||||||
| 
						 | 
					@ -558,9 +578,10 @@ Methods
 | 
				
			||||||
                 :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`.
 | 
					                 :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`.
 | 
				
			||||||
    :param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance.
 | 
					    :param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance.
 | 
				
			||||||
    :param anchor: The text anchor alignment. Determines the relative location of
 | 
					    :param anchor: The text anchor alignment. Determines the relative location of
 | 
				
			||||||
                   the anchor to the text. The default alignment is top left.
 | 
					                   the anchor to the text. The default alignment is top left,
 | 
				
			||||||
                   See :ref:`text-anchors` for valid values. This parameter is
 | 
					                   specifically ``la`` for horizontal text and ``lt`` for
 | 
				
			||||||
                   ignored for non-TrueType fonts.
 | 
					                   vertical text. See :ref:`text-anchors` for details.
 | 
				
			||||||
 | 
					                   This parameter is ignored for non-TrueType fonts.
 | 
				
			||||||
    :param spacing: If the text is passed on to
 | 
					    :param spacing: If the text is passed on to
 | 
				
			||||||
                    :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`,
 | 
					                    :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`,
 | 
				
			||||||
                    the number of pixels between lines.
 | 
					                    the number of pixels between lines.
 | 
				
			||||||
| 
						 | 
					@ -588,9 +609,15 @@ Methods
 | 
				
			||||||
                     Requires libraqm.
 | 
					                     Requires libraqm.
 | 
				
			||||||
    :param stroke_width: The width of the text stroke.
 | 
					    :param stroke_width: The width of the text stroke.
 | 
				
			||||||
    :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
 | 
					    :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
 | 
				
			||||||
 | 
					    :param font_size: If ``font`` is not provided, then the size to use for the default
 | 
				
			||||||
 | 
					                      font.
 | 
				
			||||||
 | 
					                      Keyword-only argument.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    .. versionadded:: 10.1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :return: ``(left, top, right, bottom)`` bounding box
 | 
					    :return: ``(left, top, right, bottom)`` bounding box
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False)
 | 
					.. py:method:: ImageDraw.multiline_textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False, font_size=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Returns bounding box (in pixels) of given text relative to given anchor
 | 
					    Returns bounding box (in pixels) of given text relative to given anchor
 | 
				
			||||||
    when rendered in font with provided direction, features, and language.
 | 
					    when rendered in font with provided direction, features, and language.
 | 
				
			||||||
| 
						 | 
					@ -606,9 +633,10 @@ Methods
 | 
				
			||||||
    :param text: Text to be measured.
 | 
					    :param text: Text to be measured.
 | 
				
			||||||
    :param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance.
 | 
					    :param font: A :py:class:`~PIL.ImageFont.FreeTypeFont` instance.
 | 
				
			||||||
    :param anchor: The text anchor alignment. Determines the relative location of
 | 
					    :param anchor: The text anchor alignment. Determines the relative location of
 | 
				
			||||||
                   the anchor to the text. The default alignment is top left.
 | 
					                   the anchor to the text. The default alignment is top left,
 | 
				
			||||||
                   See :ref:`text-anchors` for valid values. This parameter is
 | 
					                   specifically ``la`` for horizontal text and ``lt`` for
 | 
				
			||||||
                   ignored for non-TrueType fonts.
 | 
					                   vertical text. See :ref:`text-anchors` for details.
 | 
				
			||||||
 | 
					                   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"`` or ``"right"``. Determines the relative alignment of lines.
 | 
				
			||||||
                  Use the ``anchor`` parameter to specify the alignment to ``xy``.
 | 
					                  Use the ``anchor`` parameter to specify the alignment to ``xy``.
 | 
				
			||||||
| 
						 | 
					@ -632,6 +660,12 @@ Methods
 | 
				
			||||||
                     Requires libraqm.
 | 
					                     Requires libraqm.
 | 
				
			||||||
    :param stroke_width: The width of the text stroke.
 | 
					    :param stroke_width: The width of the text stroke.
 | 
				
			||||||
    :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
 | 
					    :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX).
 | 
				
			||||||
 | 
					    :param font_size: If ``font`` is not provided, then the size to use for the default
 | 
				
			||||||
 | 
					                      font.
 | 
				
			||||||
 | 
					                      Keyword-only argument.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    .. versionadded:: 10.1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :return: ``(left, top, right, bottom)`` bounding box
 | 
					    :return: ``(left, top, right, bottom)`` bounding box
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. py:method:: getdraw(im=None, hints=None)
 | 
					.. py:method:: getdraw(im=None, hints=None)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||