Merge branch 'main' into plainPPM
| 
						 | 
					@ -25,8 +25,8 @@ install:
 | 
				
			||||||
- mv c:\pillow-depends-main c:\pillow-depends
 | 
					- mv c:\pillow-depends-main c:\pillow-depends
 | 
				
			||||||
- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
 | 
					- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images
 | 
				
			||||||
- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
 | 
					- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\
 | 
				
			||||||
- ..\pillow-depends\gs9550w32.exe /S
 | 
					- ..\pillow-depends\gs9561w32.exe /S
 | 
				
			||||||
- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.55.0\bin;%PATH%
 | 
					- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH%
 | 
				
			||||||
- cd c:\pillow\winbuild\
 | 
					- cd c:\pillow\winbuild\
 | 
				
			||||||
- ps: |
 | 
					- ps: |
 | 
				
			||||||
        c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
 | 
					        c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
 | 
				
			||||||
| 
						 | 
					@ -43,7 +43,7 @@ build_script:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test_script:
 | 
					test_script:
 | 
				
			||||||
- cd c:\pillow
 | 
					- cd c:\pillow
 | 
				
			||||||
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov'
 | 
					- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout'
 | 
				
			||||||
- 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'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
# gather the coverage data
 | 
					# gather the coverage data
 | 
				
			||||||
python3 -m pip install codecov
 | 
					python3 -m pip install codecov
 | 
				
			||||||
if [[ $MATRIX_DOCKER ]]; then
 | 
					if [[ $MATRIX_DOCKER ]]; then
 | 
				
			||||||
  coverage xml --ignore-errors
 | 
					  python3 -m coverage xml --ignore-errors
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
  coverage xml
 | 
					  python3 -m coverage xml
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
set -e
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
coverage erase
 | 
					python3 -m coverage erase
 | 
				
			||||||
if [ $(uname) == "Darwin" ]; then
 | 
					if [ $(uname) == "Darwin" ]; then
 | 
				
			||||||
    export CPPFLAGS="-I/usr/local/miniconda/include";
 | 
					    export CPPFLAGS="-I/usr/local/miniconda/include";
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,13 +13,17 @@ aptget_update()
 | 
				
			||||||
        return 1
 | 
					        return 1
 | 
				
			||||||
    fi
 | 
					    fi
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
aptget_update || aptget_update retry || aptget_update retry
 | 
					if [[ $(uname) != CYGWIN* ]]; then
 | 
				
			||||||
 | 
					    aptget_update || aptget_update retry || aptget_update retry
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
set -e
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
 | 
					if [[ $(uname) != CYGWIN* ]]; then
 | 
				
			||||||
                         ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
 | 
					    sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
 | 
				
			||||||
                         cmake meson imagemagick libharfbuzz-dev libfribidi-dev
 | 
					                             ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
 | 
				
			||||||
 | 
					                             cmake meson imagemagick libharfbuzz-dev libfribidi-dev
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
| 
						 | 
					@ -32,24 +36,28 @@ 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 test-image-results
 | 
					python3 -m pip install test-image-results
 | 
				
			||||||
python3 -m pip install numpy
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# PyQt5 doesn't support PyPy3
 | 
					if [[ $(uname) != CYGWIN* ]]; then
 | 
				
			||||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
 | 
					    # TODO Remove condition when NumPy supports 3.11
 | 
				
			||||||
  # arm64, ppc64le, s390x CPUs:
 | 
					    if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi
 | 
				
			||||||
  # "ERROR: Could not find a version that satisfies the requirement pyqt5"
 | 
					
 | 
				
			||||||
    sudo apt-get -qq install libxcb-xinerama0 pyqt5-dev-tools
 | 
					    # PyQt6 doesn't support PyPy3
 | 
				
			||||||
    python3 -m pip install pyqt5
 | 
					    if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
 | 
				
			||||||
 | 
					        sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxkbcommon-x11-0
 | 
				
			||||||
 | 
					        python3 -m pip install pyqt6
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # webp
 | 
				
			||||||
 | 
					    pushd depends && ./install_webp.sh && popd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # libimagequant
 | 
				
			||||||
 | 
					    pushd depends && ./install_imagequant.sh && popd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # raqm
 | 
				
			||||||
 | 
					    pushd depends && ./install_raqm.sh && popd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # extra test images
 | 
				
			||||||
 | 
					    pushd depends && ./install_extra_test_images.sh && popd
 | 
				
			||||||
 | 
					else
 | 
				
			||||||
 | 
					    cd depends && ./install_extra_test_images.sh && cd ..
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					 | 
				
			||||||
# webp
 | 
					 | 
				
			||||||
pushd depends && ./install_webp.sh && popd
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# libimagequant
 | 
					 | 
				
			||||||
pushd depends && ./install_imagequant.sh && popd
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# raqm
 | 
					 | 
				
			||||||
pushd depends && ./install_raqm.sh && popd
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# extra test images
 | 
					 | 
				
			||||||
pushd depends && ./install_extra_test_images.sh && popd
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,6 @@ trim_trailing_whitespace = true
 | 
				
			||||||
[*.yml]
 | 
					[*.yml]
 | 
				
			||||||
# Two-space indentation
 | 
					# Two-space indentation
 | 
				
			||||||
indent_size = 2
 | 
					indent_size = 2
 | 
				
			||||||
indent_style = space
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Tab indentation (no size specified)
 | 
					# Tab indentation (no size specified)
 | 
				
			||||||
[Makefile]
 | 
					[Makefile]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -4,7 +4,7 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Bug fixes, feature additions, etc.
 | 
					## Bug fixes, feature additions, etc.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil
 | 
					Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [discussions](https://github.com/python-pillow/Pillow/discussions/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Fork the Pillow repository.
 | 
					- Fork the Pillow repository.
 | 
				
			||||||
- Create a branch from `main`.
 | 
					- Create a branch from `main`.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								.github/mergify.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -8,6 +8,7 @@ pull_request_rules:
 | 
				
			||||||
      - status-success=Docker Test Successful
 | 
					      - status-success=Docker Test Successful
 | 
				
			||||||
      - status-success=Windows Test Successful
 | 
					      - status-success=Windows Test Successful
 | 
				
			||||||
      - status-success=MinGW Test Successful
 | 
					      - status-success=MinGW Test Successful
 | 
				
			||||||
 | 
					      - status-success=Cygwin Test Successful
 | 
				
			||||||
      - status-success=continuous-integration/appveyor/pr
 | 
					      - status-success=continuous-integration/appveyor/pr
 | 
				
			||||||
    actions:
 | 
					    actions:
 | 
				
			||||||
      merge:
 | 
					      merge:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										3
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -15,7 +15,8 @@ python3 -m pip install pyroma
 | 
				
			||||||
python3 -m pip install test-image-results
 | 
					python3 -m pip install test-image-results
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
 | 
					echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg
 | 
				
			||||||
python3 -m pip install numpy
 | 
					# TODO Remove condition when NumPy supports 3.11
 | 
				
			||||||
 | 
					if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; 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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										27
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,27 @@
 | 
				
			||||||
 | 
					name: Close stale issues
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  schedule:
 | 
				
			||||||
 | 
					  - cron: "10 0 * * *"
 | 
				
			||||||
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					permissions:
 | 
				
			||||||
 | 
					  issues: write
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  stale:
 | 
				
			||||||
 | 
					    if: github.repository_owner == 'python-pillow'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					    - name: "Check issues"
 | 
				
			||||||
 | 
					      uses: actions/stale@v5
 | 
				
			||||||
 | 
					      with:
 | 
				
			||||||
 | 
					        repo-token: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
 | 
					        only-labels: "Awaiting OP Action"
 | 
				
			||||||
 | 
					        close-issue-message: "Closing this issue as no feedback has been received."
 | 
				
			||||||
 | 
					        days-before-stale: 7
 | 
				
			||||||
 | 
					        days-before-issue-close: 0
 | 
				
			||||||
 | 
					        days-before-pr-close: -1
 | 
				
			||||||
 | 
					        labels-to-remove-when-unstale: "Awaiting OP Action"
 | 
				
			||||||
							
								
								
									
										107
									
								
								.github/workflows/test-cygwin.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,107 @@
 | 
				
			||||||
 | 
					name: Test Cygwin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on: [push, pull_request, workflow_dispatch]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  build:
 | 
				
			||||||
 | 
					    runs-on: windows-latest
 | 
				
			||||||
 | 
					    strategy:
 | 
				
			||||||
 | 
					      fail-fast: false
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        python-minor-version: [7, 8, 9]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    timeout-minutes: 40
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    name: Python 3.${{ matrix.python-minor-version }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: Fix line endings
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          git config --global core.autocrlf input
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Checkout Pillow
 | 
				
			||||||
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Install Cygwin
 | 
				
			||||||
 | 
					        uses: cygwin/cygwin-install-action@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          platform: x86_64
 | 
				
			||||||
 | 
					          packages: >
 | 
				
			||||||
 | 
					            ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel
 | 
				
			||||||
 | 
					            libimagequant-devel libjpeg-devel liblapack-devel
 | 
				
			||||||
 | 
					            liblcms2-devel libopenjp2-devel libraqm-devel
 | 
				
			||||||
 | 
					            libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0
 | 
				
			||||||
 | 
					            make netpbm perl
 | 
				
			||||||
 | 
					            python3${{ matrix.python-minor-version }}-cffi
 | 
				
			||||||
 | 
					            python3${{ matrix.python-minor-version }}-cython
 | 
				
			||||||
 | 
					            python3${{ matrix.python-minor-version }}-devel
 | 
				
			||||||
 | 
					            python3${{ matrix.python-minor-version }}-numpy
 | 
				
			||||||
 | 
					            python3${{ matrix.python-minor-version }}-sip
 | 
				
			||||||
 | 
					            python3${{ matrix.python-minor-version }}-tkinter
 | 
				
			||||||
 | 
					            qt5-devel-tools subversion xorg-server-extra zlib-devel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Add Lapack to PATH
 | 
				
			||||||
 | 
					        uses: egor-tensin/cleanup-path@v1
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: pip cache
 | 
				
			||||||
 | 
					        uses: actions/cache@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          path: 'C:\cygwin\home\runneradmin\.cache\pip'
 | 
				
			||||||
 | 
					          key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }}
 | 
				
			||||||
 | 
					          restore-keys: |
 | 
				
			||||||
 | 
					            ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Build system information
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          dash.exe -c "python3 .github/workflows/system-info.py"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Install dependencies
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          bash.exe .ci/install.sh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Install a different NumPy
 | 
				
			||||||
 | 
					        shell: dash.exe -l "{0}"
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          python3 -m pip install -U 'numpy!=1.21.*'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Build
 | 
				
			||||||
 | 
					        shell: bash.exe -eo pipefail -o igncr "{0}"
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          .ci/build.sh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Test
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Prepare to upload errors
 | 
				
			||||||
 | 
					        if: failure()
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          dash.exe -c "mkdir -p Tests/errors"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Upload errors
 | 
				
			||||||
 | 
					        uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					        if: failure()
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          name: errors
 | 
				
			||||||
 | 
					          path: Tests/errors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: After success
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          bash.exe .ci/after_success.sh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Upload coverage
 | 
				
			||||||
 | 
					        uses: codecov/codecov-action@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          file: ./coverage.xml
 | 
				
			||||||
 | 
					          flags: GHA_Cygwin
 | 
				
			||||||
 | 
					          name: Cygwin Python 3.${{ matrix.python-minor-version }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  success:
 | 
				
			||||||
 | 
					    needs: build
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    name: Cygwin Test Successful
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: Success
 | 
				
			||||||
 | 
					        run: echo Cygwin Test Successful
 | 
				
			||||||
							
								
								
									
										15
									
								
								.github/workflows/test-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -11,9 +11,9 @@ jobs:
 | 
				
			||||||
      matrix:
 | 
					      matrix:
 | 
				
			||||||
        docker: [
 | 
					        docker: [
 | 
				
			||||||
          # Run slower jobs first to give them a headstart and reduce waiting time
 | 
					          # Run slower jobs first to give them a headstart and reduce waiting time
 | 
				
			||||||
          ubuntu-20.04-focal-arm64v8,
 | 
					          ubuntu-22.04-jammy-arm64v8,
 | 
				
			||||||
          ubuntu-20.04-focal-ppc64le,
 | 
					          ubuntu-22.04-jammy-ppc64le,
 | 
				
			||||||
          ubuntu-20.04-focal-s390x,
 | 
					          ubuntu-22.04-jammy-s390x,
 | 
				
			||||||
          # Then run the remainder
 | 
					          # Then run the remainder
 | 
				
			||||||
          alpine,
 | 
					          alpine,
 | 
				
			||||||
          amazon-2-amd64,
 | 
					          amazon-2-amd64,
 | 
				
			||||||
| 
						 | 
					@ -23,19 +23,20 @@ jobs:
 | 
				
			||||||
          centos-stream-9-amd64,
 | 
					          centos-stream-9-amd64,
 | 
				
			||||||
          debian-10-buster-x86,
 | 
					          debian-10-buster-x86,
 | 
				
			||||||
          debian-11-bullseye-x86,
 | 
					          debian-11-bullseye-x86,
 | 
				
			||||||
          fedora-34-amd64,
 | 
					 | 
				
			||||||
          fedora-35-amd64,
 | 
					          fedora-35-amd64,
 | 
				
			||||||
 | 
					          fedora-36-amd64,
 | 
				
			||||||
          gentoo,
 | 
					          gentoo,
 | 
				
			||||||
          ubuntu-18.04-bionic-amd64,
 | 
					          ubuntu-18.04-bionic-amd64,
 | 
				
			||||||
          ubuntu-20.04-focal-amd64,
 | 
					          ubuntu-20.04-focal-amd64,
 | 
				
			||||||
 | 
					          ubuntu-22.04-jammy-amd64,
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        dockerTag: [main]
 | 
					        dockerTag: [main]
 | 
				
			||||||
        include:
 | 
					        include:
 | 
				
			||||||
          - docker: "ubuntu-20.04-focal-arm64v8"
 | 
					          - docker: "ubuntu-22.04-jammy-arm64v8"
 | 
				
			||||||
            qemu-arch: "aarch64"
 | 
					            qemu-arch: "aarch64"
 | 
				
			||||||
          - docker: "ubuntu-20.04-focal-ppc64le"
 | 
					          - docker: "ubuntu-22.04-jammy-ppc64le"
 | 
				
			||||||
            qemu-arch: "ppc64le"
 | 
					            qemu-arch: "ppc64le"
 | 
				
			||||||
          - docker: "ubuntu-20.04-focal-s390x"
 | 
					          - docker: "ubuntu-22.04-jammy-s390x"
 | 
				
			||||||
            qemu-arch: "s390x"
 | 
					            qemu-arch: "s390x"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    name: ${{ matrix.docker }}
 | 
					    name: ${{ matrix.docker }}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										12
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -8,7 +8,7 @@ jobs:
 | 
				
			||||||
    strategy:
 | 
					    strategy:
 | 
				
			||||||
      fail-fast: false
 | 
					      fail-fast: false
 | 
				
			||||||
      matrix:
 | 
					      matrix:
 | 
				
			||||||
        python-version: ["3.7", "3.8", "3.9", "3.10"]
 | 
					        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11-dev"]
 | 
				
			||||||
        architecture: ["x86", "x64"]
 | 
					        architecture: ["x86", "x64"]
 | 
				
			||||||
        include:
 | 
					        include:
 | 
				
			||||||
          # PyPy 7.3.4+ only ships 64-bit binaries for Windows
 | 
					          # PyPy 7.3.4+ only ships 64-bit binaries for Windows
 | 
				
			||||||
| 
						 | 
					@ -41,10 +41,10 @@ jobs:
 | 
				
			||||||
        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: python .github/workflows/system-info.py
 | 
					      run: python3 .github/workflows/system-info.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - name: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
 | 
					    - name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
 | 
				
			||||||
      run: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
 | 
					      run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - name: Install dependencies
 | 
					    - name: Install dependencies
 | 
				
			||||||
      id: install
 | 
					      id: install
 | 
				
			||||||
| 
						 | 
					@ -52,8 +52,8 @@ jobs:
 | 
				
			||||||
        7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
 | 
					        7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\"
 | 
				
			||||||
        echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
 | 
					        echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        winbuild\depends\gs9550w32.exe /S
 | 
					        winbuild\depends\gs9561w32.exe /S
 | 
				
			||||||
        echo "C:\Program Files (x86)\gs\gs9.55.0\bin" >> $env:GITHUB_PATH
 | 
					        echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        xcopy /S /Y winbuild\depends\test_images\* Tests\images\
 | 
					        xcopy /S /Y winbuild\depends\test_images\* Tests\images\
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										5
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -15,6 +15,7 @@ jobs:
 | 
				
			||||||
        python-version: [
 | 
					        python-version: [
 | 
				
			||||||
          "pypy-3.8",
 | 
					          "pypy-3.8",
 | 
				
			||||||
          "pypy-3.7",
 | 
					          "pypy-3.7",
 | 
				
			||||||
 | 
					          "3.11-dev",
 | 
				
			||||||
          "3.10",
 | 
					          "3.10",
 | 
				
			||||||
          "3.9",
 | 
					          "3.9",
 | 
				
			||||||
          "3.8",
 | 
					          "3.8",
 | 
				
			||||||
| 
						 | 
					@ -59,6 +60,8 @@ jobs:
 | 
				
			||||||
      if: startsWith(matrix.os, 'macOS')
 | 
					      if: startsWith(matrix.os, 'macOS')
 | 
				
			||||||
      run: |
 | 
					      run: |
 | 
				
			||||||
        .github/workflows/macos-install.sh
 | 
					        .github/workflows/macos-install.sh
 | 
				
			||||||
 | 
					      env:
 | 
				
			||||||
 | 
					        GHA_PYTHON_VERSION: ${{ matrix.python-version }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - name: Build
 | 
					    - name: Build
 | 
				
			||||||
      run: |
 | 
					      run: |
 | 
				
			||||||
| 
						 | 
					@ -93,7 +96,7 @@ jobs:
 | 
				
			||||||
    - name: Docs
 | 
					    - name: Docs
 | 
				
			||||||
      if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
 | 
					      if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10
 | 
				
			||||||
      run: |
 | 
					      run: |
 | 
				
			||||||
        python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph
 | 
					        python3 -m pip install furo sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph
 | 
				
			||||||
        make doccheck
 | 
					        make doccheck
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - name: After success
 | 
					    - name: After success
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								.github/workflows/tidelift.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
					@ -4,9 +4,11 @@ on:
 | 
				
			||||||
    - cron: "30 2 * * *"  # daily at 02:30 UTC
 | 
					    - cron: "30 2 * * *"  # daily at 02:30 UTC
 | 
				
			||||||
  push:
 | 
					  push:
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
 | 
					      - "Pipfile*"
 | 
				
			||||||
      - ".github/workflows/tidelift.yml"
 | 
					      - ".github/workflows/tidelift.yml"
 | 
				
			||||||
  pull_request:
 | 
					  pull_request:
 | 
				
			||||||
    paths:
 | 
					    paths:
 | 
				
			||||||
 | 
					      - "Pipfile*"
 | 
				
			||||||
      - ".github/workflows/tidelift.yml"
 | 
					      - ".github/workflows/tidelift.yml"
 | 
				
			||||||
  workflow_dispatch:
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
repos:
 | 
					repos:
 | 
				
			||||||
  - repo: https://github.com/psf/black
 | 
					  - repo: https://github.com/psf/black
 | 
				
			||||||
    rev: fc0be6eb1e2a96091e6f64009ee5e9081bf8b6c6  # frozen: 22.1.0
 | 
					    rev: 22.3.0
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: black
 | 
					      - id: black
 | 
				
			||||||
        args: ["--target-version", "py37"]
 | 
					        args: ["--target-version", "py37"]
 | 
				
			||||||
| 
						 | 
					@ -9,38 +9,43 @@ repos:
 | 
				
			||||||
        types: []
 | 
					        types: []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - repo: https://github.com/PyCQA/isort
 | 
					  - repo: https://github.com/PyCQA/isort
 | 
				
			||||||
    rev: c5e8fa75dda5f764d20f66a215d71c21cfa198e1  # frozen: 5.10.1
 | 
					    rev: 5.10.1
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: isort
 | 
					      - id: isort
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - repo: https://github.com/asottile/yesqa
 | 
					  - repo: https://github.com/asottile/yesqa
 | 
				
			||||||
    rev: 35cf7dc24fa922927caded7a21b2a8cb04bf8e10  # frozen: v1.3.0
 | 
					    rev: v1.3.0
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: yesqa
 | 
					      - id: yesqa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - repo: https://github.com/Lucas-C/pre-commit-hooks
 | 
					  - repo: https://github.com/Lucas-C/pre-commit-hooks
 | 
				
			||||||
    rev: ca52c4245639abd55c970e6bbbca95cab3de22d8  # frozen: v1.1.13
 | 
					    rev: v1.2.0
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: remove-tabs
 | 
					      - id: remove-tabs
 | 
				
			||||||
        exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
 | 
					        exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - repo: https://github.com/PyCQA/flake8
 | 
					  - repo: https://github.com/PyCQA/flake8
 | 
				
			||||||
    rev: cbeb4c9c4137cff1568659fcc48e8b85cddd0c8d  # frozen: 4.0.1
 | 
					    rev: 4.0.1
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: flake8
 | 
					      - id: flake8
 | 
				
			||||||
        additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
 | 
					        additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - repo: https://github.com/pre-commit/pygrep-hooks
 | 
					  - repo: https://github.com/pre-commit/pygrep-hooks
 | 
				
			||||||
    rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce  # frozen: v1.9.0
 | 
					    rev: v1.9.0
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: python-check-blanket-noqa
 | 
					      - 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: 8fe62d14e0b4d7d845a7022c5c2c3ae41bdd3f26  # frozen: v4.1.0
 | 
					    rev: v4.2.0
 | 
				
			||||||
    hooks:
 | 
					    hooks:
 | 
				
			||||||
      - id: check-merge-conflict
 | 
					      - id: check-merge-conflict
 | 
				
			||||||
      - id: check-yaml
 | 
					      - id: check-yaml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  - repo: https://github.com/sphinx-contrib/sphinx-lint
 | 
				
			||||||
 | 
					    rev: v0.6
 | 
				
			||||||
 | 
					    hooks:
 | 
				
			||||||
 | 
					      - id: sphinx-lint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ci:
 | 
					ci:
 | 
				
			||||||
  autoupdate_schedule: quarterly
 | 
					  autoupdate_schedule: monthly
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2637
									
								
								CHANGES.rst
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						| 
						 | 
					@ -77,7 +77,7 @@ release-test:
 | 
				
			||||||
	-rm dist/*.egg
 | 
						-rm dist/*.egg
 | 
				
			||||||
	-rmdir dist
 | 
						-rmdir dist
 | 
				
			||||||
	python3 -m pytest -qq
 | 
						python3 -m pytest -qq
 | 
				
			||||||
	python3 -m check-manifest
 | 
						python3 -m check_manifest
 | 
				
			||||||
	python3 -m pyroma .
 | 
						python3 -m pyroma .
 | 
				
			||||||
	$(MAKE) readme
 | 
						$(MAKE) readme
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -85,6 +85,8 @@ release-test:
 | 
				
			||||||
sdist:
 | 
					sdist:
 | 
				
			||||||
	python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build
 | 
						python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build
 | 
				
			||||||
	python3 -m build --sdist
 | 
						python3 -m build --sdist
 | 
				
			||||||
 | 
						python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine
 | 
				
			||||||
 | 
						python3 -m twine check --strict dist/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.PHONY: test
 | 
					.PHONY: test
 | 
				
			||||||
test:
 | 
					test:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,9 @@ As of 2019, Pillow development is
 | 
				
			||||||
            <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
 | 
					            <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
 | 
				
			||||||
                alt="GitHub Actions build status (Test MinGW)"
 | 
					                alt="GitHub Actions build status (Test MinGW)"
 | 
				
			||||||
                src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a>
 | 
					                src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a>
 | 
				
			||||||
 | 
					            <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml"><img
 | 
				
			||||||
 | 
					                alt="GitHub Actions build status (Test Cygwin)"
 | 
				
			||||||
 | 
					                src="https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg"></a>
 | 
				
			||||||
            <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
 | 
					            <a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
 | 
				
			||||||
                alt="GitHub Actions build status (Test Docker)"
 | 
					                alt="GitHub Actions build status (Test Docker)"
 | 
				
			||||||
                src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>
 | 
					                src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										11
									
								
								RELEASING.md
									
									
									
									
									
								
							
							
						
						| 
						 | 
					@ -24,13 +24,12 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
 | 
				
			||||||
* [ ] Create and check source distribution:
 | 
					* [ ] Create and check source distribution:
 | 
				
			||||||
  ```bash
 | 
					  ```bash
 | 
				
			||||||
  make sdist
 | 
					  make sdist
 | 
				
			||||||
  twine check dist/*
 | 
					 | 
				
			||||||
  ```
 | 
					  ```
 | 
				
			||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
 | 
					* [ ] 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.:
 | 
					* [ ] Check and upload all binaries and source distributions e.g.:
 | 
				
			||||||
  ```bash
 | 
					  ```bash
 | 
				
			||||||
  twine check dist/*
 | 
					  python3 -m twine check --strict dist/*
 | 
				
			||||||
  twine upload dist/Pillow-5.2.0*
 | 
					  python3 -m twine upload dist/Pillow-5.2.0*
 | 
				
			||||||
  ```
 | 
					  ```
 | 
				
			||||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
 | 
					* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
 | 
				
			||||||
* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py`
 | 
					* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py`
 | 
				
			||||||
| 
						 | 
					@ -61,13 +60,12 @@ Released as needed for security, installation or critical bug fixes.
 | 
				
			||||||
* [ ] Create and check source distribution:
 | 
					* [ ] Create and check source distribution:
 | 
				
			||||||
  ```bash
 | 
					  ```bash
 | 
				
			||||||
  make sdist
 | 
					  make sdist
 | 
				
			||||||
  twine check dist/*
 | 
					 | 
				
			||||||
  ```
 | 
					  ```
 | 
				
			||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
 | 
					* [ ] 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.:
 | 
					* [ ] Check and upload all binaries and source distributions e.g.:
 | 
				
			||||||
  ```bash
 | 
					  ```bash
 | 
				
			||||||
  twine check dist/*
 | 
					  python3 -m twine check --strict dist/*
 | 
				
			||||||
  twine upload dist/Pillow-5.2.1*
 | 
					  python3 -m twine upload dist/Pillow-5.2.1*
 | 
				
			||||||
  ```
 | 
					  ```
 | 
				
			||||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
 | 
					* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -91,7 +89,6 @@ Released as needed privately to individual vendors for critical security-related
 | 
				
			||||||
* [ ] Create and check source distribution:
 | 
					* [ ] Create and check source distribution:
 | 
				
			||||||
  ```bash
 | 
					  ```bash
 | 
				
			||||||
  make sdist
 | 
					  make sdist
 | 
				
			||||||
  twine check dist/*
 | 
					 | 
				
			||||||
  ```
 | 
					  ```
 | 
				
			||||||
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
 | 
					* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
 | 
				
			||||||
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
 | 
					* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ Dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Install::
 | 
					Install::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    python3 -m pip install pytest pytest-cov
 | 
					    python3 -m pip install pytest pytest-cov pytest-timeout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Execution
 | 
					Execution
 | 
				
			||||||
---------
 | 
					---------
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -324,7 +324,7 @@ def is_mingw():
 | 
				
			||||||
    return sysconfig.get_platform() == "mingw"
 | 
					    return sysconfig.get_platform() == "mingw"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class cached_property:
 | 
					class CachedProperty:
 | 
				
			||||||
    def __init__(self, func):
 | 
					    def __init__(self, func):
 | 
				
			||||||
        self.func = func
 | 
					        self.func = func
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								Tests/images/16bit.r.tif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/comment_after_last_frame.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/cross_scan_line_truncated.tga
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 Before Width: | Height: | Size: 58 B After Width: | Height: | Size: 198 B  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/duplicate_number_of_loops.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_bigtiff.tif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_rle8.bmp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 26 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_rle8_row_overflow.bmp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 26 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/imagedraw/discontiguous_corners_polygon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 486 B  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/imagedraw_polygon_1px_high_translucent.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 76 B  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/issue_6194.j2k
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/multiple_comments.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/no_palette.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 48 B  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/no_palette_with_background.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 54 B  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/no_palette_with_transparency.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/second_frame_comment.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.3 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/tiff_wrong_bits_per_sample_3.tiff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/tiny.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 16 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/zero_height.j2k
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -40,6 +40,7 @@ def test_questionable():
 | 
				
			||||||
        "rgb32fakealpha.bmp",
 | 
					        "rgb32fakealpha.bmp",
 | 
				
			||||||
        "rgb24largepal.bmp",
 | 
					        "rgb24largepal.bmp",
 | 
				
			||||||
        "pal8os2sp.bmp",
 | 
					        "pal8os2sp.bmp",
 | 
				
			||||||
 | 
					        "pal8rletrns.bmp",
 | 
				
			||||||
        "rgb32bf-xbgr.bmp",
 | 
					        "rgb32bf-xbgr.bmp",
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    for f in get_files("q"):
 | 
					    for f in get_files("q"):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,7 +25,7 @@ def box_blur(image, radius=1, n=1):
 | 
				
			||||||
    return image._new(image.im.box_blur(radius, n))
 | 
					    return image._new(image.im.box_blur(radius, n))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def assertImage(im, data, delta=0):
 | 
					def assert_image(im, data, delta=0):
 | 
				
			||||||
    it = iter(im.getdata())
 | 
					    it = iter(im.getdata())
 | 
				
			||||||
    for data_row in data:
 | 
					    for data_row in data:
 | 
				
			||||||
        im_row = [next(it) for _ in range(im.size[0])]
 | 
					        im_row = [next(it) for _ in range(im.size[0])]
 | 
				
			||||||
| 
						 | 
					@ -35,12 +35,12 @@ def assertImage(im, data, delta=0):
 | 
				
			||||||
        next(it)
 | 
					        next(it)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def assertBlur(im, radius, data, passes=1, delta=0):
 | 
					def assert_blur(im, radius, data, passes=1, delta=0):
 | 
				
			||||||
    # check grayscale image
 | 
					    # check grayscale image
 | 
				
			||||||
    assertImage(box_blur(im, radius, passes), data, delta)
 | 
					    assert_image(box_blur(im, radius, passes), data, delta)
 | 
				
			||||||
    rgba = Image.merge("RGBA", (im, im, im, im))
 | 
					    rgba = Image.merge("RGBA", (im, im, im, im))
 | 
				
			||||||
    for band in box_blur(rgba, radius, passes).split():
 | 
					    for band in box_blur(rgba, radius, passes).split():
 | 
				
			||||||
        assertImage(band, data, delta)
 | 
					        assert_image(band, data, delta)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_color_modes():
 | 
					def test_color_modes():
 | 
				
			||||||
| 
						 | 
					@ -64,7 +64,7 @@ def test_color_modes():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_radius_0():
 | 
					def test_radius_0():
 | 
				
			||||||
    assertBlur(
 | 
					    assert_blur(
 | 
				
			||||||
        sample,
 | 
					        sample,
 | 
				
			||||||
        0,
 | 
					        0,
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
| 
						 | 
					@ -80,7 +80,7 @@ def test_radius_0():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_radius_0_02():
 | 
					def test_radius_0_02():
 | 
				
			||||||
    assertBlur(
 | 
					    assert_blur(
 | 
				
			||||||
        sample,
 | 
					        sample,
 | 
				
			||||||
        0.02,
 | 
					        0.02,
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
| 
						 | 
					@ -97,7 +97,7 @@ def test_radius_0_02():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_radius_0_05():
 | 
					def test_radius_0_05():
 | 
				
			||||||
    assertBlur(
 | 
					    assert_blur(
 | 
				
			||||||
        sample,
 | 
					        sample,
 | 
				
			||||||
        0.05,
 | 
					        0.05,
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
| 
						 | 
					@ -114,7 +114,7 @@ def test_radius_0_05():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_radius_0_1():
 | 
					def test_radius_0_1():
 | 
				
			||||||
    assertBlur(
 | 
					    assert_blur(
 | 
				
			||||||
        sample,
 | 
					        sample,
 | 
				
			||||||
        0.1,
 | 
					        0.1,
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
| 
						 | 
					@ -131,7 +131,7 @@ def test_radius_0_1():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_radius_0_5():
 | 
					def test_radius_0_5():
 | 
				
			||||||
    assertBlur(
 | 
					    assert_blur(
 | 
				
			||||||
        sample,
 | 
					        sample,
 | 
				
			||||||
        0.5,
 | 
					        0.5,
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
| 
						 | 
					@ -148,7 +148,7 @@ def test_radius_0_5():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_radius_1():
 | 
					def test_radius_1():
 | 
				
			||||||
    assertBlur(
 | 
					    assert_blur(
 | 
				
			||||||
        sample,
 | 
					        sample,
 | 
				
			||||||
        1,
 | 
					        1,
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
| 
						 | 
					@ -165,7 +165,7 @@ def test_radius_1():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_radius_1_5():
 | 
					def test_radius_1_5():
 | 
				
			||||||
    assertBlur(
 | 
					    assert_blur(
 | 
				
			||||||
        sample,
 | 
					        sample,
 | 
				
			||||||
        1.5,
 | 
					        1.5,
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
| 
						 | 
					@ -182,7 +182,7 @@ def test_radius_1_5():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_radius_bigger_then_half():
 | 
					def test_radius_bigger_then_half():
 | 
				
			||||||
    assertBlur(
 | 
					    assert_blur(
 | 
				
			||||||
        sample,
 | 
					        sample,
 | 
				
			||||||
        3,
 | 
					        3,
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
| 
						 | 
					@ -199,7 +199,7 @@ def test_radius_bigger_then_half():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_radius_bigger_then_width():
 | 
					def test_radius_bigger_then_width():
 | 
				
			||||||
    assertBlur(
 | 
					    assert_blur(
 | 
				
			||||||
        sample,
 | 
					        sample,
 | 
				
			||||||
        10,
 | 
					        10,
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
| 
						 | 
					@ -214,7 +214,7 @@ def test_radius_bigger_then_width():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_extreme_large_radius():
 | 
					def test_extreme_large_radius():
 | 
				
			||||||
    assertBlur(
 | 
					    assert_blur(
 | 
				
			||||||
        sample,
 | 
					        sample,
 | 
				
			||||||
        600,
 | 
					        600,
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
| 
						 | 
					@ -229,7 +229,7 @@ def test_extreme_large_radius():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_two_passes():
 | 
					def test_two_passes():
 | 
				
			||||||
    assertBlur(
 | 
					    assert_blur(
 | 
				
			||||||
        sample,
 | 
					        sample,
 | 
				
			||||||
        1,
 | 
					        1,
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
| 
						 | 
					@ -247,7 +247,7 @@ def test_two_passes():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_three_passes():
 | 
					def test_three_passes():
 | 
				
			||||||
    assertBlur(
 | 
					    assert_blur(
 | 
				
			||||||
        sample,
 | 
					        sample,
 | 
				
			||||||
        1,
 | 
					        1,
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,27 +15,27 @@ except ImportError:
 | 
				
			||||||
class TestColorLut3DCoreAPI:
 | 
					class TestColorLut3DCoreAPI:
 | 
				
			||||||
    def generate_identity_table(self, channels, size):
 | 
					    def generate_identity_table(self, channels, size):
 | 
				
			||||||
        if isinstance(size, tuple):
 | 
					        if isinstance(size, tuple):
 | 
				
			||||||
            size1D, size2D, size3D = size
 | 
					            size_1d, size_2d, size_3d = size
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            size1D, size2D, size3D = (size, size, size)
 | 
					            size_1d, size_2d, size_3d = (size, size, size)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        table = [
 | 
					        table = [
 | 
				
			||||||
            [
 | 
					            [
 | 
				
			||||||
                r / (size1D - 1) if size1D != 1 else 0,
 | 
					                r / (size_1d - 1) if size_1d != 1 else 0,
 | 
				
			||||||
                g / (size2D - 1) if size2D != 1 else 0,
 | 
					                g / (size_2d - 1) if size_2d != 1 else 0,
 | 
				
			||||||
                b / (size3D - 1) if size3D != 1 else 0,
 | 
					                b / (size_3d - 1) if size_3d != 1 else 0,
 | 
				
			||||||
                r / (size1D - 1) if size1D != 1 else 0,
 | 
					                r / (size_1d - 1) if size_1d != 1 else 0,
 | 
				
			||||||
                g / (size2D - 1) if size2D != 1 else 0,
 | 
					                g / (size_2d - 1) if size_2d != 1 else 0,
 | 
				
			||||||
            ][:channels]
 | 
					            ][:channels]
 | 
				
			||||||
            for b in range(size3D)
 | 
					            for b in range(size_3d)
 | 
				
			||||||
            for g in range(size2D)
 | 
					            for g in range(size_2d)
 | 
				
			||||||
            for r in range(size1D)
 | 
					            for r in range(size_1d)
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
            channels,
 | 
					            channels,
 | 
				
			||||||
            size1D,
 | 
					            size_1d,
 | 
				
			||||||
            size2D,
 | 
					            size_2d,
 | 
				
			||||||
            size3D,
 | 
					            size_3d,
 | 
				
			||||||
            [item for sublist in table for item in sublist],
 | 
					            [item for sublist in table for item in sublist],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -567,7 +567,7 @@ class TestTransformColorLut3D:
 | 
				
			||||||
        assert tuple(lut.size) == tuple(source.size)
 | 
					        assert tuple(lut.size) == tuple(source.size)
 | 
				
			||||||
        assert len(lut.table) == len(source.table)
 | 
					        assert len(lut.table) == len(source.table)
 | 
				
			||||||
        assert lut.table != source.table
 | 
					        assert lut.table != source.table
 | 
				
			||||||
        assert lut.table[0:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
 | 
					        assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_3_to_4_channels(self):
 | 
					    def test_3_to_4_channels(self):
 | 
				
			||||||
        source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b))
 | 
					        source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b))
 | 
				
			||||||
| 
						 | 
					@ -576,7 +576,7 @@ class TestTransformColorLut3D:
 | 
				
			||||||
        assert len(lut.table) != len(source.table)
 | 
					        assert len(lut.table) != len(source.table)
 | 
				
			||||||
        assert lut.table != source.table
 | 
					        assert lut.table != source.table
 | 
				
			||||||
        # fmt: off
 | 
					        # fmt: off
 | 
				
			||||||
        assert lut.table[0:16] == [
 | 
					        assert lut.table[:16] == [
 | 
				
			||||||
            0.0, 0.0, 0.0, 1,  0.2**2, 0.0, 0.0, 1,
 | 
					            0.0, 0.0, 0.0, 1,  0.2**2, 0.0, 0.0, 1,
 | 
				
			||||||
            0.4**2, 0.0, 0.0, 1,  0.6**2, 0.0, 0.0, 1]
 | 
					            0.4**2, 0.0, 0.0, 1,  0.6**2, 0.0, 0.0, 1]
 | 
				
			||||||
        # fmt: on
 | 
					        # fmt: on
 | 
				
			||||||
| 
						 | 
					@ -592,7 +592,7 @@ class TestTransformColorLut3D:
 | 
				
			||||||
        assert len(lut.table) != len(source.table)
 | 
					        assert len(lut.table) != len(source.table)
 | 
				
			||||||
        assert lut.table != source.table
 | 
					        assert lut.table != source.table
 | 
				
			||||||
        # fmt: off
 | 
					        # fmt: off
 | 
				
			||||||
        assert lut.table[0:18] == [
 | 
					        assert lut.table[:18] == [
 | 
				
			||||||
            1.0, 1.0, 1.0,  0.75, 1.0, 1.0,  0.0, 1.0, 1.0,
 | 
					            1.0, 1.0, 1.0,  0.75, 1.0, 1.0,  0.0, 1.0, 1.0,
 | 
				
			||||||
            1.0, 0.96, 1.0,  0.75, 0.96, 1.0,  0.0, 0.96, 1.0]
 | 
					            1.0, 0.96, 1.0,  0.75, 0.96, 1.0,  0.0, 0.96, 1.0]
 | 
				
			||||||
        # fmt: on
 | 
					        # fmt: on
 | 
				
			||||||
| 
						 | 
					@ -606,7 +606,7 @@ class TestTransformColorLut3D:
 | 
				
			||||||
        assert len(lut.table) == len(source.table)
 | 
					        assert len(lut.table) == len(source.table)
 | 
				
			||||||
        assert lut.table != source.table
 | 
					        assert lut.table != source.table
 | 
				
			||||||
        # fmt: off
 | 
					        # fmt: off
 | 
				
			||||||
        assert lut.table[0:16] == [
 | 
					        assert lut.table[:16] == [
 | 
				
			||||||
            0.0, 0.0, 0.0, 0.5,  0.2**2, 0.0, 0.0, 0.5,
 | 
					            0.0, 0.0, 0.0, 0.5,  0.2**2, 0.0, 0.0, 0.5,
 | 
				
			||||||
            0.4**2, 0.0, 0.0, 0.5,  0.6**2, 0.0, 0.0, 0.5]
 | 
					            0.4**2, 0.0, 0.0, 0.5,  0.6**2, 0.0, 0.0, 0.5]
 | 
				
			||||||
        # fmt: on
 | 
					        # fmt: on
 | 
				
			||||||
| 
						 | 
					@ -622,7 +622,7 @@ class TestTransformColorLut3D:
 | 
				
			||||||
        assert len(lut.table) == len(source.table)
 | 
					        assert len(lut.table) == len(source.table)
 | 
				
			||||||
        assert lut.table != source.table
 | 
					        assert lut.table != source.table
 | 
				
			||||||
        # fmt: off
 | 
					        # fmt: off
 | 
				
			||||||
        assert lut.table[0:18] == [
 | 
					        assert lut.table[:18] == [
 | 
				
			||||||
            0.0, 0.0, 0.0,  0.16, 0.0, 0.0,  0.24, 0.0, 0.0,
 | 
					            0.0, 0.0, 0.0,  0.16, 0.0, 0.0,  0.24, 0.0, 0.0,
 | 
				
			||||||
            0.24, 0.0, 0.0,  0.8 - (0.8**2), 0, 0,  0, 0, 0]
 | 
					            0.24, 0.0, 0.0,  0.8 - (0.8**2), 0, 0,  0, 0, 0]
 | 
				
			||||||
        # fmt: on
 | 
					        # fmt: on
 | 
				
			||||||
| 
						 | 
					@ -639,7 +639,7 @@ class TestTransformColorLut3D:
 | 
				
			||||||
        assert len(lut.table) == len(source.table)
 | 
					        assert len(lut.table) == len(source.table)
 | 
				
			||||||
        assert lut.table != source.table
 | 
					        assert lut.table != source.table
 | 
				
			||||||
        # fmt: off
 | 
					        # fmt: off
 | 
				
			||||||
        assert lut.table[0:16] == [
 | 
					        assert lut.table[:16] == [
 | 
				
			||||||
            0.0, 0.0, 0.0, 0.5,  0.25, 0.0, 0.0, 0.5,
 | 
					            0.0, 0.0, 0.0, 0.5,  0.25, 0.0, 0.0, 0.5,
 | 
				
			||||||
            0.0, 0.0, 0.0, 0.5,  0.0, 0.16, 0.0, 0.5]
 | 
					            0.0, 0.0, 0.0, 0.5,  0.0, 0.16, 0.0, 0.5]
 | 
				
			||||||
        # fmt: on
 | 
					        # fmt: on
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,7 +51,6 @@ class TestDecompressionBomb:
 | 
				
			||||||
            with Image.open(TEST_FILE):
 | 
					            with Image.open(TEST_FILE):
 | 
				
			||||||
                pass
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @pytest.mark.xfail(reason="different exception")
 | 
					 | 
				
			||||||
    def test_exception_ico(self):
 | 
					    def test_exception_ico(self):
 | 
				
			||||||
        with pytest.raises(Image.DecompressionBombError):
 | 
					        with pytest.raises(Image.DecompressionBombError):
 | 
				
			||||||
            with Image.open("Tests/images/decompression_bomb.ico"):
 | 
					            with Image.open("Tests/images/decompression_bomb.ico"):
 | 
				
			||||||
| 
						 | 
					@ -70,15 +69,15 @@ class TestDecompressionBomb:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestDecompressionCrop:
 | 
					class TestDecompressionCrop:
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def setup_class(self):
 | 
					    def setup_class(cls):
 | 
				
			||||||
        width, height = 128, 128
 | 
					        width, height = 128, 128
 | 
				
			||||||
        Image.MAX_IMAGE_PIXELS = height * width * 4 - 1
 | 
					        Image.MAX_IMAGE_PIXELS = height * width * 4 - 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def teardown_class(self):
 | 
					    def teardown_class(cls):
 | 
				
			||||||
        Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
 | 
					        Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def testEnlargeCrop(self):
 | 
					    def test_enlarge_crop(self):
 | 
				
			||||||
        # Crops can extend the extents, therefore we should have the
 | 
					        # Crops can extend the extents, therefore we should have the
 | 
				
			||||||
        # same decompression bomb warnings on them.
 | 
					        # same decompression bomb warnings on them.
 | 
				
			||||||
        with hopper() as src:
 | 
					        with hopper() as src:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										91
									
								
								Tests/test_deprecate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,91 @@
 | 
				
			||||||
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from PIL import _deprecate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
 | 
					    "version, expected",
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            10,
 | 
				
			||||||
 | 
					            "Old thing is deprecated and will be removed in Pillow 10 "
 | 
				
			||||||
 | 
					            r"\(2023-07-01\)\. Use new thing instead\.",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            None,
 | 
				
			||||||
 | 
					            r"Old thing is deprecated and will be removed in a future version\. "
 | 
				
			||||||
 | 
					            r"Use new thing instead\.",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_version(version, expected):
 | 
				
			||||||
 | 
					    with pytest.warns(DeprecationWarning, match=expected):
 | 
				
			||||||
 | 
					        _deprecate.deprecate("Old thing", version, "new thing")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_unknown_version():
 | 
				
			||||||
 | 
					    expected = r"Unknown removal version, update PIL\._deprecate\?"
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError, match=expected):
 | 
				
			||||||
 | 
					        _deprecate.deprecate("Old thing", 12345, "new thing")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
 | 
					    "deprecated, plural, expected",
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            "Old thing",
 | 
				
			||||||
 | 
					            False,
 | 
				
			||||||
 | 
					            r"Old thing is deprecated and should be removed\.",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            "Old things",
 | 
				
			||||||
 | 
					            True,
 | 
				
			||||||
 | 
					            r"Old things are deprecated and should be removed\.",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_old_version(deprecated, plural, expected):
 | 
				
			||||||
 | 
					    expected = r""
 | 
				
			||||||
 | 
					    with pytest.raises(RuntimeError, match=expected):
 | 
				
			||||||
 | 
					        _deprecate.deprecate(deprecated, 1, plural=plural)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_plural():
 | 
				
			||||||
 | 
					    expected = (
 | 
				
			||||||
 | 
					        r"Old things are deprecated and will be removed in Pillow 10 \(2023-07-01\)\. "
 | 
				
			||||||
 | 
					        r"Use new thing instead\."
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    with pytest.warns(DeprecationWarning, match=expected):
 | 
				
			||||||
 | 
					        _deprecate.deprecate("Old things", 10, "new thing", plural=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_replacement_and_action():
 | 
				
			||||||
 | 
					    expected = "Use only one of 'replacement' and 'action'"
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError, match=expected):
 | 
				
			||||||
 | 
					        _deprecate.deprecate(
 | 
				
			||||||
 | 
					            "Old thing", 10, replacement="new thing", action="Upgrade to new thing"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
 | 
					    "action",
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					        "Upgrade to new thing",
 | 
				
			||||||
 | 
					        "Upgrade to new thing.",
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_action(action):
 | 
				
			||||||
 | 
					    expected = (
 | 
				
			||||||
 | 
					        r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)\. "
 | 
				
			||||||
 | 
					        r"Upgrade to new thing\."
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    with pytest.warns(DeprecationWarning, match=expected):
 | 
				
			||||||
 | 
					        _deprecate.deprecate("Old thing", 10, action=action)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_no_replacement_or_action():
 | 
				
			||||||
 | 
					    expected = (
 | 
				
			||||||
 | 
					        r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    with pytest.warns(DeprecationWarning, match=expected):
 | 
				
			||||||
 | 
					        _deprecate.deprecate("Old thing", 10)
 | 
				
			||||||
							
								
								
									
										18
									
								
								Tests/test_deprecated_imageqt.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					import warnings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					with warnings.catch_warnings(record=True) as w:
 | 
				
			||||||
 | 
					    # Arrange: cause all warnings to always be triggered
 | 
				
			||||||
 | 
					    warnings.simplefilter("always")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Act: trigger a warning with Qt5
 | 
				
			||||||
 | 
					    from PIL import ImageQt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_deprecated():
 | 
				
			||||||
 | 
					    # Assert
 | 
				
			||||||
 | 
					    if ImageQt.qt_version in ("5", "side2"):
 | 
				
			||||||
 | 
					        assert len(w) == 1
 | 
				
			||||||
 | 
					        assert issubclass(w[0].category, DeprecationWarning)
 | 
				
			||||||
 | 
					        assert "deprecated" in str(w[0].message)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        assert len(w) == 0
 | 
				
			||||||
| 
						 | 
					@ -637,6 +637,15 @@ def test_apng_save_blend(tmp_path):
 | 
				
			||||||
        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
 | 
					        assert im.getpixel((0, 0)) == (0, 255, 0, 255)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_seek_after_close():
 | 
				
			||||||
 | 
					    im = Image.open("Tests/images/apng/delay.png")
 | 
				
			||||||
 | 
					    im.seek(1)
 | 
				
			||||||
 | 
					    im.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					        im.seek(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_constants_deprecation():
 | 
					def test_constants_deprecation():
 | 
				
			||||||
    for enum, prefix in {
 | 
					    for enum, prefix in {
 | 
				
			||||||
        PngImagePlugin.Disposal: "APNG_DISPOSE_",
 | 
					        PngImagePlugin.Disposal: "APNG_DISPOSE_",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,12 @@ import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import BmpImagePlugin, Image
 | 
					from PIL import BmpImagePlugin, Image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
 | 
					from .helper import (
 | 
				
			||||||
 | 
					    assert_image_equal,
 | 
				
			||||||
 | 
					    assert_image_equal_tofile,
 | 
				
			||||||
 | 
					    assert_image_similar_tofile,
 | 
				
			||||||
 | 
					    hopper,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_sanity(tmp_path):
 | 
					def test_sanity(tmp_path):
 | 
				
			||||||
| 
						 | 
					@ -125,6 +130,42 @@ def test_rgba_bitfields():
 | 
				
			||||||
    assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
 | 
					    assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_rle8():
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/hopper_rle8.bmp") as im:
 | 
				
			||||||
 | 
					        assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # This test image has been manually hexedited
 | 
				
			||||||
 | 
					    # to have rows with too much data
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im:
 | 
				
			||||||
 | 
					        assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Signal end of bitmap before the image is finished
 | 
				
			||||||
 | 
					    with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp:
 | 
				
			||||||
 | 
					        data = fp.read(1063) + b"\x01"
 | 
				
			||||||
 | 
					        with Image.open(io.BytesIO(data)) as im:
 | 
				
			||||||
 | 
					            with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					                im.load()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
 | 
					    "file_name,length",
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        # EOF immediately after the header
 | 
				
			||||||
 | 
					        ("Tests/images/hopper_rle8.bmp", 1078),
 | 
				
			||||||
 | 
					        # EOF during delta
 | 
				
			||||||
 | 
					        ("Tests/images/bmp/q/pal8rletrns.bmp", 3670),
 | 
				
			||||||
 | 
					        # EOF when reading data in absolute mode
 | 
				
			||||||
 | 
					        ("Tests/images/bmp/g/pal8rle.bmp", 1064),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_rle8_eof(file_name, length):
 | 
				
			||||||
 | 
					    with open(file_name, "rb") as fp:
 | 
				
			||||||
 | 
					        data = fp.read(length)
 | 
				
			||||||
 | 
					        with Image.open(io.BytesIO(data)) as im:
 | 
				
			||||||
 | 
					            with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					                im.load()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_offset():
 | 
					def test_offset():
 | 
				
			||||||
    # This image has been hexedited
 | 
					    # This image has been hexedited
 | 
				
			||||||
    # to exclude the palette size from the pixel data offset
 | 
					    # to exclude the palette size from the pixel data offset
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,6 +46,15 @@ def test_closed_file():
 | 
				
			||||||
        im.close()
 | 
					        im.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_seek_after_close():
 | 
				
			||||||
 | 
					    im = Image.open(animated_test_file)
 | 
				
			||||||
 | 
					    im.seek(1)
 | 
				
			||||||
 | 
					    im.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					        im.seek(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_context_manager():
 | 
					def test_context_manager():
 | 
				
			||||||
    with warnings.catch_warnings():
 | 
					    with warnings.catch_warnings():
 | 
				
			||||||
        with Image.open(static_test_file) as im:
 | 
					        with Image.open(static_test_file) as im:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@ from io import BytesIO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, features
 | 
					from PIL import GifImagePlugin, Image, ImageDraw, ImagePalette, ImageSequence, features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .helper import (
 | 
					from .helper import (
 | 
				
			||||||
    assert_image_equal,
 | 
					    assert_image_equal,
 | 
				
			||||||
| 
						 | 
					@ -46,6 +46,19 @@ def test_closed_file():
 | 
				
			||||||
        im.close()
 | 
					        im.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_seek_after_close():
 | 
				
			||||||
 | 
					    im = Image.open("Tests/images/iss634.gif")
 | 
				
			||||||
 | 
					    im.load()
 | 
				
			||||||
 | 
					    im.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					        im.is_animated
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					        im.n_frames
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					        im.seek(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_context_manager():
 | 
					def test_context_manager():
 | 
				
			||||||
    with warnings.catch_warnings():
 | 
					    with warnings.catch_warnings():
 | 
				
			||||||
        with Image.open(TEST_GIF) as im:
 | 
					        with Image.open(TEST_GIF) as im:
 | 
				
			||||||
| 
						 | 
					@ -59,6 +72,51 @@ def test_invalid_file():
 | 
				
			||||||
        GifImagePlugin.GifImageFile(invalid_file)
 | 
					        GifImagePlugin.GifImageFile(invalid_file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_l_mode_transparency():
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
 | 
				
			||||||
 | 
					        assert im.mode == "L"
 | 
				
			||||||
 | 
					        assert im.load()[0, 0] == 128
 | 
				
			||||||
 | 
					        assert im.info["transparency"] == 255
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        im.seek(1)
 | 
				
			||||||
 | 
					        assert im.mode == "L"
 | 
				
			||||||
 | 
					        assert im.load()[0, 0] == 128
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_strategy():
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/chi.gif") as im:
 | 
				
			||||||
 | 
					        expected_zero = im.convert("RGB")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        im.seek(1)
 | 
				
			||||||
 | 
					        expected_one = im.convert("RGB")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/chi.gif") as im:
 | 
				
			||||||
 | 
					            assert im.mode == "RGB"
 | 
				
			||||||
 | 
					            assert_image_equal(im, expected_zero)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        GifImagePlugin.LOADING_STRATEGY = (
 | 
				
			||||||
 | 
					            GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        # Stay in P mode with only a global palette
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/chi.gif") as im:
 | 
				
			||||||
 | 
					            assert im.mode == "P"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            im.seek(1)
 | 
				
			||||||
 | 
					            assert im.mode == "P"
 | 
				
			||||||
 | 
					            assert_image_equal(im.convert("RGB"), expected_one)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Change to RGB mode when a frame has an individual palette
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/iss634.gif") as im:
 | 
				
			||||||
 | 
					            assert im.mode == "P"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            im.seek(1)
 | 
				
			||||||
 | 
					            assert im.mode == "RGB"
 | 
				
			||||||
 | 
					    finally:
 | 
				
			||||||
 | 
					        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_optimize():
 | 
					def test_optimize():
 | 
				
			||||||
    def test_grayscale(optimize):
 | 
					    def test_grayscale(optimize):
 | 
				
			||||||
        im = Image.new("L", (1, 1), 0)
 | 
					        im = Image.new("L", (1, 1), 0)
 | 
				
			||||||
| 
						 | 
					@ -296,16 +354,23 @@ def test_seek_rewind():
 | 
				
			||||||
            assert_image_equal(im, expected)
 | 
					            assert_image_equal(im, expected)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_n_frames():
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
    for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]:
 | 
					    "path, n_frames",
 | 
				
			||||||
        # Test is_animated before n_frames
 | 
					    (
 | 
				
			||||||
        with Image.open(path) as im:
 | 
					        (TEST_GIF, 1),
 | 
				
			||||||
            assert im.is_animated == (n_frames != 1)
 | 
					        ("Tests/images/comment_after_last_frame.gif", 2),
 | 
				
			||||||
 | 
					        ("Tests/images/iss634.gif", 42),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_n_frames(path, n_frames):
 | 
				
			||||||
 | 
					    # Test is_animated before n_frames
 | 
				
			||||||
 | 
					    with Image.open(path) as im:
 | 
				
			||||||
 | 
					        assert im.is_animated == (n_frames != 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Test is_animated after n_frames
 | 
					    # Test is_animated after n_frames
 | 
				
			||||||
        with Image.open(path) as im:
 | 
					    with Image.open(path) as im:
 | 
				
			||||||
            assert im.n_frames == n_frames
 | 
					        assert im.n_frames == n_frames
 | 
				
			||||||
            assert im.is_animated == (n_frames != 1)
 | 
					        assert im.is_animated == (n_frames != 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_no_change():
 | 
					def test_no_change():
 | 
				
			||||||
| 
						 | 
					@ -383,18 +448,38 @@ def test_dispose_background_transparency():
 | 
				
			||||||
        assert px[35, 30][3] == 0
 | 
					        assert px[35, 30][3] == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_transparent_dispose():
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
    expected_colors = [
 | 
					    "loading_strategy, expected_colors",
 | 
				
			||||||
        (2, 1, 2),
 | 
					    (
 | 
				
			||||||
        ((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
 | 
					        (
 | 
				
			||||||
        ((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
 | 
					            GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
 | 
				
			||||||
    ]
 | 
					            (
 | 
				
			||||||
    with Image.open("Tests/images/transparent_dispose.gif") as img:
 | 
					                (2, 1, 2),
 | 
				
			||||||
        for frame in range(3):
 | 
					                ((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)),
 | 
				
			||||||
            img.seek(frame)
 | 
					                ((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)),
 | 
				
			||||||
            for x in range(3):
 | 
					            ),
 | 
				
			||||||
                color = img.getpixel((x, 0))
 | 
					        ),
 | 
				
			||||||
                assert color == expected_colors[frame][x]
 | 
					        (
 | 
				
			||||||
 | 
					            GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                (2, 1, 2),
 | 
				
			||||||
 | 
					                (0, 1, 0),
 | 
				
			||||||
 | 
					                (2, 1, 2),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_transparent_dispose(loading_strategy, expected_colors):
 | 
				
			||||||
 | 
					    GifImagePlugin.LOADING_STRATEGY = loading_strategy
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/transparent_dispose.gif") as img:
 | 
				
			||||||
 | 
					            for frame in range(3):
 | 
				
			||||||
 | 
					                img.seek(frame)
 | 
				
			||||||
 | 
					                for x in range(3):
 | 
				
			||||||
 | 
					                    color = img.getpixel((x, 0))
 | 
				
			||||||
 | 
					                    assert color == expected_colors[frame][x]
 | 
				
			||||||
 | 
					    finally:
 | 
				
			||||||
 | 
					        GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_dispose_previous():
 | 
					def test_dispose_previous():
 | 
				
			||||||
| 
						 | 
					@ -554,7 +639,8 @@ def test_dispose2_background(tmp_path):
 | 
				
			||||||
        assert im.getpixel((0, 0)) == (255, 0, 0)
 | 
					        assert im.getpixel((0, 0)) == (255, 0, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_transparency_in_second_frame():
 | 
					def test_transparency_in_second_frame(tmp_path):
 | 
				
			||||||
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
    with Image.open("Tests/images/different_transparency.gif") as im:
 | 
					    with Image.open("Tests/images/different_transparency.gif") as im:
 | 
				
			||||||
        assert im.info["transparency"] == 0
 | 
					        assert im.info["transparency"] == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -564,6 +650,14 @@ def test_transparency_in_second_frame():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
 | 
					        assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        im.save(out, save_all=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with Image.open(out) as reread:
 | 
				
			||||||
 | 
					        reread.seek(reread.tell() + 1)
 | 
				
			||||||
 | 
					        assert_image_equal_tofile(
 | 
				
			||||||
 | 
					            reread, "Tests/images/different_transparency_merged.png"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_no_transparency_in_second_frame():
 | 
					def test_no_transparency_in_second_frame():
 | 
				
			||||||
    with Image.open("Tests/images/iss634.gif") as img:
 | 
					    with Image.open("Tests/images/iss634.gif") as img:
 | 
				
			||||||
| 
						 | 
					@ -575,6 +669,22 @@ def test_no_transparency_in_second_frame():
 | 
				
			||||||
        assert img.histogram()[255] == 0
 | 
					        assert img.histogram()[255] == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_remapped_transparency(tmp_path):
 | 
				
			||||||
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    im = Image.new("P", (1, 2))
 | 
				
			||||||
 | 
					    im2 = im.copy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Add transparency at a higher index
 | 
				
			||||||
 | 
					    # so that it will be optimized to a lower index
 | 
				
			||||||
 | 
					    im.putpixel((0, 1), 5)
 | 
				
			||||||
 | 
					    im.info["transparency"] = 5
 | 
				
			||||||
 | 
					    im.save(out, save_all=True, append_images=[im2])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with Image.open(out) as reloaded:
 | 
				
			||||||
 | 
					        assert reloaded.info["transparency"] == reloaded.getpixel((0, 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_duration(tmp_path):
 | 
					def test_duration(tmp_path):
 | 
				
			||||||
    duration = 1000
 | 
					    duration = 1000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -626,6 +736,23 @@ def test_multiple_duration(tmp_path):
 | 
				
			||||||
                pass
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_roundtrip_info_duration(tmp_path):
 | 
				
			||||||
 | 
					    duration_list = [100, 500, 500]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/transparent_dispose.gif") as im:
 | 
				
			||||||
 | 
					        assert [
 | 
				
			||||||
 | 
					            frame.info["duration"] for frame in ImageSequence.Iterator(im)
 | 
				
			||||||
 | 
					        ] == duration_list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        im.save(out, save_all=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with Image.open(out) as reloaded:
 | 
				
			||||||
 | 
					        assert [
 | 
				
			||||||
 | 
					            frame.info["duration"] for frame in ImageSequence.Iterator(reloaded)
 | 
				
			||||||
 | 
					        ] == duration_list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_identical_frames(tmp_path):
 | 
					def test_identical_frames(tmp_path):
 | 
				
			||||||
    duration_list = [1000, 1500, 2000, 4000]
 | 
					    duration_list = [1000, 1500, 2000, 4000]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -677,9 +804,16 @@ def test_number_of_loops(tmp_path):
 | 
				
			||||||
    im = Image.new("L", (100, 100), "#000")
 | 
					    im = Image.new("L", (100, 100), "#000")
 | 
				
			||||||
    im.save(out, loop=number_of_loops)
 | 
					    im.save(out, loop=number_of_loops)
 | 
				
			||||||
    with Image.open(out) as reread:
 | 
					    with Image.open(out) as reread:
 | 
				
			||||||
 | 
					 | 
				
			||||||
        assert reread.info["loop"] == number_of_loops
 | 
					        assert reread.info["loop"] == number_of_loops
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check that even if a subsequent GIF frame has the number of loops specified,
 | 
				
			||||||
 | 
					    # only the value from the first frame is used
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/duplicate_number_of_loops.gif") as im:
 | 
				
			||||||
 | 
					        assert im.info["loop"] == 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        im.seek(1)
 | 
				
			||||||
 | 
					        assert im.info["loop"] == 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_background(tmp_path):
 | 
					def test_background(tmp_path):
 | 
				
			||||||
    out = str(tmp_path / "temp.gif")
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
| 
						 | 
					@ -712,6 +846,9 @@ def test_comment(tmp_path):
 | 
				
			||||||
    with Image.open(out) as reread:
 | 
					    with Image.open(out) as reread:
 | 
				
			||||||
        assert reread.info["comment"] == im.info["comment"].encode()
 | 
					        assert reread.info["comment"] == im.info["comment"].encode()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test that GIF89a is used for comments
 | 
				
			||||||
 | 
					        assert reread.info["version"] == b"GIF89a"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_comment_over_255(tmp_path):
 | 
					def test_comment_over_255(tmp_path):
 | 
				
			||||||
    out = str(tmp_path / "temp.gif")
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
| 
						 | 
					@ -722,43 +859,95 @@ def test_comment_over_255(tmp_path):
 | 
				
			||||||
    im.info["comment"] = comment
 | 
					    im.info["comment"] = comment
 | 
				
			||||||
    im.save(out)
 | 
					    im.save(out)
 | 
				
			||||||
    with Image.open(out) as reread:
 | 
					    with Image.open(out) as reread:
 | 
				
			||||||
 | 
					 | 
				
			||||||
        assert reread.info["comment"] == comment
 | 
					        assert reread.info["comment"] == comment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test that GIF89a is used for comments
 | 
				
			||||||
 | 
					        assert reread.info["version"] == b"GIF89a"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_zero_comment_subblocks():
 | 
					def test_zero_comment_subblocks():
 | 
				
			||||||
    with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im:
 | 
					    with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im:
 | 
				
			||||||
        assert_image_equal_tofile(im, TEST_GIF)
 | 
					        assert_image_equal_tofile(im, TEST_GIF)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_read_multiple_comment_blocks():
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/multiple_comments.gif") as im:
 | 
				
			||||||
 | 
					        # Multiple comment blocks in a frame are separated not concatenated
 | 
				
			||||||
 | 
					        assert im.info["comment"] == b"Test comment 1\nTest comment 2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_empty_string_comment(tmp_path):
 | 
				
			||||||
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/chi.gif") as im:
 | 
				
			||||||
 | 
					        assert "comment" in im.info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Empty string comment should suppress existing comment
 | 
				
			||||||
 | 
					        im.save(out, save_all=True, comment="")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with Image.open(out) as reread:
 | 
				
			||||||
 | 
					        for frame in ImageSequence.Iterator(reread):
 | 
				
			||||||
 | 
					            assert "comment" not in frame.info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_retain_comment_in_subsequent_frames(tmp_path):
 | 
				
			||||||
 | 
					    # Test that a comment block at the beginning is kept
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/chi.gif") as im:
 | 
				
			||||||
 | 
					        for frame in ImageSequence.Iterator(im):
 | 
				
			||||||
 | 
					            assert frame.info["comment"] == b"Created with GIMP"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/second_frame_comment.gif") as im:
 | 
				
			||||||
 | 
					        assert "comment" not in im.info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test that a comment in the middle is read
 | 
				
			||||||
 | 
					        im.seek(1)
 | 
				
			||||||
 | 
					        assert im.info["comment"] == b"Comment in the second frame"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test that it is still present in a later frame
 | 
				
			||||||
 | 
					        im.seek(2)
 | 
				
			||||||
 | 
					        assert im.info["comment"] == b"Comment in the second frame"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test that rewinding removes the comment
 | 
				
			||||||
 | 
					        im.seek(0)
 | 
				
			||||||
 | 
					        assert "comment" not in im.info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test that a saved image keeps the comment
 | 
				
			||||||
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/dispose_prev.gif") as im:
 | 
				
			||||||
 | 
					        im.save(out, save_all=True, comment="Test")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with Image.open(out) as reread:
 | 
				
			||||||
 | 
					        for frame in ImageSequence.Iterator(reread):
 | 
				
			||||||
 | 
					            assert frame.info["comment"] == b"Test"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_version(tmp_path):
 | 
					def test_version(tmp_path):
 | 
				
			||||||
    out = str(tmp_path / "temp.gif")
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def assertVersionAfterSave(im, version):
 | 
					    def assert_version_after_save(im, version):
 | 
				
			||||||
        im.save(out)
 | 
					        im.save(out)
 | 
				
			||||||
        with Image.open(out) as reread:
 | 
					        with Image.open(out) as reread:
 | 
				
			||||||
            assert reread.info["version"] == version
 | 
					            assert reread.info["version"] == version
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Test that GIF87a is used by default
 | 
					    # Test that GIF87a is used by default
 | 
				
			||||||
    im = Image.new("L", (100, 100), "#000")
 | 
					    im = Image.new("L", (100, 100), "#000")
 | 
				
			||||||
    assertVersionAfterSave(im, b"GIF87a")
 | 
					    assert_version_after_save(im, b"GIF87a")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Test setting the version to 89a
 | 
					    # Test setting the version to 89a
 | 
				
			||||||
    im = Image.new("L", (100, 100), "#000")
 | 
					    im = Image.new("L", (100, 100), "#000")
 | 
				
			||||||
    im.info["version"] = b"89a"
 | 
					    im.info["version"] = b"89a"
 | 
				
			||||||
    assertVersionAfterSave(im, b"GIF89a")
 | 
					    assert_version_after_save(im, b"GIF89a")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Test that adding a GIF89a feature changes the version
 | 
					    # Test that adding a GIF89a feature changes the version
 | 
				
			||||||
    im.info["transparency"] = 1
 | 
					    im.info["transparency"] = 1
 | 
				
			||||||
    assertVersionAfterSave(im, b"GIF89a")
 | 
					    assert_version_after_save(im, b"GIF89a")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Test that a GIF87a image is also saved in that format
 | 
					    # Test that a GIF87a image is also saved in that format
 | 
				
			||||||
    with Image.open("Tests/images/test.colors.gif") as im:
 | 
					    with Image.open("Tests/images/test.colors.gif") as im:
 | 
				
			||||||
        assertVersionAfterSave(im, b"GIF87a")
 | 
					        assert_version_after_save(im, b"GIF87a")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Test that a GIF89a image is also saved in that format
 | 
					        # Test that a GIF89a image is also saved in that format
 | 
				
			||||||
        im.info["version"] = b"GIF89a"
 | 
					        im.info["version"] = b"GIF89a"
 | 
				
			||||||
        assertVersionAfterSave(im, b"GIF87a")
 | 
					        assert_version_after_save(im, b"GIF87a")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_append_images(tmp_path):
 | 
					def test_append_images(tmp_path):
 | 
				
			||||||
| 
						 | 
					@ -773,10 +962,10 @@ def test_append_images(tmp_path):
 | 
				
			||||||
        assert reread.n_frames == 3
 | 
					        assert reread.n_frames == 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Tests appending using a generator
 | 
					    # Tests appending using a generator
 | 
				
			||||||
    def imGenerator(ims):
 | 
					    def im_generator(ims):
 | 
				
			||||||
        yield from ims
 | 
					        yield from ims
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    im.save(out, save_all=True, append_images=imGenerator(ims))
 | 
					    im.save(out, save_all=True, append_images=im_generator(ims))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with Image.open(out) as reread:
 | 
					    with Image.open(out) as reread:
 | 
				
			||||||
        assert reread.n_frames == 3
 | 
					        assert reread.n_frames == 3
 | 
				
			||||||
| 
						 | 
					@ -831,6 +1020,17 @@ def test_rgb_transparency(tmp_path):
 | 
				
			||||||
        assert "transparency" not in reloaded.info
 | 
					        assert "transparency" not in reloaded.info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_rgba_transparency(tmp_path):
 | 
				
			||||||
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    im = hopper("P")
 | 
				
			||||||
 | 
					    im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with Image.open(out) as reloaded:
 | 
				
			||||||
 | 
					        reloaded.seek(1)
 | 
				
			||||||
 | 
					        assert_image_equal(hopper("P").convert("RGB"), reloaded)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_bbox(tmp_path):
 | 
					def test_bbox(tmp_path):
 | 
				
			||||||
    out = str(tmp_path / "temp.gif")
 | 
					    out = str(tmp_path / "temp.gif")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -960,6 +1160,11 @@ def test_lzw_bits():
 | 
				
			||||||
def test_extents():
 | 
					def test_extents():
 | 
				
			||||||
    with Image.open("Tests/images/test_extents.gif") as im:
 | 
					    with Image.open("Tests/images/test_extents.gif") as im:
 | 
				
			||||||
        assert im.size == (100, 100)
 | 
					        assert im.size == (100, 100)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Check that n_frames does not change the size
 | 
				
			||||||
 | 
					        assert im.n_frames == 2
 | 
				
			||||||
 | 
					        assert im.size == (100, 100)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        im.seek(1)
 | 
					        im.seek(1)
 | 
				
			||||||
        assert im.size == (150, 150)
 | 
					        assert im.size == (150, 150)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,15 +4,13 @@ import warnings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import IcnsImagePlugin, Image, _binary, features
 | 
					from PIL import IcnsImagePlugin, Image, _binary
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .helper import assert_image_equal, assert_image_similar_tofile
 | 
					from .helper import assert_image_equal, assert_image_similar_tofile, skip_unless_feature
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# sample icon file
 | 
					# sample icon file
 | 
				
			||||||
TEST_FILE = "Tests/images/pillow.icns"
 | 
					TEST_FILE = "Tests/images/pillow.icns"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENABLE_JPEG2K = features.check_codec("jpg_2000")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_sanity():
 | 
					def test_sanity():
 | 
				
			||||||
    # Loading this icon by default should result in the largest size
 | 
					    # Loading this icon by default should result in the largest size
 | 
				
			||||||
| 
						 | 
					@ -111,14 +109,12 @@ def test_older_icon():
 | 
				
			||||||
                assert im2.size == (wr, hr)
 | 
					                assert im2.size == (wr, hr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@skip_unless_feature("jpg_2000")
 | 
				
			||||||
def test_jp2_icon():
 | 
					def test_jp2_icon():
 | 
				
			||||||
    # This icon uses JPEG 2000 images instead of the PNG images.
 | 
					    # This icon uses JPEG 2000 images instead of the PNG images.
 | 
				
			||||||
    # The advantage of doing this is that OS X 10.5 supports JPEG 2000
 | 
					    # The advantage of doing this is that OS X 10.5 supports JPEG 2000
 | 
				
			||||||
    # but not PNG; some commercial software therefore does just this.
 | 
					    # but not PNG; some commercial software therefore does just this.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if not ENABLE_JPEG2K:
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    with Image.open("Tests/images/pillow3.icns") as im:
 | 
					    with Image.open("Tests/images/pillow3.icns") as im:
 | 
				
			||||||
        for w, h, r in im.info["sizes"]:
 | 
					        for w, h, r in im.info["sizes"]:
 | 
				
			||||||
            wr = w * r
 | 
					            wr = w * r
 | 
				
			||||||
| 
						 | 
					@ -149,6 +145,7 @@ def test_not_an_icns_file():
 | 
				
			||||||
            IcnsImagePlugin.IcnsFile(fp)
 | 
					            IcnsImagePlugin.IcnsFile(fp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@skip_unless_feature("jpg_2000")
 | 
				
			||||||
def test_icns_decompression_bomb():
 | 
					def test_icns_decompression_bomb():
 | 
				
			||||||
    with Image.open(
 | 
					    with Image.open(
 | 
				
			||||||
        "Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns"
 | 
					        "Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import io
 | 
					import io
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,6 +71,53 @@ def test_save_to_bytes():
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_no_duplicates(tmp_path):
 | 
				
			||||||
 | 
					    temp_file = str(tmp_path / "temp.ico")
 | 
				
			||||||
 | 
					    temp_file2 = str(tmp_path / "temp2.ico")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    im = hopper()
 | 
				
			||||||
 | 
					    sizes = [(32, 32), (64, 64)]
 | 
				
			||||||
 | 
					    im.save(temp_file, "ico", sizes=sizes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sizes.append(sizes[-1])
 | 
				
			||||||
 | 
					    im.save(temp_file2, "ico", sizes=sizes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert os.path.getsize(temp_file) == os.path.getsize(temp_file2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_different_bit_depths(tmp_path):
 | 
				
			||||||
 | 
					    temp_file = str(tmp_path / "temp.ico")
 | 
				
			||||||
 | 
					    temp_file2 = str(tmp_path / "temp2.ico")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    im = hopper()
 | 
				
			||||||
 | 
					    im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    hopper("1").save(
 | 
				
			||||||
 | 
					        temp_file2,
 | 
				
			||||||
 | 
					        "ico",
 | 
				
			||||||
 | 
					        bitmap_format="bmp",
 | 
				
			||||||
 | 
					        sizes=[(128, 128)],
 | 
				
			||||||
 | 
					        append_images=[im],
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert os.path.getsize(temp_file) != os.path.getsize(temp_file2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test that only matching sizes of different bit depths are saved
 | 
				
			||||||
 | 
					    temp_file3 = str(tmp_path / "temp3.ico")
 | 
				
			||||||
 | 
					    temp_file4 = str(tmp_path / "temp4.ico")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)])
 | 
				
			||||||
 | 
					    im.save(
 | 
				
			||||||
 | 
					        temp_file4,
 | 
				
			||||||
 | 
					        "ico",
 | 
				
			||||||
 | 
					        bitmap_format="bmp",
 | 
				
			||||||
 | 
					        sizes=[(128, 128)],
 | 
				
			||||||
 | 
					        append_images=[Image.new("P", (64, 64))],
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert os.path.getsize(temp_file3) == os.path.getsize(temp_file4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
 | 
					@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
 | 
				
			||||||
def test_save_to_bytes_bmp(mode):
 | 
					def test_save_to_bytes_bmp(mode):
 | 
				
			||||||
    output = io.BytesIO()
 | 
					    output = io.BytesIO()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,6 +68,13 @@ class TestFileJpeg:
 | 
				
			||||||
            assert im.format == "JPEG"
 | 
					            assert im.format == "JPEG"
 | 
				
			||||||
            assert im.get_format_mimetype() == "image/jpeg"
 | 
					            assert im.get_format_mimetype() == "image/jpeg"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
 | 
				
			||||||
 | 
					    def test_zero(self, size, tmp_path):
 | 
				
			||||||
 | 
					        f = str(tmp_path / "temp.jpg")
 | 
				
			||||||
 | 
					        im = Image.new("RGB", size)
 | 
				
			||||||
 | 
					        with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					            im.save(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_app(self):
 | 
					    def test_app(self):
 | 
				
			||||||
        # Test APP/COM reader (@PIL135)
 | 
					        # Test APP/COM reader (@PIL135)
 | 
				
			||||||
        with Image.open(TEST_FILE) as im:
 | 
					        with Image.open(TEST_FILE) as im:
 | 
				
			||||||
| 
						 | 
					@ -736,7 +743,7 @@ class TestFileJpeg:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Act / Assert
 | 
					            # Act / Assert
 | 
				
			||||||
            # "When the image resolution is unknown, 72 [dpi] is designated."
 | 
					            # "When the image resolution is unknown, 72 [dpi] is designated."
 | 
				
			||||||
            # http://www.exiv2.org/tags.html
 | 
					            # https://exiv2.org/tags.html
 | 
				
			||||||
            assert im.info.get("dpi") == (72, 72)
 | 
					            assert im.info.get("dpi") == (72, 72)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_invalid_exif(self):
 | 
					    def test_invalid_exif(self):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -209,6 +209,49 @@ def test_layers():
 | 
				
			||||||
        assert_image_similar(im, test_card, 0.4)
 | 
					        assert_image_similar(im, test_card, 0.4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
 | 
					    "name, args, offset, data",
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        ("foo.j2k", {}, 0, b"\xff\x4f"),
 | 
				
			||||||
 | 
					        ("foo.jp2", {}, 4, b"jP"),
 | 
				
			||||||
 | 
					        (None, {"no_jp2": True}, 0, b"\xff\x4f"),
 | 
				
			||||||
 | 
					        ("foo.j2k", {"no_jp2": True}, 0, b"\xff\x4f"),
 | 
				
			||||||
 | 
					        ("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"),
 | 
				
			||||||
 | 
					        ("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"),
 | 
				
			||||||
 | 
					        ("foo.jp2", {"no_jp2": False}, 4, b"jP"),
 | 
				
			||||||
 | 
					        ("foo.jp2", {"no_jp2": False}, 4, b"jP"),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_no_jp2(name, args, offset, data):
 | 
				
			||||||
 | 
					    out = BytesIO()
 | 
				
			||||||
 | 
					    if name:
 | 
				
			||||||
 | 
					        out.name = name
 | 
				
			||||||
 | 
					    test_card.save(out, "JPEG2000", **args)
 | 
				
			||||||
 | 
					    out.seek(offset)
 | 
				
			||||||
 | 
					    assert out.read(2) == data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_mct():
 | 
				
			||||||
 | 
					    # Three component
 | 
				
			||||||
 | 
					    for val in (0, 1):
 | 
				
			||||||
 | 
					        out = BytesIO()
 | 
				
			||||||
 | 
					        test_card.save(out, "JPEG2000", mct=val, no_jp2=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert out.getvalue()[59] == val
 | 
				
			||||||
 | 
					        with Image.open(out) as im:
 | 
				
			||||||
 | 
					            assert_image_similar(im, test_card, 1.0e-3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Single component should have MCT disabled
 | 
				
			||||||
 | 
					    for val in (0, 1):
 | 
				
			||||||
 | 
					        out = BytesIO()
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/16bit.cropped.jp2") as jp2:
 | 
				
			||||||
 | 
					            jp2.save(out, "JPEG2000", mct=val, no_jp2=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert out.getvalue()[53] == 0
 | 
				
			||||||
 | 
					        with Image.open(out) as im:
 | 
				
			||||||
 | 
					            assert_image_similar(im, jp2, 1.0e-3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_rgba():
 | 
					def test_rgba():
 | 
				
			||||||
    # Arrange
 | 
					    # Arrange
 | 
				
			||||||
    with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
 | 
					    with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k:
 | 
				
			||||||
| 
						 | 
					@ -255,6 +298,11 @@ def test_16bit_jp2_roundtrips():
 | 
				
			||||||
        assert_image_equal(im, jp2)
 | 
					        assert_image_equal(im, jp2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_issue_6194():
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/issue_6194.j2k") as im:
 | 
				
			||||||
 | 
					        assert im.getpixel((5, 5)) == 31
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_unbound_local():
 | 
					def test_unbound_local():
 | 
				
			||||||
    # prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
 | 
					    # prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
 | 
				
			||||||
    with pytest.raises(OSError):
 | 
					    with pytest.raises(OSError):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,6 @@ import itertools
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
from collections import namedtuple
 | 
					from collections import namedtuple
 | 
				
			||||||
from ctypes import c_float
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -168,14 +167,11 @@ class TestFileLibTiff(LibTiffTestCase):
 | 
				
			||||||
                    val = original[tag]
 | 
					                    val = original[tag]
 | 
				
			||||||
                    if tag.endswith("Resolution"):
 | 
					                    if tag.endswith("Resolution"):
 | 
				
			||||||
                        if legacy_api:
 | 
					                        if legacy_api:
 | 
				
			||||||
                            assert (
 | 
					                            assert val[0][0] / val[0][1] == (
 | 
				
			||||||
                                c_float(val[0][0] / val[0][1]).value
 | 
					                                4294967295 / 113653537
 | 
				
			||||||
                                == c_float(value[0][0] / value[0][1]).value
 | 
					 | 
				
			||||||
                            ), f"{tag} didn't roundtrip"
 | 
					                            ), f"{tag} didn't roundtrip"
 | 
				
			||||||
                        else:
 | 
					                        else:
 | 
				
			||||||
                            assert (
 | 
					                            assert val == 37.79000115940079, f"{tag} didn't roundtrip"
 | 
				
			||||||
                                c_float(val).value == c_float(value).value
 | 
					 | 
				
			||||||
                            ), f"{tag} didn't roundtrip"
 | 
					 | 
				
			||||||
                    else:
 | 
					                    else:
 | 
				
			||||||
                        assert val == value, f"{tag} didn't roundtrip"
 | 
					                        assert val == value, f"{tag} didn't roundtrip"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -501,8 +497,8 @@ class TestFileLibTiff(LibTiffTestCase):
 | 
				
			||||||
        im.save(out, compression="tiff_adobe_deflate")
 | 
					        im.save(out, compression="tiff_adobe_deflate")
 | 
				
			||||||
        assert_image_equal_tofile(im, out)
 | 
					        assert_image_equal_tofile(im, out)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_palette_save(self, tmp_path):
 | 
					    @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000")))
 | 
				
			||||||
        im = hopper("P")
 | 
					    def test_palette_save(self, im, tmp_path):
 | 
				
			||||||
        out = str(tmp_path / "temp.tif")
 | 
					        out = str(tmp_path / "temp.tif")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        TiffImagePlugin.WRITE_LIBTIFF = True
 | 
					        TiffImagePlugin.WRITE_LIBTIFF = True
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,6 +48,14 @@ def test_closed_file():
 | 
				
			||||||
        im.close()
 | 
					        im.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_seek_after_close():
 | 
				
			||||||
 | 
					    im = Image.open(test_files[0])
 | 
				
			||||||
 | 
					    im.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					        im.seek(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_context_manager():
 | 
					def test_context_manager():
 | 
				
			||||||
    with warnings.catch_warnings():
 | 
					    with warnings.catch_warnings():
 | 
				
			||||||
        with Image.open(test_files[0]) as im:
 | 
					        with Image.open(test_files[0]) as im:
 | 
				
			||||||
| 
						 | 
					@ -116,6 +124,15 @@ def test_parallax():
 | 
				
			||||||
        assert exif.get_ifd(0x927C)[0xB211] == -3.125
 | 
					        assert exif.get_ifd(0x927C)[0xB211] == -3.125
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_reload_exif_after_seek():
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/sugarshack.mpo") as im:
 | 
				
			||||||
 | 
					        exif = im.getexif()
 | 
				
			||||||
 | 
					        del exif[296]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        im.seek(1)
 | 
				
			||||||
 | 
					        assert 296 in exif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_mp():
 | 
					def test_mp():
 | 
				
			||||||
    for test_file in test_files:
 | 
					    for test_file in test_files:
 | 
				
			||||||
        with Image.open(test_file) as im:
 | 
					        with Image.open(test_file) as im:
 | 
				
			||||||
| 
						 | 
					@ -145,10 +162,10 @@ def test_mp_attribute():
 | 
				
			||||||
    for test_file in test_files:
 | 
					    for test_file in test_files:
 | 
				
			||||||
        with Image.open(test_file) as im:
 | 
					        with Image.open(test_file) as im:
 | 
				
			||||||
            mpinfo = im._getmp()
 | 
					            mpinfo = im._getmp()
 | 
				
			||||||
        frameNumber = 0
 | 
					        frame_number = 0
 | 
				
			||||||
        for mpentry in mpinfo[0xB002]:
 | 
					        for mpentry in mpinfo[0xB002]:
 | 
				
			||||||
            mpattr = mpentry["Attribute"]
 | 
					            mpattr = mpentry["Attribute"]
 | 
				
			||||||
            if frameNumber:
 | 
					            if frame_number:
 | 
				
			||||||
                assert not mpattr["RepresentativeImageFlag"]
 | 
					                assert not mpattr["RepresentativeImageFlag"]
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                assert mpattr["RepresentativeImageFlag"]
 | 
					                assert mpattr["RepresentativeImageFlag"]
 | 
				
			||||||
| 
						 | 
					@ -157,7 +174,7 @@ def test_mp_attribute():
 | 
				
			||||||
            assert mpattr["ImageDataFormat"] == "JPEG"
 | 
					            assert mpattr["ImageDataFormat"] == "JPEG"
 | 
				
			||||||
            assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)"
 | 
					            assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)"
 | 
				
			||||||
            assert mpattr["Reserved"] == 0
 | 
					            assert mpattr["Reserved"] == 0
 | 
				
			||||||
            frameNumber += 1
 | 
					            frame_number += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_seek():
 | 
					def test_seek():
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -131,10 +131,10 @@ def test_save_all(tmp_path):
 | 
				
			||||||
        assert os.path.getsize(outfile) > 0
 | 
					        assert os.path.getsize(outfile) > 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Test appending using a generator
 | 
					        # Test appending using a generator
 | 
				
			||||||
        def imGenerator(ims):
 | 
					        def im_generator(ims):
 | 
				
			||||||
            yield from ims
 | 
					            yield from ims
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        im.save(outfile, save_all=True, append_images=imGenerator(ims))
 | 
					        im.save(outfile, save_all=True, append_images=im_generator(ims))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assert os.path.isfile(outfile)
 | 
					    assert os.path.isfile(outfile)
 | 
				
			||||||
    assert os.path.getsize(outfile) > 0
 | 
					    assert os.path.getsize(outfile) > 0
 | 
				
			||||||
| 
						 | 
					@ -253,9 +253,9 @@ def test_pdf_append(tmp_path):
 | 
				
			||||||
        check_pdf_pages_consistency(pdf)
 | 
					        check_pdf_pages_consistency(pdf)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # append two images
 | 
					    # append two images
 | 
				
			||||||
    mode_CMYK = hopper("CMYK")
 | 
					    mode_cmyk = hopper("CMYK")
 | 
				
			||||||
    mode_P = hopper("P")
 | 
					    mode_p = hopper("P")
 | 
				
			||||||
    mode_CMYK.save(pdf_filename, append=True, save_all=True, append_images=[mode_P])
 | 
					    mode_cmyk.save(pdf_filename, append=True, save_all=True, append_images=[mode_p])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # open the PDF again, check pages and info again
 | 
					    # open the PDF again, check pages and info again
 | 
				
			||||||
    with PdfParser.PdfParser(pdf_filename) as pdf:
 | 
					    with PdfParser.PdfParser(pdf_filename) as pdf:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -635,6 +635,17 @@ class TestFilePng:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
 | 
					            assert_image_equal_tofile(im, "Tests/images/bw_gradient.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @pytest.mark.parametrize("cid", (b"IHDR", b"pHYs", b"acTL", b"fcTL", b"fdAT"))
 | 
				
			||||||
 | 
					    def test_truncated_chunks(self, cid):
 | 
				
			||||||
 | 
					        fp = BytesIO()
 | 
				
			||||||
 | 
					        with PngImagePlugin.PngStream(fp) as png:
 | 
				
			||||||
 | 
					            with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					                png.call(cid, 0, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ImageFile.LOAD_TRUNCATED_IMAGES = True
 | 
				
			||||||
 | 
					            png.call(cid, 0, 0)
 | 
				
			||||||
 | 
					            ImageFile.LOAD_TRUNCATED_IMAGES = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_specify_bits(self, tmp_path):
 | 
					    def test_specify_bits(self, tmp_path):
 | 
				
			||||||
        im = hopper("P")
 | 
					        im = hopper("P")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,16 +13,53 @@ TEST_FILE = "Tests/images/hopper.ppm"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_sanity():
 | 
					def test_sanity():
 | 
				
			||||||
    with Image.open(TEST_FILE) as im:
 | 
					    with Image.open(TEST_FILE) as im:
 | 
				
			||||||
        im.load()
 | 
					 | 
				
			||||||
        assert im.mode == "RGB"
 | 
					        assert im.mode == "RGB"
 | 
				
			||||||
        assert im.size == (128, 128)
 | 
					        assert im.size == (128, 128)
 | 
				
			||||||
        assert im.format, "PPM"
 | 
					        assert im.format == "PPM"
 | 
				
			||||||
        assert im.get_format_mimetype() == "image/x-portable-pixmap"
 | 
					        assert im.get_format_mimetype() == "image/x-portable-pixmap"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
 | 
					    "data, mode, pixels",
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        (b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)),
 | 
				
			||||||
 | 
					        (b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
 | 
				
			||||||
 | 
					        # P6 with maxval < 255
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11",
 | 
				
			||||||
 | 
					            "RGB",
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                (0, 15, 30),
 | 
				
			||||||
 | 
					                (120, 135, 150),
 | 
				
			||||||
 | 
					                (225, 240, 255),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        # P6 with maxval > 255
 | 
				
			||||||
 | 
					        # Scale down to 255, since there is no RGB mode with more than 8-bit
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
 | 
				
			||||||
 | 
					            b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
 | 
				
			||||||
 | 
					            "RGB",
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                (0, 1, 2),
 | 
				
			||||||
 | 
					                (127, 128, 129),
 | 
				
			||||||
 | 
					                (254, 255, 255),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def test_arbitrary_maxval(data, mode, pixels):
 | 
				
			||||||
 | 
					    fp = BytesIO(data)
 | 
				
			||||||
 | 
					    with Image.open(fp) as im:
 | 
				
			||||||
 | 
					        assert im.size == (3, 1)
 | 
				
			||||||
 | 
					        assert im.mode == mode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        px = im.load()
 | 
				
			||||||
 | 
					        assert tuple(px[x, 0] for x in range(3)) == pixels
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_16bit_pgm():
 | 
					def test_16bit_pgm():
 | 
				
			||||||
    with Image.open("Tests/images/16_bit_binary.pgm") as im:
 | 
					    with Image.open("Tests/images/16_bit_binary.pgm") as im:
 | 
				
			||||||
        im.load()
 | 
					 | 
				
			||||||
        assert im.mode == "I"
 | 
					        assert im.mode == "I"
 | 
				
			||||||
        assert im.size == (20, 100)
 | 
					        assert im.size == (20, 100)
 | 
				
			||||||
        assert im.get_format_mimetype() == "image/x-portable-graymap"
 | 
					        assert im.get_format_mimetype() == "image/x-portable-graymap"
 | 
				
			||||||
| 
						 | 
					@ -32,8 +69,6 @@ def test_16bit_pgm():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_16bit_pgm_write(tmp_path):
 | 
					def test_16bit_pgm_write(tmp_path):
 | 
				
			||||||
    with Image.open("Tests/images/16_bit_binary.pgm") as im:
 | 
					    with Image.open("Tests/images/16_bit_binary.pgm") as im:
 | 
				
			||||||
        im.load()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        f = str(tmp_path / "temp.pgm")
 | 
					        f = str(tmp_path / "temp.pgm")
 | 
				
			||||||
        im.save(f, "PPM")
 | 
					        im.save(f, "PPM")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,17 +117,6 @@ def test_16bit_plain_pgm(tmp_path):
 | 
				
			||||||
        assert_image_equal_tofile(im, "Tests/images/hopper_16bit.pgm")
 | 
					        assert_image_equal_tofile(im, "Tests/images/hopper_16bit.pgm")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_32bit_plain_pgm(tmp_path):
 | 
					 | 
				
			||||||
    # P2 with maxval 2 ** 31 - 1
 | 
					 | 
				
			||||||
    with Image.open("Tests/images/hopper_32bit_plain.pgm") as im:
 | 
					 | 
				
			||||||
        assert im.mode == "I"
 | 
					 | 
				
			||||||
        assert im.size == (128, 128)
 | 
					 | 
				
			||||||
        assert im.get_format_mimetype() == "image/x-portable-graymap"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # P5 with maxval 2 ** 31 - 1
 | 
					 | 
				
			||||||
        assert_image_equal_tofile(im, "Tests/images/hopper_32bit.pgm")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def test_plain_pbm_data_with_comments(tmp_path):
 | 
					def test_plain_pbm_data_with_comments(tmp_path):
 | 
				
			||||||
    path1 = str(tmp_path / "temp1.ppm")
 | 
					    path1 = str(tmp_path / "temp1.ppm")
 | 
				
			||||||
    path2 = str(tmp_path / "temp2.ppm")
 | 
					    path2 = str(tmp_path / "temp2.ppm")
 | 
				
			||||||
| 
						 | 
					@ -222,22 +246,11 @@ def test_header_token_too_long(tmp_path):
 | 
				
			||||||
        with Image.open(path):
 | 
					        with Image.open(path):
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assert str(e.value) == "Token too long in file header: b'01234567890'"
 | 
					    assert str(e.value) == "Token too long in file header: 01234567890"
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def test_too_many_colors(tmp_path):
 | 
					 | 
				
			||||||
    path = str(tmp_path / "temp.ppm")
 | 
					 | 
				
			||||||
    with open(path, "wb") as f:
 | 
					 | 
				
			||||||
        f.write(b"P6\n1 1\n1000\n")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    with pytest.raises(ValueError) as e:
 | 
					 | 
				
			||||||
        with Image.open(path):
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assert str(e.value) == "Too many colors for band: 1000"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_truncated_header(tmp_path):
 | 
					def test_truncated_header(tmp_path):
 | 
				
			||||||
 | 
					    # Test EOF in header
 | 
				
			||||||
    path = str(tmp_path / "temp.pgm")
 | 
					    path = str(tmp_path / "temp.pgm")
 | 
				
			||||||
    with open(path, "w") as f:
 | 
					    with open(path, "w") as f:
 | 
				
			||||||
        f.write("P6")
 | 
					        f.write("P6")
 | 
				
			||||||
| 
						 | 
					@ -248,6 +261,25 @@ def test_truncated_header(tmp_path):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assert str(e.value) == "Reached EOF while reading header"
 | 
					    assert str(e.value) == "Reached EOF while reading header"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test EOF for PyDecoder
 | 
				
			||||||
 | 
					    fp = BytesIO(b"P5 3 1 4")
 | 
				
			||||||
 | 
					    with Image.open(fp) as im:
 | 
				
			||||||
 | 
					        with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					            im.load()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.mark.parametrize("maxval", (0, 65536))
 | 
				
			||||||
 | 
					def test_invalid_maxval(maxval, tmp_path):
 | 
				
			||||||
 | 
					    path = str(tmp_path / "temp.ppm")
 | 
				
			||||||
 | 
					    with open(path, "w") as f:
 | 
				
			||||||
 | 
					        f.write("P6\n3 1 " + str(maxval))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError) as e:
 | 
				
			||||||
 | 
					        with Image.open(path):
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert str(e.value) == "maxval must be greater than 0 and less than 65536"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_neg_ppm():
 | 
					def test_neg_ppm():
 | 
				
			||||||
    # Storage.c accepted negative values for xsize, ysize.  the
 | 
					    # Storage.c accepted negative values for xsize, ysize.  the
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -101,6 +101,10 @@ def test_cross_scan_line():
 | 
				
			||||||
    with Image.open("Tests/images/cross_scan_line.tga") as im:
 | 
					    with Image.open("Tests/images/cross_scan_line.tga") as im:
 | 
				
			||||||
        assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png")
 | 
					        assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/cross_scan_line_truncated.tga") as im:
 | 
				
			||||||
 | 
					        with pytest.raises(OSError):
 | 
				
			||||||
 | 
					            im.load()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_save(tmp_path):
 | 
					def test_save(tmp_path):
 | 
				
			||||||
    test_file = "Tests/images/tga_id_field.tga"
 | 
					    test_file = "Tests/images/tga_id_field.tga"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,6 +70,15 @@ class TestFileTiff:
 | 
				
			||||||
            im.load()
 | 
					            im.load()
 | 
				
			||||||
            im.close()
 | 
					            im.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_seek_after_close(self):
 | 
				
			||||||
 | 
					        im = Image.open("Tests/images/multipage.tiff")
 | 
				
			||||||
 | 
					        im.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					            im.n_frames
 | 
				
			||||||
 | 
					        with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					            im.seek(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_context_manager(self):
 | 
					    def test_context_manager(self):
 | 
				
			||||||
        with warnings.catch_warnings():
 | 
					        with warnings.catch_warnings():
 | 
				
			||||||
            with Image.open("Tests/images/multipage.tiff") as im:
 | 
					            with Image.open("Tests/images/multipage.tiff") as im:
 | 
				
			||||||
| 
						 | 
					@ -87,18 +96,38 @@ class TestFileTiff:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
 | 
					            assert_image_similar_tofile(im, "Tests/images/pil136.png", 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_bigtiff(self):
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/hopper_bigtiff.tif") as im:
 | 
				
			||||||
 | 
					            assert_image_equal_tofile(im, "Tests/images/hopper.tif")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @pytest.mark.parametrize(
 | 
					    @pytest.mark.parametrize(
 | 
				
			||||||
        "file_name,mode,size,offset",
 | 
					        "file_name,mode,size,tile",
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
            ("tiff_wrong_bits_per_sample.tiff", "RGBA", (52, 53), 160),
 | 
					            (
 | 
				
			||||||
            ("tiff_wrong_bits_per_sample_2.tiff", "RGB", (16, 16), 8),
 | 
					                "tiff_wrong_bits_per_sample.tiff",
 | 
				
			||||||
 | 
					                "RGBA",
 | 
				
			||||||
 | 
					                (52, 53),
 | 
				
			||||||
 | 
					                [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                "tiff_wrong_bits_per_sample_2.tiff",
 | 
				
			||||||
 | 
					                "RGB",
 | 
				
			||||||
 | 
					                (16, 16),
 | 
				
			||||||
 | 
					                [("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                "tiff_wrong_bits_per_sample_3.tiff",
 | 
				
			||||||
 | 
					                "RGBA",
 | 
				
			||||||
 | 
					                (512, 256),
 | 
				
			||||||
 | 
					                [("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    def test_wrong_bits_per_sample(self, file_name, mode, size, offset):
 | 
					    def test_wrong_bits_per_sample(self, file_name, mode, size, tile):
 | 
				
			||||||
        with Image.open("Tests/images/" + file_name) as im:
 | 
					        with Image.open("Tests/images/" + file_name) as im:
 | 
				
			||||||
            assert im.mode == mode
 | 
					            assert im.mode == mode
 | 
				
			||||||
            assert im.size == size
 | 
					            assert im.size == size
 | 
				
			||||||
            assert im.tile == [("raw", (0, 0) + size, offset, (mode, 0, 1))]
 | 
					            assert im.tile == tile
 | 
				
			||||||
            im.load()
 | 
					            im.load()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_set_legacy_api(self):
 | 
					    def test_set_legacy_api(self):
 | 
				
			||||||
| 
						 | 
					@ -147,14 +176,14 @@ class TestFileTiff:
 | 
				
			||||||
            assert im.info["dpi"] == (71.0, 71.0)
 | 
					            assert im.info["dpi"] == (71.0, 71.0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @pytest.mark.parametrize(
 | 
					    @pytest.mark.parametrize(
 | 
				
			||||||
        "resolutionUnit, dpi",
 | 
					        "resolution_unit, dpi",
 | 
				
			||||||
        [(None, 72.8), (2, 72.8), (3, 184.912)],
 | 
					        [(None, 72.8), (2, 72.8), (3, 184.912)],
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    def test_load_float_dpi(self, resolutionUnit, dpi):
 | 
					    def test_load_float_dpi(self, resolution_unit, dpi):
 | 
				
			||||||
        with Image.open(
 | 
					        with Image.open(
 | 
				
			||||||
            "Tests/images/hopper_float_dpi_" + str(resolutionUnit) + ".tif"
 | 
					            "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
 | 
				
			||||||
        ) as im:
 | 
					        ) as im:
 | 
				
			||||||
            assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit
 | 
					            assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
 | 
				
			||||||
            assert im.info["dpi"] == (dpi, dpi)
 | 
					            assert im.info["dpi"] == (dpi, dpi)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_save_float_dpi(self, tmp_path):
 | 
					    def test_save_float_dpi(self, tmp_path):
 | 
				
			||||||
| 
						 | 
					@ -221,6 +250,15 @@ class TestFileTiff:
 | 
				
			||||||
        assert b[0] == ord(b"\x01")
 | 
					        assert b[0] == ord(b"\x01")
 | 
				
			||||||
        assert b[1] == ord(b"\xe0")
 | 
					        assert b[1] == ord(b"\xe0")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_16bit_r(self):
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/16bit.r.tif") as im:
 | 
				
			||||||
 | 
					            assert im.getpixel((0, 0)) == 480
 | 
				
			||||||
 | 
					            assert im.mode == "I;16"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            b = im.tobytes()
 | 
				
			||||||
 | 
					        assert b[0] == ord(b"\xe0")
 | 
				
			||||||
 | 
					        assert b[1] == ord(b"\x01")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_16bit_s(self):
 | 
					    def test_16bit_s(self):
 | 
				
			||||||
        with Image.open("Tests/images/16bit.s.tif") as im:
 | 
					        with Image.open("Tests/images/16bit.s.tif") as im:
 | 
				
			||||||
            im.load()
 | 
					            im.load()
 | 
				
			||||||
| 
						 | 
					@ -459,6 +497,26 @@ class TestFileTiff:
 | 
				
			||||||
            exif = im.getexif()
 | 
					            exif = im.getexif()
 | 
				
			||||||
            check_exif(exif)
 | 
					            check_exif(exif)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_modify_exif(self, tmp_path):
 | 
				
			||||||
 | 
					        outfile = str(tmp_path / "temp.tif")
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/ifd_tag_type.tiff") as im:
 | 
				
			||||||
 | 
					            exif = im.getexif()
 | 
				
			||||||
 | 
					            exif[256] = 100
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            im.save(outfile, exif=exif)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with Image.open(outfile) as im:
 | 
				
			||||||
 | 
					            exif = im.getexif()
 | 
				
			||||||
 | 
					            assert exif[256] == 100
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_reload_exif_after_seek(self):
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/multipage.tiff") as im:
 | 
				
			||||||
 | 
					            exif = im.getexif()
 | 
				
			||||||
 | 
					            del exif[256]
 | 
				
			||||||
 | 
					            im.seek(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            assert 256 in exif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_exif_frames(self):
 | 
					    def test_exif_frames(self):
 | 
				
			||||||
        # Test that EXIF data can change across frames
 | 
					        # Test that EXIF data can change across frames
 | 
				
			||||||
        with Image.open("Tests/images/g4-multi.tiff") as im:
 | 
					        with Image.open("Tests/images/g4-multi.tiff") as im:
 | 
				
			||||||
| 
						 | 
					@ -598,6 +656,17 @@ class TestFileTiff:
 | 
				
			||||||
        with Image.open(infile) as im:
 | 
					        with Image.open(infile) as im:
 | 
				
			||||||
            assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
 | 
					            assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_planar_configuration_save(self, tmp_path):
 | 
				
			||||||
 | 
					        infile = "Tests/images/tiff_tiled_planar_raw.tif"
 | 
				
			||||||
 | 
					        with Image.open(infile) as im:
 | 
				
			||||||
 | 
					            assert im._planar_configuration == 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            outfile = str(tmp_path / "temp.tif")
 | 
				
			||||||
 | 
					            im.save(outfile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            with Image.open(outfile) as reloaded:
 | 
				
			||||||
 | 
					                assert_image_equal_tofile(reloaded, infile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_palette(self, tmp_path):
 | 
					    def test_palette(self, tmp_path):
 | 
				
			||||||
        def roundtrip(mode):
 | 
					        def roundtrip(mode):
 | 
				
			||||||
            outfile = str(tmp_path / "temp.tif")
 | 
					            outfile = str(tmp_path / "temp.tif")
 | 
				
			||||||
| 
						 | 
					@ -631,11 +700,11 @@ class TestFileTiff:
 | 
				
			||||||
            assert reread.n_frames == 3
 | 
					            assert reread.n_frames == 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Test appending using a generator
 | 
					        # Test appending using a generator
 | 
				
			||||||
        def imGenerator(ims):
 | 
					        def im_generator(ims):
 | 
				
			||||||
            yield from ims
 | 
					            yield from ims
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        mp = BytesIO()
 | 
					        mp = BytesIO()
 | 
				
			||||||
        im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims))
 | 
					        im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        mp.seek(0, os.SEEK_SET)
 | 
					        mp.seek(0, os.SEEK_SET)
 | 
				
			||||||
        with Image.open(mp) as reread:
 | 
					        with Image.open(mp) as reread:
 | 
				
			||||||
| 
						 | 
					@ -666,6 +735,13 @@ class TestFileTiff:
 | 
				
			||||||
        with Image.open(outfile) as reloaded:
 | 
					        with Image.open(outfile) as reloaded:
 | 
				
			||||||
            assert reloaded.info["icc_profile"] == icc_profile
 | 
					            assert reloaded.info["icc_profile"] == icc_profile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_save_bmp_compression(self, tmp_path):
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/hopper.bmp") as im:
 | 
				
			||||||
 | 
					            assert im.info["compression"] == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            outfile = str(tmp_path / "temp.tif")
 | 
				
			||||||
 | 
					            im.save(outfile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_discard_icc_profile(self, tmp_path):
 | 
					    def test_discard_icc_profile(self, tmp_path):
 | 
				
			||||||
        outfile = str(tmp_path / "temp.tif")
 | 
					        outfile = str(tmp_path / "temp.tif")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,26 +28,26 @@ def test_rt_metadata(tmp_path):
 | 
				
			||||||
    # For text items, we still have to decode('ascii','replace') because
 | 
					    # For text items, we still have to decode('ascii','replace') because
 | 
				
			||||||
    # the tiff file format can't take 8 bit bytes in that field.
 | 
					    # the tiff file format can't take 8 bit bytes in that field.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    basetextdata = "This is some arbitrary metadata for a text field"
 | 
					    base_text_data = "This is some arbitrary metadata for a text field"
 | 
				
			||||||
    bindata = basetextdata.encode("ascii") + b" \xff"
 | 
					    bin_data = base_text_data.encode("ascii") + b" \xff"
 | 
				
			||||||
    textdata = basetextdata + " " + chr(255)
 | 
					    text_data = base_text_data + " " + chr(255)
 | 
				
			||||||
    reloaded_textdata = basetextdata + " ?"
 | 
					    reloaded_text_data = base_text_data + " ?"
 | 
				
			||||||
    floatdata = 12.345
 | 
					    float_data = 12.345
 | 
				
			||||||
    doubledata = 67.89
 | 
					    double_data = 67.89
 | 
				
			||||||
    info = TiffImagePlugin.ImageFileDirectory()
 | 
					    info = TiffImagePlugin.ImageFileDirectory()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ImageJMetaData = TAG_IDS["ImageJMetaData"]
 | 
					    ImageJMetaData = TAG_IDS["ImageJMetaData"]
 | 
				
			||||||
    ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"]
 | 
					    ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"]
 | 
				
			||||||
    ImageDescription = TAG_IDS["ImageDescription"]
 | 
					    ImageDescription = TAG_IDS["ImageDescription"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    info[ImageJMetaDataByteCounts] = len(bindata)
 | 
					    info[ImageJMetaDataByteCounts] = len(bin_data)
 | 
				
			||||||
    info[ImageJMetaData] = bindata
 | 
					    info[ImageJMetaData] = bin_data
 | 
				
			||||||
    info[TAG_IDS["RollAngle"]] = floatdata
 | 
					    info[TAG_IDS["RollAngle"]] = float_data
 | 
				
			||||||
    info.tagtype[TAG_IDS["RollAngle"]] = 11
 | 
					    info.tagtype[TAG_IDS["RollAngle"]] = 11
 | 
				
			||||||
    info[TAG_IDS["YawAngle"]] = doubledata
 | 
					    info[TAG_IDS["YawAngle"]] = double_data
 | 
				
			||||||
    info.tagtype[TAG_IDS["YawAngle"]] = 12
 | 
					    info.tagtype[TAG_IDS["YawAngle"]] = 12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    info[ImageDescription] = textdata
 | 
					    info[ImageDescription] = text_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    f = str(tmp_path / "temp.tif")
 | 
					    f = str(tmp_path / "temp.tif")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,28 +55,28 @@ def test_rt_metadata(tmp_path):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with Image.open(f) as loaded:
 | 
					    with Image.open(f) as loaded:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert loaded.tag[ImageJMetaDataByteCounts] == (len(bindata),)
 | 
					        assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
 | 
				
			||||||
        assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bindata),)
 | 
					        assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert loaded.tag[ImageJMetaData] == bindata
 | 
					        assert loaded.tag[ImageJMetaData] == bin_data
 | 
				
			||||||
        assert loaded.tag_v2[ImageJMetaData] == bindata
 | 
					        assert loaded.tag_v2[ImageJMetaData] == bin_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert loaded.tag[ImageDescription] == (reloaded_textdata,)
 | 
					        assert loaded.tag[ImageDescription] == (reloaded_text_data,)
 | 
				
			||||||
        assert loaded.tag_v2[ImageDescription] == reloaded_textdata
 | 
					        assert loaded.tag_v2[ImageDescription] == reloaded_text_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0]
 | 
					        loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0]
 | 
				
			||||||
        assert round(abs(loaded_float - floatdata), 5) == 0
 | 
					        assert round(abs(loaded_float - float_data), 5) == 0
 | 
				
			||||||
        loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0]
 | 
					        loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0]
 | 
				
			||||||
        assert round(abs(loaded_double - doubledata), 7) == 0
 | 
					        assert round(abs(loaded_double - double_data), 7) == 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # check with 2 element ImageJMetaDataByteCounts, issue #2006
 | 
					    # check with 2 element ImageJMetaDataByteCounts, issue #2006
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8)
 | 
					    info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
 | 
				
			||||||
    img.save(f, tiffinfo=info)
 | 
					    img.save(f, tiffinfo=info)
 | 
				
			||||||
    with Image.open(f) as loaded:
 | 
					    with Image.open(f) as loaded:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bindata) - 8)
 | 
					        assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
 | 
				
			||||||
        assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bindata) - 8)
 | 
					        assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_read_metadata():
 | 
					def test_read_metadata():
 | 
				
			||||||
| 
						 | 
					@ -356,7 +356,7 @@ def test_empty_values():
 | 
				
			||||||
    assert 33432 in info
 | 
					    assert 33432 in info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_PhotoshopInfo(tmp_path):
 | 
					def test_photoshop_info(tmp_path):
 | 
				
			||||||
    with Image.open("Tests/images/issue_2278.tif") as im:
 | 
					    with Image.open("Tests/images/issue_2278.tif") as im:
 | 
				
			||||||
        assert len(im.tag_v2[34377]) == 70
 | 
					        assert len(im.tag_v2[34377]) == 70
 | 
				
			||||||
        assert isinstance(im.tag_v2[34377], bytes)
 | 
					        assert isinstance(im.tag_v2[34377], bytes)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,7 @@ import pytest
 | 
				
			||||||
from PIL import Image, WebPImagePlugin, features
 | 
					from PIL import Image, WebPImagePlugin, features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .helper import (
 | 
					from .helper import (
 | 
				
			||||||
 | 
					    assert_image_equal,
 | 
				
			||||||
    assert_image_similar,
 | 
					    assert_image_similar,
 | 
				
			||||||
    assert_image_similar_tofile,
 | 
					    assert_image_similar_tofile,
 | 
				
			||||||
    hopper,
 | 
					    hopper,
 | 
				
			||||||
| 
						 | 
					@ -105,6 +106,19 @@ class TestFileWebp:
 | 
				
			||||||
        hopper().save(buffer_method, format="WEBP", method=6)
 | 
					        hopper().save(buffer_method, format="WEBP", method=6)
 | 
				
			||||||
        assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
 | 
					        assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @skip_unless_feature("webp_anim")
 | 
				
			||||||
 | 
					    def test_save_all(self, tmp_path):
 | 
				
			||||||
 | 
					        temp_file = str(tmp_path / "temp.webp")
 | 
				
			||||||
 | 
					        im = Image.new("RGB", (1, 1))
 | 
				
			||||||
 | 
					        im2 = Image.new("RGB", (1, 1), "#f00")
 | 
				
			||||||
 | 
					        im.save(temp_file, save_all=True, append_images=[im2])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with Image.open(temp_file) as reloaded:
 | 
				
			||||||
 | 
					            assert_image_equal(im, reloaded)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            reloaded.seek(1)
 | 
				
			||||||
 | 
					            assert_image_similar(im2, reloaded, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_icc_profile(self, tmp_path):
 | 
					    def test_icc_profile(self, tmp_path):
 | 
				
			||||||
        self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
 | 
					        self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
 | 
				
			||||||
        if _webp.HAVE_WEBPANIM:
 | 
					        if _webp.HAVE_WEBPANIM:
 | 
				
			||||||
| 
						 | 
					@ -171,9 +185,25 @@ class TestFileWebp:
 | 
				
			||||||
            Image.open(blob).load()
 | 
					            Image.open(blob).load()
 | 
				
			||||||
            Image.open(blob).load()
 | 
					            Image.open(blob).load()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @skip_unless_feature("webp")
 | 
					    @pytest.mark.parametrize(
 | 
				
			||||||
 | 
					        "background",
 | 
				
			||||||
 | 
					        (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    @skip_unless_feature("webp_anim")
 | 
				
			||||||
 | 
					    def test_invalid_background(self, background, tmp_path):
 | 
				
			||||||
 | 
					        temp_file = str(tmp_path / "temp.webp")
 | 
				
			||||||
 | 
					        im = hopper()
 | 
				
			||||||
 | 
					        with pytest.raises(OSError):
 | 
				
			||||||
 | 
					            im.save(temp_file, save_all=True, append_images=[im], background=background)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @skip_unless_feature("webp_anim")
 | 
					    @skip_unless_feature("webp_anim")
 | 
				
			||||||
    def test_background_from_gif(self, tmp_path):
 | 
					    def test_background_from_gif(self, tmp_path):
 | 
				
			||||||
 | 
					        # Save L mode GIF with background
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/no_palette_with_background.gif") as im:
 | 
				
			||||||
 | 
					            out_webp = str(tmp_path / "temp.webp")
 | 
				
			||||||
 | 
					            im.save(out_webp, save_all=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Save P mode GIF with background
 | 
				
			||||||
        with Image.open("Tests/images/chi.gif") as im:
 | 
					        with Image.open("Tests/images/chi.gif") as im:
 | 
				
			||||||
            original_value = im.convert("RGB").getpixel((1, 1))
 | 
					            original_value = im.convert("RGB").getpixel((1, 1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -191,7 +221,6 @@ class TestFileWebp:
 | 
				
			||||||
        difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
 | 
					        difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
 | 
				
			||||||
        assert difference < 5
 | 
					        assert difference < 5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @skip_unless_feature("webp")
 | 
					 | 
				
			||||||
    @skip_unless_feature("webp_anim")
 | 
					    @skip_unless_feature("webp_anim")
 | 
				
			||||||
    def test_duration(self, tmp_path):
 | 
					    def test_duration(self, tmp_path):
 | 
				
			||||||
        with Image.open("Tests/images/dispose_bgnd.gif") as im:
 | 
					        with Image.open("Tests/images/dispose_bgnd.gif") as im:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					from packaging.version import parse as parse_version
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import Image
 | 
					from PIL import Image, features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .helper import (
 | 
					from .helper import (
 | 
				
			||||||
    assert_image_equal,
 | 
					    assert_image_equal,
 | 
				
			||||||
| 
						 | 
					@ -27,7 +28,6 @@ def test_n_frames():
 | 
				
			||||||
        assert im.is_animated
 | 
					        assert im.is_animated
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
 | 
					 | 
				
			||||||
def test_write_animation_L(tmp_path):
 | 
					def test_write_animation_L(tmp_path):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Convert an animated GIF to animated WebP, then compare the frame count, and first
 | 
					    Convert an animated GIF to animated WebP, then compare the frame count, and first
 | 
				
			||||||
| 
						 | 
					@ -46,6 +46,11 @@ def test_write_animation_L(tmp_path):
 | 
				
			||||||
            orig.load()
 | 
					            orig.load()
 | 
				
			||||||
            im.load()
 | 
					            im.load()
 | 
				
			||||||
            assert_image_similar(im, orig.convert("RGBA"), 32.9)
 | 
					            assert_image_similar(im, orig.convert("RGBA"), 32.9)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if is_big_endian():
 | 
				
			||||||
 | 
					                webp = parse_version(features.version_module("webp"))
 | 
				
			||||||
 | 
					                if webp < parse_version("1.2.2"):
 | 
				
			||||||
 | 
					                    pytest.skip("Fails with libwebp earlier than 1.2.2")
 | 
				
			||||||
            orig.seek(orig.n_frames - 1)
 | 
					            orig.seek(orig.n_frames - 1)
 | 
				
			||||||
            im.seek(im.n_frames - 1)
 | 
					            im.seek(im.n_frames - 1)
 | 
				
			||||||
            orig.load()
 | 
					            orig.load()
 | 
				
			||||||
| 
						 | 
					@ -53,7 +58,6 @@ def test_write_animation_L(tmp_path):
 | 
				
			||||||
            assert_image_similar(im, orig.convert("RGBA"), 32.9)
 | 
					            assert_image_similar(im, orig.convert("RGBA"), 32.9)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian")
 | 
					 | 
				
			||||||
def test_write_animation_RGB(tmp_path):
 | 
					def test_write_animation_RGB(tmp_path):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Write an animated WebP from RGB frames, and ensure the frames
 | 
					    Write an animated WebP from RGB frames, and ensure the frames
 | 
				
			||||||
| 
						 | 
					@ -69,6 +73,10 @@ def test_write_animation_RGB(tmp_path):
 | 
				
			||||||
            assert_image_equal(im, frame1.convert("RGBA"))
 | 
					            assert_image_equal(im, frame1.convert("RGBA"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Compare second frame to original
 | 
					            # Compare second frame to original
 | 
				
			||||||
 | 
					            if is_big_endian():
 | 
				
			||||||
 | 
					                webp = parse_version(features.version_module("webp"))
 | 
				
			||||||
 | 
					                if webp < parse_version("1.2.2"):
 | 
				
			||||||
 | 
					                    pytest.skip("Fails with libwebp earlier than 1.2.2")
 | 
				
			||||||
            im.seek(1)
 | 
					            im.seek(1)
 | 
				
			||||||
            im.load()
 | 
					            im.load()
 | 
				
			||||||
            assert_image_equal(im, frame2.convert("RGBA"))
 | 
					            assert_image_equal(im, frame2.convert("RGBA"))
 | 
				
			||||||
| 
						 | 
					@ -82,14 +90,14 @@ def test_write_animation_RGB(tmp_path):
 | 
				
			||||||
            check(temp_file1)
 | 
					            check(temp_file1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Tests appending using a generator
 | 
					            # Tests appending using a generator
 | 
				
			||||||
            def imGenerator(ims):
 | 
					            def im_generator(ims):
 | 
				
			||||||
                yield from ims
 | 
					                yield from ims
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            temp_file2 = str(tmp_path / "temp_generator.webp")
 | 
					            temp_file2 = str(tmp_path / "temp_generator.webp")
 | 
				
			||||||
            frame1.copy().save(
 | 
					            frame1.copy().save(
 | 
				
			||||||
                temp_file2,
 | 
					                temp_file2,
 | 
				
			||||||
                save_all=True,
 | 
					                save_all=True,
 | 
				
			||||||
                append_images=imGenerator([frame2]),
 | 
					                append_images=im_generator([frame2]),
 | 
				
			||||||
                lossless=True,
 | 
					                lossless=True,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            check(temp_file2)
 | 
					            check(temp_file2)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ import warnings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError
 | 
					from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError, features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .helper import (
 | 
					from .helper import (
 | 
				
			||||||
    assert_image_equal,
 | 
					    assert_image_equal,
 | 
				
			||||||
| 
						 | 
					@ -161,6 +161,8 @@ class TestImage:
 | 
				
			||||||
            assert im.size == (128, 128)
 | 
					            assert im.size == (128, 128)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for ext in (".jpg", ".jp2"):
 | 
					            for ext in (".jpg", ".jp2"):
 | 
				
			||||||
 | 
					                if ext == ".jp2" and not features.check_codec("jpg_2000"):
 | 
				
			||||||
 | 
					                    pytest.skip("jpg_2000 not available")
 | 
				
			||||||
                temp_file = str(tmp_path / ("temp." + ext))
 | 
					                temp_file = str(tmp_path / ("temp." + ext))
 | 
				
			||||||
                if os.path.exists(temp_file):
 | 
					                if os.path.exists(temp_file):
 | 
				
			||||||
                    os.remove(temp_file)
 | 
					                    os.remove(temp_file)
 | 
				
			||||||
| 
						 | 
					@ -170,7 +172,7 @@ class TestImage:
 | 
				
			||||||
        temp_file = str(tmp_path / "temp.jpg")
 | 
					        temp_file = str(tmp_path / "temp.jpg")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        class FP:
 | 
					        class FP:
 | 
				
			||||||
            def write(a, b):
 | 
					            def write(self, b):
 | 
				
			||||||
                pass
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fp = FP()
 | 
					        fp = FP()
 | 
				
			||||||
| 
						 | 
					@ -602,11 +604,34 @@ class TestImage:
 | 
				
			||||||
        with Image.open("Tests/images/hopper.gif") as im:
 | 
					        with Image.open("Tests/images/hopper.gif") as im:
 | 
				
			||||||
            assert_image_equal(im, im.remap_palette(list(range(256))))
 | 
					            assert_image_equal(im, im.remap_palette(list(range(256))))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test identity transform with an RGBA palette
 | 
				
			||||||
 | 
					        im = Image.new("P", (256, 1))
 | 
				
			||||||
 | 
					        for x in range(256):
 | 
				
			||||||
 | 
					            im.putpixel((x, 0), x)
 | 
				
			||||||
 | 
					        im.putpalette(list(range(256)) * 4, "RGBA")
 | 
				
			||||||
 | 
					        im_remapped = im.remap_palette(list(range(256)))
 | 
				
			||||||
 | 
					        assert_image_equal(im, im_remapped)
 | 
				
			||||||
 | 
					        assert im.palette.palette == im_remapped.palette.palette
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Test illegal image mode
 | 
					        # Test illegal image mode
 | 
				
			||||||
        with hopper() as im:
 | 
					        with hopper() as im:
 | 
				
			||||||
            with pytest.raises(ValueError):
 | 
					            with pytest.raises(ValueError):
 | 
				
			||||||
                im.remap_palette(None)
 | 
					                im.remap_palette(None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_remap_palette_transparency(self):
 | 
				
			||||||
 | 
					        im = Image.new("P", (1, 2))
 | 
				
			||||||
 | 
					        im.putpixel((0, 1), 1)
 | 
				
			||||||
 | 
					        im.info["transparency"] = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        im_remapped = im.remap_palette([1, 0])
 | 
				
			||||||
 | 
					        assert im_remapped.info["transparency"] == 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test unused transparency
 | 
				
			||||||
 | 
					        im.info["transparency"] = 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        im_remapped = im.remap_palette([1, 0])
 | 
				
			||||||
 | 
					        assert "transparency" not in im_remapped.info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test__new(self):
 | 
					    def test__new(self):
 | 
				
			||||||
        im = hopper("RGB")
 | 
					        im = hopper("RGB")
 | 
				
			||||||
        im_p = hopper("P")
 | 
					        im_p = hopper("P")
 | 
				
			||||||
| 
						 | 
					@ -652,6 +677,15 @@ class TestImage:
 | 
				
			||||||
            with warnings.catch_warnings():
 | 
					            with warnings.catch_warnings():
 | 
				
			||||||
                im.save(temp_file)
 | 
					                im.save(temp_file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_no_new_file_on_error(self, tmp_path):
 | 
				
			||||||
 | 
					        temp_file = str(tmp_path / "temp.jpg")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        im = Image.new("RGB", (0, 0))
 | 
				
			||||||
 | 
					        with pytest.raises(ValueError):
 | 
				
			||||||
 | 
					            im.save(temp_file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert not os.path.exists(temp_file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_load_on_nonexclusive_multiframe(self):
 | 
					    def test_load_on_nonexclusive_multiframe(self):
 | 
				
			||||||
        with open("Tests/images/frozenpond.mpo", "rb") as fp:
 | 
					        with open("Tests/images/frozenpond.mpo", "rb") as fp:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -666,6 +700,19 @@ class TestImage:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            assert not fp.closed
 | 
					            assert not fp.closed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_empty_exif(self):
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/exif.png") as im:
 | 
				
			||||||
 | 
					            exif = im.getexif()
 | 
				
			||||||
 | 
					        assert dict(exif) != {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test that exif data is cleared after another load
 | 
				
			||||||
 | 
					        exif.load(None)
 | 
				
			||||||
 | 
					        assert dict(exif) == {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test loading just the EXIF header
 | 
				
			||||||
 | 
					        exif.load(b"Exif\x00\x00")
 | 
				
			||||||
 | 
					        assert dict(exif) == {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @mark_if_feature_version(
 | 
					    @mark_if_feature_version(
 | 
				
			||||||
        pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
 | 
					        pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
| 
						 | 
					@ -802,6 +849,35 @@ class TestImage:
 | 
				
			||||||
        im = Image.new("RGB", size)
 | 
					        im = Image.new("RGB", size)
 | 
				
			||||||
        assert im.tobytes() == b""
 | 
					        assert im.tobytes() == b""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_apply_transparency(self):
 | 
				
			||||||
 | 
					        im = Image.new("P", (1, 1))
 | 
				
			||||||
 | 
					        im.putpalette((0, 0, 0, 1, 1, 1))
 | 
				
			||||||
 | 
					        assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test that no transformation is applied without transparency
 | 
				
			||||||
 | 
					        im.apply_transparency()
 | 
				
			||||||
 | 
					        assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test that a transparency index is applied
 | 
				
			||||||
 | 
					        im.info["transparency"] = 0
 | 
				
			||||||
 | 
					        im.apply_transparency()
 | 
				
			||||||
 | 
					        assert "transparency" not in im.info
 | 
				
			||||||
 | 
					        assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 255): 1}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test that existing transparency is kept
 | 
				
			||||||
 | 
					        im = Image.new("P", (1, 1))
 | 
				
			||||||
 | 
					        im.putpalette((0, 0, 0, 255, 1, 1, 1, 128), "RGBA")
 | 
				
			||||||
 | 
					        im.info["transparency"] = 0
 | 
				
			||||||
 | 
					        im.apply_transparency()
 | 
				
			||||||
 | 
					        assert im.palette.colors == {(0, 0, 0, 0): 0, (1, 1, 1, 128): 1}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test that transparency bytes are applied
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/pil123p.png") as im:
 | 
				
			||||||
 | 
					            assert isinstance(im.info["transparency"], bytes)
 | 
				
			||||||
 | 
					            assert im.palette.colors[(27, 35, 6)] == 24
 | 
				
			||||||
 | 
					            im.apply_transparency()
 | 
				
			||||||
 | 
					            assert im.palette.colors[(27, 35, 6, 214)] == 24
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_categories_deprecation(self):
 | 
					    def test_categories_deprecation(self):
 | 
				
			||||||
        with pytest.warns(DeprecationWarning):
 | 
					        with pytest.warns(DeprecationWarning):
 | 
				
			||||||
            assert hopper().category == 0
 | 
					            assert hopper().category == 0
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,3 @@
 | 
				
			||||||
import ctypes
 | 
					 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import subprocess
 | 
					import subprocess
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
| 
						 | 
					@ -154,14 +153,17 @@ class TestImageGetPixel(AccessTest):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Check 0
 | 
					        # Check 0
 | 
				
			||||||
        im = Image.new(mode, (0, 0), None)
 | 
					        im = Image.new(mode, (0, 0), None)
 | 
				
			||||||
        with pytest.raises(IndexError):
 | 
					        assert im.load() is not None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        error = ValueError if self._need_cffi_access else IndexError
 | 
				
			||||||
 | 
					        with pytest.raises(error):
 | 
				
			||||||
            im.putpixel((0, 0), c)
 | 
					            im.putpixel((0, 0), c)
 | 
				
			||||||
        with pytest.raises(IndexError):
 | 
					        with pytest.raises(error):
 | 
				
			||||||
            im.getpixel((0, 0))
 | 
					            im.getpixel((0, 0))
 | 
				
			||||||
        # Check 0 negative index
 | 
					        # Check 0 negative index
 | 
				
			||||||
        with pytest.raises(IndexError):
 | 
					        with pytest.raises(error):
 | 
				
			||||||
            im.putpixel((-1, -1), c)
 | 
					            im.putpixel((-1, -1), c)
 | 
				
			||||||
        with pytest.raises(IndexError):
 | 
					        with pytest.raises(error):
 | 
				
			||||||
            im.getpixel((-1, -1))
 | 
					            im.getpixel((-1, -1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # check initial color
 | 
					        # check initial color
 | 
				
			||||||
| 
						 | 
					@ -176,10 +178,10 @@ class TestImageGetPixel(AccessTest):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Check 0
 | 
					        # Check 0
 | 
				
			||||||
        im = Image.new(mode, (0, 0), c)
 | 
					        im = Image.new(mode, (0, 0), c)
 | 
				
			||||||
        with pytest.raises(IndexError):
 | 
					        with pytest.raises(error):
 | 
				
			||||||
            im.getpixel((0, 0))
 | 
					            im.getpixel((0, 0))
 | 
				
			||||||
        # Check 0 negative index
 | 
					        # Check 0 negative index
 | 
				
			||||||
        with pytest.raises(IndexError):
 | 
					        with pytest.raises(error):
 | 
				
			||||||
            im.getpixel((-1, -1))
 | 
					            im.getpixel((-1, -1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_basic(self):
 | 
					    def test_basic(self):
 | 
				
			||||||
| 
						 | 
					@ -401,6 +403,8 @@ class TestEmbeddable:
 | 
				
			||||||
        "not from shell",
 | 
					        "not from shell",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    def test_embeddable(self):
 | 
					    def test_embeddable(self):
 | 
				
			||||||
 | 
					        import ctypes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with open("embed_pil.c", "w") as fh:
 | 
					        with open("embed_pil.c", "w") as fh:
 | 
				
			||||||
            fh.write(
 | 
					            fh.write(
 | 
				
			||||||
                """
 | 
					                """
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -80,3 +80,15 @@ def test_fromarray():
 | 
				
			||||||
    with pytest.raises(TypeError):
 | 
					    with pytest.raises(TypeError):
 | 
				
			||||||
        wrapped = Wrapper(test("L"), {"shape": (100, 128)})
 | 
					        wrapped = Wrapper(test("L"), {"shape": (100, 128)})
 | 
				
			||||||
        Image.fromarray(wrapped)
 | 
					        Image.fromarray(wrapped)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_fromarray_palette():
 | 
				
			||||||
 | 
					    # Arrange
 | 
				
			||||||
 | 
					    i = im.convert("L")
 | 
				
			||||||
 | 
					    a = numpy.array(i)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Act
 | 
				
			||||||
 | 
					    out = Image.fromarray(a, "P")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Assert that the Python and C palettes match
 | 
				
			||||||
 | 
					    assert len(out.palette.colors) == len(out.im.getpalette()) / 3
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,15 +27,15 @@ def test_sanity():
 | 
				
			||||||
        "HSV",
 | 
					        "HSV",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for mode in modes:
 | 
					    for input_mode in modes:
 | 
				
			||||||
        im = hopper(mode)
 | 
					        im = hopper(input_mode)
 | 
				
			||||||
        for mode in modes:
 | 
					        for output_mode in modes:
 | 
				
			||||||
            convert(im, mode)
 | 
					            convert(im, output_mode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Check 0
 | 
					        # Check 0
 | 
				
			||||||
        im = Image.new(mode, (0, 0))
 | 
					        im = Image.new(input_mode, (0, 0))
 | 
				
			||||||
        for mode in modes:
 | 
					        for output_mode in modes:
 | 
				
			||||||
            convert(im, mode)
 | 
					            convert(im, output_mode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_default():
 | 
					def test_default():
 | 
				
			||||||
| 
						 | 
					@ -70,6 +70,11 @@ def test_16bit():
 | 
				
			||||||
    with Image.open("Tests/images/16bit.cropped.tif") as im:
 | 
					    with Image.open("Tests/images/16bit.cropped.tif") as im:
 | 
				
			||||||
        _test_float_conversion(im)
 | 
					        _test_float_conversion(im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for color in (65535, 65536):
 | 
				
			||||||
 | 
					        im = Image.new("I", (1, 1), color)
 | 
				
			||||||
 | 
					        im_i16 = im.convert("I;16")
 | 
				
			||||||
 | 
					        assert im_i16.getpixel((0, 0)) == 65535
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_16bit_workaround():
 | 
					def test_16bit_workaround():
 | 
				
			||||||
    with Image.open("Tests/images/16bit.cropped.tif") as im:
 | 
					    with Image.open("Tests/images/16bit.cropped.tif") as im:
 | 
				
			||||||
| 
						 | 
					@ -135,6 +140,10 @@ def test_trns_l(tmp_path):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    f = str(tmp_path / "temp.png")
 | 
					    f = str(tmp_path / "temp.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    im_la = im.convert("LA")
 | 
				
			||||||
 | 
					    assert "transparency" not in im_la.info
 | 
				
			||||||
 | 
					    im_la.save(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    im_rgb = im.convert("RGB")
 | 
					    im_rgb = im.convert("RGB")
 | 
				
			||||||
    assert im_rgb.info["transparency"] == (128, 128, 128)  # undone
 | 
					    assert im_rgb.info["transparency"] == (128, 128, 128)  # undone
 | 
				
			||||||
    im_rgb.save(f)
 | 
					    im_rgb.save(f)
 | 
				
			||||||
| 
						 | 
					@ -213,6 +222,20 @@ def test_p_la():
 | 
				
			||||||
    assert_image_similar(alpha, comparable, 5)
 | 
					    assert_image_similar(alpha, comparable, 5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_p2pa_alpha():
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/tiny.png") as im:
 | 
				
			||||||
 | 
					        assert im.mode == "P"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        im_pa = im.convert("PA")
 | 
				
			||||||
 | 
					    assert im_pa.mode == "PA"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    im_a = im_pa.getchannel("A")
 | 
				
			||||||
 | 
					    for x in range(4):
 | 
				
			||||||
 | 
					        alpha = 255 if x > 1 else 0
 | 
				
			||||||
 | 
					        for y in range(4):
 | 
				
			||||||
 | 
					            assert im_a.getpixel((x, y)) == alpha
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_matrix_illegal_conversion():
 | 
					def test_matrix_illegal_conversion():
 | 
				
			||||||
    # Arrange
 | 
					    # Arrange
 | 
				
			||||||
    im = hopper("CMYK")
 | 
					    im = hopper("CMYK")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,8 +6,8 @@ from .helper import hopper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_copy():
 | 
					def test_copy():
 | 
				
			||||||
    croppedCoordinates = (10, 10, 20, 20)
 | 
					    cropped_coordinates = (10, 10, 20, 20)
 | 
				
			||||||
    croppedSize = (10, 10)
 | 
					    cropped_size = (10, 10)
 | 
				
			||||||
    for mode in "1", "P", "L", "RGB", "I", "F":
 | 
					    for mode in "1", "P", "L", "RGB", "I", "F":
 | 
				
			||||||
        # Internal copy method
 | 
					        # Internal copy method
 | 
				
			||||||
        im = hopper(mode)
 | 
					        im = hopper(mode)
 | 
				
			||||||
| 
						 | 
					@ -23,15 +23,15 @@ def test_copy():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Internal copy method on a cropped image
 | 
					        # Internal copy method on a cropped image
 | 
				
			||||||
        im = hopper(mode)
 | 
					        im = hopper(mode)
 | 
				
			||||||
        out = im.crop(croppedCoordinates).copy()
 | 
					        out = im.crop(cropped_coordinates).copy()
 | 
				
			||||||
        assert out.mode == im.mode
 | 
					        assert out.mode == im.mode
 | 
				
			||||||
        assert out.size == croppedSize
 | 
					        assert out.size == cropped_size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Python's copy method on a cropped image
 | 
					        # Python's copy method on a cropped image
 | 
				
			||||||
        im = hopper(mode)
 | 
					        im = hopper(mode)
 | 
				
			||||||
        out = copy.copy(im.crop(croppedCoordinates))
 | 
					        out = copy.copy(im.crop(cropped_coordinates))
 | 
				
			||||||
        assert out.mode == im.mode
 | 
					        assert out.mode == im.mode
 | 
				
			||||||
        assert out.size == croppedSize
 | 
					        assert out.size == cropped_size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_copy_zero():
 | 
					def test_copy_zero():
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -99,10 +99,10 @@ def test_rankfilter_properties():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_builtinfilter_p():
 | 
					def test_builtinfilter_p():
 | 
				
			||||||
    builtinFilter = ImageFilter.BuiltinFilter()
 | 
					    builtin_filter = ImageFilter.BuiltinFilter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with pytest.raises(ValueError):
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
        builtinFilter.filter(hopper("P"))
 | 
					        builtin_filter.filter(hopper("P"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_kernel_not_enough_coefficients():
 | 
					def test_kernel_not_enough_coefficients():
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,12 @@
 | 
				
			||||||
 | 
					import warnings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import Image, ImageQt
 | 
					from PIL import Image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					with warnings.catch_warnings():
 | 
				
			||||||
 | 
					    warnings.simplefilter("ignore", category=DeprecationWarning)
 | 
				
			||||||
 | 
					    from PIL import ImageQt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .helper import assert_image_equal, hopper
 | 
					from .helper import assert_image_equal, hopper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
from PIL import Image
 | 
					from PIL import Image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .helper import assert_image_equal, cached_property
 | 
					from .helper import CachedProperty, assert_image_equal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestImagingPaste:
 | 
					class TestImagingPaste:
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,7 @@ class TestImagingPaste:
 | 
				
			||||||
        im.paste(im2, mask)
 | 
					        im.paste(im2, mask)
 | 
				
			||||||
        self.assert_9points_image(im, expected)
 | 
					        self.assert_9points_image(im, expected)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @cached_property
 | 
					    @CachedProperty
 | 
				
			||||||
    def mask_1(self):
 | 
					    def mask_1(self):
 | 
				
			||||||
        mask = Image.new("1", (self.size, self.size))
 | 
					        mask = Image.new("1", (self.size, self.size))
 | 
				
			||||||
        px = mask.load()
 | 
					        px = mask.load()
 | 
				
			||||||
| 
						 | 
					@ -43,11 +43,11 @@ class TestImagingPaste:
 | 
				
			||||||
                px[y, x] = (x + y) % 2
 | 
					                px[y, x] = (x + y) % 2
 | 
				
			||||||
        return mask
 | 
					        return mask
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @cached_property
 | 
					    @CachedProperty
 | 
				
			||||||
    def mask_L(self):
 | 
					    def mask_L(self):
 | 
				
			||||||
        return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
 | 
					        return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @cached_property
 | 
					    @CachedProperty
 | 
				
			||||||
    def gradient_L(self):
 | 
					    def gradient_L(self):
 | 
				
			||||||
        gradient = Image.new("L", (self.size, self.size))
 | 
					        gradient = Image.new("L", (self.size, self.size))
 | 
				
			||||||
        px = gradient.load()
 | 
					        px = gradient.load()
 | 
				
			||||||
| 
						 | 
					@ -56,7 +56,7 @@ class TestImagingPaste:
 | 
				
			||||||
                px[y, x] = (x + y) % 255
 | 
					                px[y, x] = (x + y) % 255
 | 
				
			||||||
        return gradient
 | 
					        return gradient
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @cached_property
 | 
					    @CachedProperty
 | 
				
			||||||
    def gradient_RGB(self):
 | 
					    def gradient_RGB(self):
 | 
				
			||||||
        return Image.merge(
 | 
					        return Image.merge(
 | 
				
			||||||
            "RGB",
 | 
					            "RGB",
 | 
				
			||||||
| 
						 | 
					@ -67,7 +67,17 @@ class TestImagingPaste:
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @cached_property
 | 
					    @CachedProperty
 | 
				
			||||||
 | 
					    def gradient_LA(self):
 | 
				
			||||||
 | 
					        return Image.merge(
 | 
				
			||||||
 | 
					            "LA",
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					                self.gradient_L,
 | 
				
			||||||
 | 
					                self.gradient_L.transpose(Image.Transpose.ROTATE_90),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @CachedProperty
 | 
				
			||||||
    def gradient_RGBA(self):
 | 
					    def gradient_RGBA(self):
 | 
				
			||||||
        return Image.merge(
 | 
					        return Image.merge(
 | 
				
			||||||
            "RGBA",
 | 
					            "RGBA",
 | 
				
			||||||
| 
						 | 
					@ -79,7 +89,7 @@ class TestImagingPaste:
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @cached_property
 | 
					    @CachedProperty
 | 
				
			||||||
    def gradient_RGBa(self):
 | 
					    def gradient_RGBa(self):
 | 
				
			||||||
        return Image.merge(
 | 
					        return Image.merge(
 | 
				
			||||||
            "RGBa",
 | 
					            "RGBa",
 | 
				
			||||||
| 
						 | 
					@ -145,6 +155,28 @@ class TestImagingPaste:
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_image_mask_LA(self):
 | 
				
			||||||
 | 
					        for mode in ("RGBA", "RGB", "L"):
 | 
				
			||||||
 | 
					            im = Image.new(mode, (200, 200), "white")
 | 
				
			||||||
 | 
					            im2 = getattr(self, "gradient_" + mode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.assert_9points_paste(
 | 
				
			||||||
 | 
					                im,
 | 
				
			||||||
 | 
					                im2,
 | 
				
			||||||
 | 
					                self.gradient_LA,
 | 
				
			||||||
 | 
					                [
 | 
				
			||||||
 | 
					                    (128, 191, 255, 191),
 | 
				
			||||||
 | 
					                    (112, 207, 206, 111),
 | 
				
			||||||
 | 
					                    (128, 254, 128, 1),
 | 
				
			||||||
 | 
					                    (208, 208, 239, 239),
 | 
				
			||||||
 | 
					                    (192, 191, 191, 191),
 | 
				
			||||||
 | 
					                    (207, 207, 112, 113),
 | 
				
			||||||
 | 
					                    (255, 255, 255, 255),
 | 
				
			||||||
 | 
					                    (239, 207, 207, 239),
 | 
				
			||||||
 | 
					                    (255, 191, 128, 191),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_image_mask_RGBA(self):
 | 
					    def test_image_mask_RGBA(self):
 | 
				
			||||||
        for mode in ("RGBA", "RGB", "L"):
 | 
					        for mode in ("RGBA", "RGB", "L"):
 | 
				
			||||||
            im = Image.new(mode, (200, 200), "white")
 | 
					            im = Image.new(mode, (200, 200), "white")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,7 @@
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from PIL import Image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .helper import assert_image_equal, hopper
 | 
					from .helper import assert_image_equal, hopper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,17 +12,31 @@ def test_sanity():
 | 
				
			||||||
        im.point(list(range(256)))
 | 
					        im.point(list(range(256)))
 | 
				
			||||||
    im.point(list(range(256)) * 3)
 | 
					    im.point(list(range(256)) * 3)
 | 
				
			||||||
    im.point(lambda x: x)
 | 
					    im.point(lambda x: x)
 | 
				
			||||||
 | 
					    im.point(lambda x: x * 1.2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    im = im.convert("I")
 | 
					    im = im.convert("I")
 | 
				
			||||||
    with pytest.raises(ValueError):
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
        im.point(list(range(256)))
 | 
					        im.point(list(range(256)))
 | 
				
			||||||
    im.point(lambda x: x * 1)
 | 
					    im.point(lambda x: x * 1)
 | 
				
			||||||
    im.point(lambda x: x + 1)
 | 
					    im.point(lambda x: x + 1)
 | 
				
			||||||
 | 
					    im.point(lambda x: x - 1)
 | 
				
			||||||
    im.point(lambda x: x * 1 + 1)
 | 
					    im.point(lambda x: x * 1 + 1)
 | 
				
			||||||
 | 
					    im.point(lambda x: 0.1 + 0.2 * x)
 | 
				
			||||||
 | 
					    im.point(lambda x: -x)
 | 
				
			||||||
 | 
					    im.point(lambda x: x - 0.5)
 | 
				
			||||||
 | 
					    im.point(lambda x: 1 - x / 2)
 | 
				
			||||||
 | 
					    im.point(lambda x: (2 + x) / 3)
 | 
				
			||||||
 | 
					    im.point(lambda x: 0.5)
 | 
				
			||||||
 | 
					    im.point(lambda x: x / 1)
 | 
				
			||||||
 | 
					    im.point(lambda x: x + x)
 | 
				
			||||||
    with pytest.raises(TypeError):
 | 
					    with pytest.raises(TypeError):
 | 
				
			||||||
        im.point(lambda x: x - 1)
 | 
					        im.point(lambda x: x * x)
 | 
				
			||||||
    with pytest.raises(TypeError):
 | 
					    with pytest.raises(TypeError):
 | 
				
			||||||
        im.point(lambda x: x / 1)
 | 
					        im.point(lambda x: x / x)
 | 
				
			||||||
 | 
					    with pytest.raises(TypeError):
 | 
				
			||||||
 | 
					        im.point(lambda x: 1 / x)
 | 
				
			||||||
 | 
					    with pytest.raises(TypeError):
 | 
				
			||||||
 | 
					        im.point(lambda x: x // 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_16bit_lut():
 | 
					def test_16bit_lut():
 | 
				
			||||||
| 
						 | 
					@ -46,3 +62,8 @@ def test_f_mode():
 | 
				
			||||||
    im = hopper("F")
 | 
					    im = hopper("F")
 | 
				
			||||||
    with pytest.raises(ValueError):
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
        im.point(None)
 | 
					        im.point(None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_coerce_e_deprecation():
 | 
				
			||||||
 | 
					    with pytest.warns(DeprecationWarning):
 | 
				
			||||||
 | 
					        assert Image.coerce_e(2).data == 2
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -458,7 +458,7 @@ class TestCoreResampleBox:
 | 
				
			||||||
        def split_range(size, tiles):
 | 
					        def split_range(size, tiles):
 | 
				
			||||||
            scale = size / tiles
 | 
					            scale = size / tiles
 | 
				
			||||||
            for i in range(tiles):
 | 
					            for i in range(tiles):
 | 
				
			||||||
                yield (int(round(scale * i)), int(round(scale * (i + 1))))
 | 
					                yield int(round(scale * i)), int(round(scale * (i + 1)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        tiled = Image.new(im.mode, dst_size)
 | 
					        tiled = Image.new(im.mode, dst_size)
 | 
				
			||||||
        scale = (im.size[0] / tiled.size[0], im.size[1] / tiled.size[1])
 | 
					        scale = (im.size[0] / tiled.size[0], im.size[1] / tiled.size[1])
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,6 +12,7 @@ from .helper import (
 | 
				
			||||||
    assert_image_equal_tofile,
 | 
					    assert_image_equal_tofile,
 | 
				
			||||||
    assert_image_similar,
 | 
					    assert_image_similar,
 | 
				
			||||||
    hopper,
 | 
					    hopper,
 | 
				
			||||||
 | 
					    skip_unless_feature,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -264,6 +265,14 @@ class TestImageResize:
 | 
				
			||||||
            with pytest.raises(ValueError):
 | 
					            with pytest.raises(ValueError):
 | 
				
			||||||
                im.resize((10, 10), "unknown")
 | 
					                im.resize((10, 10), "unknown")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @skip_unless_feature("libtiff")
 | 
				
			||||||
 | 
					    def test_load_first(self):
 | 
				
			||||||
 | 
					        # load() may change the size of the image
 | 
				
			||||||
 | 
					        # Test that resize() is calling it before getting the size
 | 
				
			||||||
 | 
					        with Image.open("Tests/images/g4_orientation_5.tif") as im:
 | 
				
			||||||
 | 
					            im = im.resize((64, 64))
 | 
				
			||||||
 | 
					            assert im.size == (64, 64)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_default_filter(self):
 | 
					    def test_default_filter(self):
 | 
				
			||||||
        for mode in "L", "RGB", "I", "F":
 | 
					        for mode in "L", "RGB", "I", "F":
 | 
				
			||||||
            im = hopper(mode)
 | 
					            im = hopper(mode)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,7 @@ from .helper import (
 | 
				
			||||||
    assert_image_similar,
 | 
					    assert_image_similar,
 | 
				
			||||||
    fromstring,
 | 
					    fromstring,
 | 
				
			||||||
    hopper,
 | 
					    hopper,
 | 
				
			||||||
 | 
					    skip_unless_feature,
 | 
				
			||||||
    tostring,
 | 
					    tostring,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -88,6 +89,15 @@ def test_no_resize():
 | 
				
			||||||
        assert im.size == (64, 64)
 | 
					        assert im.size == (64, 64)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@skip_unless_feature("libtiff")
 | 
				
			||||||
 | 
					def test_load_first():
 | 
				
			||||||
 | 
					    # load() may change the size of the image
 | 
				
			||||||
 | 
					    # Test that thumbnail() is calling it before performing size calculations
 | 
				
			||||||
 | 
					    with Image.open("Tests/images/g4_orientation_5.tif") as im:
 | 
				
			||||||
 | 
					        im.thumbnail((64, 64))
 | 
				
			||||||
 | 
					        assert im.size == (64, 10)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# valgrind test is failing with memory allocated in libjpeg
 | 
					# valgrind test is failing with memory allocated in libjpeg
 | 
				
			||||||
@pytest.mark.valgrind_known_error(reason="Known Failing")
 | 
					@pytest.mark.valgrind_known_error(reason="Known Failing")
 | 
				
			||||||
def test_DCT_scaling_edges():
 | 
					def test_DCT_scaling_edges():
 | 
				
			||||||
| 
						 | 
					@ -130,4 +140,4 @@ def test_reducing_gap_for_DCT_scaling():
 | 
				
			||||||
        with Image.open("Tests/images/hopper.jpg") as im:
 | 
					        with Image.open("Tests/images/hopper.jpg") as im:
 | 
				
			||||||
            im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
 | 
					            im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            assert_image_equal(ref, im)
 | 
					            assert_image_similar(ref, im, 1.4)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -655,6 +655,20 @@ def test_polygon_1px_high():
 | 
				
			||||||
    assert_image_equal_tofile(im, expected)
 | 
					    assert_image_equal_tofile(im, expected)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_polygon_1px_high_translucent():
 | 
				
			||||||
 | 
					    # Test drawing a translucent 1px high polygon
 | 
				
			||||||
 | 
					    # Arrange
 | 
				
			||||||
 | 
					    im = Image.new("RGB", (4, 3))
 | 
				
			||||||
 | 
					    draw = ImageDraw.Draw(im, "RGBA")
 | 
				
			||||||
 | 
					    expected = "Tests/images/imagedraw_polygon_1px_high_translucent.png"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Act
 | 
				
			||||||
 | 
					    draw.polygon([(1, 1), (1, 1), (3, 1), (3, 1)], (255, 0, 0, 127))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Assert
 | 
				
			||||||
 | 
					    assert_image_equal_tofile(im, expected)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_polygon_translucent():
 | 
					def test_polygon_translucent():
 | 
				
			||||||
    # Arrange
 | 
					    # Arrange
 | 
				
			||||||
    im = Image.new("RGB", (W, H))
 | 
					    im = Image.new("RGB", (W, H))
 | 
				
			||||||
| 
						 | 
					@ -1440,3 +1454,23 @@ def test_continuous_horizontal_edges_polygon():
 | 
				
			||||||
    assert_image_equal_tofile(
 | 
					    assert_image_equal_tofile(
 | 
				
			||||||
        img, expected, "continuous horizontal edges polygon failed"
 | 
					        img, expected, "continuous horizontal edges polygon failed"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_discontiguous_corners_polygon():
 | 
				
			||||||
 | 
					    img, draw = create_base_image_draw((84, 68))
 | 
				
			||||||
 | 
					    draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK)
 | 
				
			||||||
 | 
					    draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK)
 | 
				
			||||||
 | 
					    draw.polygon(
 | 
				
			||||||
 | 
					        ((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)),
 | 
				
			||||||
 | 
					        BLACK,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
 | 
				
			||||||
 | 
					    assert_image_similar_tofile(img, expected, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_polygon():
 | 
				
			||||||
 | 
					    im = Image.new("RGB", (W, H))
 | 
				
			||||||
 | 
					    draw = ImageDraw.Draw(im)
 | 
				
			||||||
 | 
					    draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red")
 | 
				
			||||||
 | 
					    expected = "Tests/images/imagedraw_outline_polygon_RGB.png"
 | 
				
			||||||
 | 
					    assert_image_similar_tofile(im, expected, 1)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,15 @@ from io import BytesIO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import BmpImagePlugin, EpsImagePlugin, Image, ImageFile, _binary, features
 | 
					from PIL import (
 | 
				
			||||||
 | 
					    BmpImagePlugin,
 | 
				
			||||||
 | 
					    EpsImagePlugin,
 | 
				
			||||||
 | 
					    Image,
 | 
				
			||||||
 | 
					    ImageFile,
 | 
				
			||||||
 | 
					    UnidentifiedImageError,
 | 
				
			||||||
 | 
					    _binary,
 | 
				
			||||||
 | 
					    features,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .helper import (
 | 
					from .helper import (
 | 
				
			||||||
    assert_image,
 | 
					    assert_image,
 | 
				
			||||||
| 
						 | 
					@ -35,9 +43,9 @@ class TestImageFile:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            parser = ImageFile.Parser()
 | 
					            parser = ImageFile.Parser()
 | 
				
			||||||
            parser.feed(data)
 | 
					            parser.feed(data)
 | 
				
			||||||
            imOut = parser.close()
 | 
					            im_out = parser.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return im, imOut
 | 
					            return im, im_out
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert_image_equal(*roundtrip("BMP"))
 | 
					        assert_image_equal(*roundtrip("BMP"))
 | 
				
			||||||
        im1, im2 = roundtrip("GIF")
 | 
					        im1, im2 = roundtrip("GIF")
 | 
				
			||||||
| 
						 | 
					@ -200,6 +208,9 @@ class MockPyEncoder(ImageFile.PyEncoder):
 | 
				
			||||||
    def encode(self, buffer):
 | 
					    def encode(self, buffer):
 | 
				
			||||||
        return 1, 1, b""
 | 
					        return 1, 1, b""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def cleanup(self):
 | 
				
			||||||
 | 
					        self.cleanup_called = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
xoff, yoff, xsize, ysize = 10, 20, 100, 100
 | 
					xoff, yoff, xsize, ysize = 10, 20, 100, 100
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -327,10 +338,12 @@ class TestPyEncoder(CodecsTest):
 | 
				
			||||||
        im = MockImageFile(buf)
 | 
					        im = MockImageFile(buf)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fp = BytesIO()
 | 
					        fp = BytesIO()
 | 
				
			||||||
 | 
					        self.encoder.cleanup_called = False
 | 
				
			||||||
        with pytest.raises(ValueError):
 | 
					        with pytest.raises(ValueError):
 | 
				
			||||||
            ImageFile._save(
 | 
					            ImageFile._save(
 | 
				
			||||||
                im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
 | 
					                im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					        assert self.encoder.cleanup_called
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with pytest.raises(ValueError):
 | 
					        with pytest.raises(ValueError):
 | 
				
			||||||
            ImageFile._save(
 | 
					            ImageFile._save(
 | 
				
			||||||
| 
						 | 
					@ -372,3 +385,7 @@ class TestPyEncoder(CodecsTest):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with pytest.raises(NotImplementedError):
 | 
					        with pytest.raises(NotImplementedError):
 | 
				
			||||||
            encoder.encode_to_file(None, None)
 | 
					            encoder.encode_to_file(None, None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_zero_height(self):
 | 
				
			||||||
 | 
					        with pytest.raises(UnidentifiedImageError):
 | 
				
			||||||
 | 
					            Image.open("Tests/images/zero_height.j2k")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,9 +65,12 @@ class TestImageFont:
 | 
				
			||||||
        return font_bytes
 | 
					        return font_bytes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_font_with_filelike(self):
 | 
					    def test_font_with_filelike(self):
 | 
				
			||||||
        ImageFont.truetype(
 | 
					        ttf = ImageFont.truetype(
 | 
				
			||||||
            self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE
 | 
					            self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					        ttf_copy = ttf.font_variant()
 | 
				
			||||||
 | 
					        assert ttf_copy.font_bytes == ttf.font_bytes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self._render(self._font_as_bytes())
 | 
					        self._render(self._font_as_bytes())
 | 
				
			||||||
        # Usage note:  making two fonts from the same buffer fails.
 | 
					        # Usage note:  making two fonts from the same buffer fails.
 | 
				
			||||||
        # shared_bytes = self._font_as_bytes()
 | 
					        # shared_bytes = self._font_as_bytes()
 | 
				
			||||||
| 
						 | 
					@ -977,6 +980,14 @@ class TestImageFont:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
 | 
					        assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_fill_deprecation(self):
 | 
				
			||||||
 | 
					        font = self.get_font()
 | 
				
			||||||
 | 
					        with pytest.warns(DeprecationWarning):
 | 
				
			||||||
 | 
					            font.getmask2("Hello world", fill=Image.core.fill)
 | 
				
			||||||
 | 
					        with pytest.warns(DeprecationWarning):
 | 
				
			||||||
 | 
					            with pytest.raises(TypeError):
 | 
				
			||||||
 | 
					                font.getmask2("Hello world", fill=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@skip_unless_feature("raqm")
 | 
					@skip_unless_feature("raqm")
 | 
				
			||||||
class TestImageFont_RaqmLayout(TestImageFont):
 | 
					class TestImageFont_RaqmLayout(TestImageFont):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,12 +48,8 @@ def img_string_normalize(im):
 | 
				
			||||||
    return img_to_string(string_to_img(im))
 | 
					    return img_to_string(string_to_img(im))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def assert_img_equal(A, B):
 | 
					def assert_img_equal_img_string(a, b_string):
 | 
				
			||||||
    assert img_to_string(A) == img_to_string(B)
 | 
					    assert img_to_string(a) == img_string_normalize(b_string)
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def assert_img_equal_img_string(A, Bstring):
 | 
					 | 
				
			||||||
    assert img_to_string(A) == img_string_normalize(Bstring)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_str_to_img():
 | 
					def test_str_to_img():
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,6 +63,7 @@ def test_sanity():
 | 
				
			||||||
    ImageOps.grayscale(hopper("L"))
 | 
					    ImageOps.grayscale(hopper("L"))
 | 
				
			||||||
    ImageOps.grayscale(hopper("RGB"))
 | 
					    ImageOps.grayscale(hopper("RGB"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ImageOps.invert(hopper("1"))
 | 
				
			||||||
    ImageOps.invert(hopper("L"))
 | 
					    ImageOps.invert(hopper("L"))
 | 
				
			||||||
    ImageOps.invert(hopper("RGB"))
 | 
					    ImageOps.invert(hopper("RGB"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -174,7 +174,7 @@ def test_overflow_segfault():
 | 
				
			||||||
    # through to the sequence. Seeing this on 32-bit Windows.
 | 
					    # through to the sequence. Seeing this on 32-bit Windows.
 | 
				
			||||||
    with pytest.raises((TypeError, MemoryError)):
 | 
					    with pytest.raises((TypeError, MemoryError)):
 | 
				
			||||||
        # post patch, this fails with a memory error
 | 
					        # post patch, this fails with a memory error
 | 
				
			||||||
        x = evil()
 | 
					        x = Evil()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # This fails due to the invalid malloc above,
 | 
					        # This fails due to the invalid malloc above,
 | 
				
			||||||
        # and segfaults
 | 
					        # and segfaults
 | 
				
			||||||
| 
						 | 
					@ -182,7 +182,7 @@ def test_overflow_segfault():
 | 
				
			||||||
            x[i] = b"0" * 16
 | 
					            x[i] = b"0" * 16
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class evil:
 | 
					class Evil:
 | 
				
			||||||
    def __init__(self):
 | 
					    def __init__(self):
 | 
				
			||||||
        self.corrupt = Image.core.path(0x4000000000000000)
 | 
					        self.corrupt = Image.core.path(0x4000000000000000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,10 +2,13 @@ import warnings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import ImageQt
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from .helper import assert_image_similar, hopper
 | 
					from .helper import assert_image_similar, hopper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					with warnings.catch_warnings() as w:
 | 
				
			||||||
 | 
					    warnings.simplefilter("ignore", category=DeprecationWarning)
 | 
				
			||||||
 | 
					    from PIL import ImageQt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pytestmark = pytest.mark.skipif(
 | 
					pytestmark = pytest.mark.skipif(
 | 
				
			||||||
    not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
 | 
					    not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,21 +65,21 @@ def test_libtiff():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_consecutive():
 | 
					def test_consecutive():
 | 
				
			||||||
    with Image.open("Tests/images/multipage.tiff") as im:
 | 
					    with Image.open("Tests/images/multipage.tiff") as im:
 | 
				
			||||||
        firstFrame = None
 | 
					        first_frame = None
 | 
				
			||||||
        for frame in ImageSequence.Iterator(im):
 | 
					        for frame in ImageSequence.Iterator(im):
 | 
				
			||||||
            if firstFrame is None:
 | 
					            if first_frame is None:
 | 
				
			||||||
                firstFrame = frame.copy()
 | 
					                first_frame = frame.copy()
 | 
				
			||||||
        for frame in ImageSequence.Iterator(im):
 | 
					        for frame in ImageSequence.Iterator(im):
 | 
				
			||||||
            assert_image_equal(frame, firstFrame)
 | 
					            assert_image_equal(frame, first_frame)
 | 
				
			||||||
            break
 | 
					            break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_palette_mmap():
 | 
					def test_palette_mmap():
 | 
				
			||||||
    # Using mmap in ImageFile can require to reload the palette.
 | 
					    # Using mmap in ImageFile can require to reload the palette.
 | 
				
			||||||
    with Image.open("Tests/images/multipage-mmap.tiff") as im:
 | 
					    with Image.open("Tests/images/multipage-mmap.tiff") as im:
 | 
				
			||||||
        color1 = im.getpalette()[0:3]
 | 
					        color1 = im.getpalette()[:3]
 | 
				
			||||||
        im.seek(0)
 | 
					        im.seek(0)
 | 
				
			||||||
        color2 = im.getpalette()[0:3]
 | 
					        color2 = im.getpalette()[:3]
 | 
				
			||||||
        assert color1 == color2
 | 
					        assert color1 == color2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,8 @@ def setup_module():
 | 
				
			||||||
        # setup tk
 | 
					        # setup tk
 | 
				
			||||||
        tk.Frame()
 | 
					        tk.Frame()
 | 
				
			||||||
        # root = tk.Tk()
 | 
					        # root = tk.Tk()
 | 
				
			||||||
 | 
					    except RuntimeError as v:
 | 
				
			||||||
 | 
					        pytest.skip(f"RuntimeError: {v}")
 | 
				
			||||||
    except tk.TclError as v:
 | 
					    except tk.TclError as v:
 | 
				
			||||||
        pytest.skip(f"TCL Error: {v}")
 | 
					        pytest.skip(f"TCL Error: {v}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -75,8 +77,16 @@ def test_photoimage_blank():
 | 
				
			||||||
        assert im_tk.width() == 100
 | 
					        assert im_tk.width() == 100
 | 
				
			||||||
        assert im_tk.height() == 100
 | 
					        assert im_tk.height() == 100
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # reloaded = ImageTk.getimage(im_tk)
 | 
					        im = Image.new(mode, (100, 100))
 | 
				
			||||||
        # assert_image_equal(reloaded, im)
 | 
					        reloaded = ImageTk.getimage(im_tk)
 | 
				
			||||||
 | 
					        assert_image_equal(reloaded.convert(mode), im)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_box_deprecation():
 | 
				
			||||||
 | 
					    im = hopper()
 | 
				
			||||||
 | 
					    im_tk = ImageTk.PhotoImage(im)
 | 
				
			||||||
 | 
					    with pytest.warns(DeprecationWarning):
 | 
				
			||||||
 | 
					        im_tk.paste(im, (0, 0, 128, 128))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_bitmapimage():
 | 
					def test_bitmapimage():
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,3 @@
 | 
				
			||||||
import ctypes
 | 
					 | 
				
			||||||
from io import BytesIO
 | 
					from io import BytesIO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import Image, ImageWin
 | 
					from PIL import Image, ImageWin
 | 
				
			||||||
| 
						 | 
					@ -8,6 +7,7 @@ from .helper import hopper, is_win32
 | 
				
			||||||
# see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652
 | 
					# see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if is_win32():
 | 
					if is_win32():
 | 
				
			||||||
 | 
					    import ctypes
 | 
				
			||||||
    import ctypes.wintypes
 | 
					    import ctypes.wintypes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class BITMAPFILEHEADER(ctypes.Structure):
 | 
					    class BITMAPFILEHEADER(ctypes.Structure):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -444,6 +444,8 @@ class TestLibUnpack:
 | 
				
			||||||
        self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0))
 | 
					        self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0))
 | 
				
			||||||
        self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16))
 | 
					        self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16))
 | 
				
			||||||
        self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15))
 | 
					        self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15))
 | 
				
			||||||
 | 
					        self.assert_unpack("RGBA", "BGRA;16L", 8, (6, 4, 2, 8), (14, 12, 10, 16))
 | 
				
			||||||
 | 
					        self.assert_unpack("RGBA", "BGRA;16B", 8, (5, 3, 1, 7), (13, 11, 9, 15))
 | 
				
			||||||
        self.assert_unpack(
 | 
					        self.assert_unpack(
 | 
				
			||||||
            "RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)
 | 
					            "RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,51 +26,51 @@ def test_basic(tmp_path):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def basic(mode):
 | 
					    def basic(mode):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        imIn = original.convert(mode)
 | 
					        im_in = original.convert(mode)
 | 
				
			||||||
        verify(imIn)
 | 
					        verify(im_in)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        w, h = imIn.size
 | 
					        w, h = im_in.size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        imOut = imIn.copy()
 | 
					        im_out = im_in.copy()
 | 
				
			||||||
        verify(imOut)  # copy
 | 
					        verify(im_out)  # copy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        imOut = imIn.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
 | 
					        im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h))
 | 
				
			||||||
        verify(imOut)  # transform
 | 
					        verify(im_out)  # transform
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        filename = str(tmp_path / "temp.im")
 | 
					        filename = str(tmp_path / "temp.im")
 | 
				
			||||||
        imIn.save(filename)
 | 
					        im_in.save(filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with Image.open(filename) as imOut:
 | 
					        with Image.open(filename) as im_out:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            verify(imIn)
 | 
					            verify(im_in)
 | 
				
			||||||
            verify(imOut)
 | 
					            verify(im_out)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        imOut = imIn.crop((0, 0, w, h))
 | 
					        im_out = im_in.crop((0, 0, w, h))
 | 
				
			||||||
        verify(imOut)
 | 
					        verify(im_out)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        imOut = Image.new(mode, (w, h), None)
 | 
					        im_out = Image.new(mode, (w, h), None)
 | 
				
			||||||
        imOut.paste(imIn.crop((0, 0, w // 2, h)), (0, 0))
 | 
					        im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0))
 | 
				
			||||||
        imOut.paste(imIn.crop((w // 2, 0, w, h)), (w // 2, 0))
 | 
					        im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        verify(imIn)
 | 
					        verify(im_in)
 | 
				
			||||||
        verify(imOut)
 | 
					        verify(im_out)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        imIn = Image.new(mode, (1, 1), 1)
 | 
					        im_in = Image.new(mode, (1, 1), 1)
 | 
				
			||||||
        assert imIn.getpixel((0, 0)) == 1
 | 
					        assert im_in.getpixel((0, 0)) == 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        imIn.putpixel((0, 0), 2)
 | 
					        im_in.putpixel((0, 0), 2)
 | 
				
			||||||
        assert imIn.getpixel((0, 0)) == 2
 | 
					        assert im_in.getpixel((0, 0)) == 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if mode == "L":
 | 
					        if mode == "L":
 | 
				
			||||||
            maximum = 255
 | 
					            maximum = 255
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            maximum = 32767
 | 
					            maximum = 32767
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        imIn = Image.new(mode, (1, 1), 256)
 | 
					        im_in = Image.new(mode, (1, 1), 256)
 | 
				
			||||||
        assert imIn.getpixel((0, 0)) == min(256, maximum)
 | 
					        assert im_in.getpixel((0, 0)) == min(256, maximum)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        imIn.putpixel((0, 0), 512)
 | 
					        im_in.putpixel((0, 0), 512)
 | 
				
			||||||
        assert imIn.getpixel((0, 0)) == min(512, maximum)
 | 
					        assert im_in.getpixel((0, 0)) == min(512, maximum)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    basic("L")
 | 
					    basic("L")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,10 @@
 | 
				
			||||||
 | 
					import warnings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import ImageQt
 | 
					with warnings.catch_warnings():
 | 
				
			||||||
 | 
					    warnings.simplefilter("ignore", category=DeprecationWarning)
 | 
				
			||||||
 | 
					    from PIL import ImageQt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
 | 
					from .helper import assert_image_equal, assert_image_equal_tofile, hopper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,10 @@
 | 
				
			||||||
 | 
					import warnings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PIL import ImageQt
 | 
					with warnings.catch_warnings():
 | 
				
			||||||
 | 
					    warnings.simplefilter("ignore", category=DeprecationWarning)
 | 
				
			||||||
 | 
					    from PIL import ImageQt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
 | 
					from .helper import assert_image_equal, assert_image_equal_tofile, hopper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ def test_is_path():
 | 
				
			||||||
    fp = "filename.ext"
 | 
					    fp = "filename.ext"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Act
 | 
					    # Act
 | 
				
			||||||
    it_is = _util.isPath(fp)
 | 
					    it_is = _util.is_path(fp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Assert
 | 
					    # Assert
 | 
				
			||||||
    assert it_is
 | 
					    assert it_is
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ def test_path_obj_is_path():
 | 
				
			||||||
    test_path = Path("filename.ext")
 | 
					    test_path = Path("filename.ext")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Act
 | 
					    # Act
 | 
				
			||||||
    it_is = _util.isPath(test_path)
 | 
					    it_is = _util.is_path(test_path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Assert
 | 
					    # Assert
 | 
				
			||||||
    assert it_is
 | 
					    assert it_is
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,7 @@ def test_is_not_path(tmp_path):
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Act
 | 
					    # Act
 | 
				
			||||||
    it_is_not = _util.isPath(fp)
 | 
					    it_is_not = _util.is_path(fp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Assert
 | 
					    # Assert
 | 
				
			||||||
    assert not it_is_not
 | 
					    assert not it_is_not
 | 
				
			||||||
| 
						 | 
					@ -44,7 +44,7 @@ def test_is_directory():
 | 
				
			||||||
    directory = "Tests"
 | 
					    directory = "Tests"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Act
 | 
					    # Act
 | 
				
			||||||
    it_is = _util.isDirectory(directory)
 | 
					    it_is = _util.is_directory(directory)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Assert
 | 
					    # Assert
 | 
				
			||||||
    assert it_is
 | 
					    assert it_is
 | 
				
			||||||
| 
						 | 
					@ -55,7 +55,7 @@ def test_is_not_directory():
 | 
				
			||||||
    text = "abc"
 | 
					    text = "abc"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Act
 | 
					    # Act
 | 
				
			||||||
    it_is_not = _util.isDirectory(text)
 | 
					    it_is_not = _util.is_directory(text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Assert
 | 
					    # Assert
 | 
				
			||||||
    assert not it_is_not
 | 
					    assert not it_is_not
 | 
				
			||||||
| 
						 | 
					@ -65,7 +65,7 @@ def test_deferred_error():
 | 
				
			||||||
    # Arrange
 | 
					    # Arrange
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Act
 | 
					    # Act
 | 
				
			||||||
    thing = _util.deferred_error(ValueError("Some error text"))
 | 
					    thing = _util.DeferredError(ValueError("Some error text"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Assert
 | 
					    # Assert
 | 
				
			||||||
    with pytest.raises(ValueError):
 | 
					    with pytest.raises(ValueError):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
#!/bin/bash
 | 
					#!/bin/bash
 | 
				
			||||||
# install openjpeg
 | 
					# install openjpeg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
archive=openjpeg-2.4.0
 | 
					archive=openjpeg-2.5.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
./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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										17
									
								
								docs/conf.py
									
									
									
									
									
								
							
							
						
						| 
						 | 
					@ -16,8 +16,6 @@
 | 
				
			||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
 | 
					# documentation root, use os.path.abspath to make it absolute, like shown here.
 | 
				
			||||||
# sys.path.insert(0, os.path.abspath('.'))
 | 
					# sys.path.insert(0, os.path.abspath('.'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import sphinx_rtd_theme
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import PIL
 | 
					import PIL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# -- General configuration ------------------------------------------------
 | 
					# -- General configuration ------------------------------------------------
 | 
				
			||||||
| 
						 | 
					@ -70,7 +68,7 @@ release = PIL.__version__
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# This is also used if you do content translation via gettext catalogs.
 | 
					# This is also used if you do content translation via gettext catalogs.
 | 
				
			||||||
# Usually you set "language" from the command line for these cases.
 | 
					# Usually you set "language" from the command line for these cases.
 | 
				
			||||||
language = None
 | 
					language = "en"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# There are two options for replacing |today|: either, you set today to some
 | 
					# There are two options for replacing |today|: either, you set today to some
 | 
				
			||||||
# non-false value, then it is used:
 | 
					# non-false value, then it is used:
 | 
				
			||||||
| 
						 | 
					@ -126,13 +124,15 @@ nitpicky = True
 | 
				
			||||||
# The theme to use for HTML and HTML Help pages.  See the documentation for
 | 
					# The theme to use for HTML and HTML Help pages.  See the documentation for
 | 
				
			||||||
# a list of builtin themes.
 | 
					# a list of builtin themes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
html_theme = "sphinx_rtd_theme"
 | 
					html_theme = "furo"
 | 
				
			||||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Theme options are theme-specific and customize the look and feel of a theme
 | 
					# Theme options are theme-specific and customize the look and feel of a theme
 | 
				
			||||||
# further.  For a list of options available for each theme, see the
 | 
					# further.  For a list of options available for each theme, see the
 | 
				
			||||||
# documentation.
 | 
					# documentation.
 | 
				
			||||||
# html_theme_options = {}
 | 
					html_theme_options = {
 | 
				
			||||||
 | 
					    "light_logo": "pillow-logo-dark-text.png",
 | 
				
			||||||
 | 
					    "dark_logo": "pillow-logo.png",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Add any paths that contain custom themes here, relative to this directory.
 | 
					# Add any paths that contain custom themes here, relative to this directory.
 | 
				
			||||||
# html_theme_path = []
 | 
					# html_theme_path = []
 | 
				
			||||||
| 
						 | 
					@ -146,7 +146,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# The name of an image file (relative to this directory) to place at the top
 | 
					# The name of an image file (relative to this directory) to place at the top
 | 
				
			||||||
# of the sidebar.
 | 
					# of the sidebar.
 | 
				
			||||||
html_logo = "resources/pillow-logo.png"
 | 
					# html_logo = "resources/pillow-logo.png"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# The name of an image file (within the static path) to use as favicon of the
 | 
					# The name of an image file (within the static path) to use as favicon of the
 | 
				
			||||||
# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 | 
					# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 | 
				
			||||||
| 
						 | 
					@ -311,10 +311,7 @@ texinfo_documents = [
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def setup(app):
 | 
					def setup(app):
 | 
				
			||||||
    app.add_js_file("js/script.js")
 | 
					 | 
				
			||||||
    app.add_css_file("css/styles.css")
 | 
					 | 
				
			||||||
    app.add_css_file("css/dark.css")
 | 
					    app.add_css_file("css/dark.css")
 | 
				
			||||||
    app.add_css_file("css/light.css")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# GitHub repo for sphinx-issues
 | 
					# GitHub repo for sphinx-issues
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,7 +69,7 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged.
 | 
				
			||||||
Constants
 | 
					Constants
 | 
				
			||||||
~~~~~~~~~
 | 
					~~~~~~~~~
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. deprecated:: 9.2.0
 | 
					.. deprecated:: 9.1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
A number of constants have been deprecated and will be removed in Pillow 10.0.0
 | 
					A number of constants have been deprecated and will be removed in Pillow 10.0.0
 | 
				
			||||||
(2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
 | 
					(2023-07-01). Instead, ``enum.IntEnum`` classes have been added.
 | 
				
			||||||
| 
						 | 
					@ -97,8 +97,8 @@ Deprecated                                             Use instead
 | 
				
			||||||
``Image.TRANSPOSE``                                    ``Image.Transpose.TRANSPOSE``
 | 
					``Image.TRANSPOSE``                                    ``Image.Transpose.TRANSPOSE``
 | 
				
			||||||
``Image.TRANSVERSE``                                   ``Image.Transpose.TRANSVERSE``
 | 
					``Image.TRANSVERSE``                                   ``Image.Transpose.TRANSVERSE``
 | 
				
			||||||
``Image.BOX``                                          ``Image.Resampling.BOX``
 | 
					``Image.BOX``                                          ``Image.Resampling.BOX``
 | 
				
			||||||
``Image.BILINEAR``                                     ``Image.Resampling.BILNEAR``
 | 
					``Image.BILINEAR``                                     ``Image.Resampling.BILINEAR``
 | 
				
			||||||
``Image.LINEAR``                                       ``Image.Resampling.BILNEAR``
 | 
					``Image.LINEAR``                                       ``Image.Resampling.BILINEAR``
 | 
				
			||||||
``Image.HAMMING``                                      ``Image.Resampling.HAMMING``
 | 
					``Image.HAMMING``                                      ``Image.Resampling.HAMMING``
 | 
				
			||||||
``Image.BICUBIC``                                      ``Image.Resampling.BICUBIC``
 | 
					``Image.BICUBIC``                                      ``Image.Resampling.BICUBIC``
 | 
				
			||||||
``Image.CUBIC``                                        ``Image.Resampling.BICUBIC``
 | 
					``Image.CUBIC``                                        ``Image.Resampling.BICUBIC``
 | 
				
			||||||
| 
						 | 
					@ -142,6 +142,42 @@ The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be re
 | 
				
			||||||
Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
 | 
					Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through
 | 
				
			||||||
:mod:`~PIL.FitsImagePlugin` instead.
 | 
					:mod:`~PIL.FitsImagePlugin` instead.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FreeTypeFont.getmask2 fill parameter
 | 
				
			||||||
 | 
					~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. deprecated:: 9.2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been
 | 
				
			||||||
 | 
					deprecated and will be removed in Pillow 10 (2023-07-01).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PhotoImage.paste box parameter
 | 
				
			||||||
 | 
					~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. deprecated:: 9.2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PyQt5 and PySide2
 | 
				
			||||||
 | 
					~~~~~~~~~~~~~~~~~
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. deprecated:: 9.2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`Qt 5 reached end-of-life <https://www.qt.io/blog/qt-5.15-released>`_ on 2020-12-08 for
 | 
				
			||||||
 | 
					open-source users (and will reach EOL on 2023-12-08 for commercial licence holders).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed
 | 
				
			||||||
 | 
					in Pillow 10 (2023-07-01). Upgrade to
 | 
				
			||||||
 | 
					`PyQt6 <https://www.riverbankcomputing.com/static/Docs/PyQt6/>`_ or
 | 
				
			||||||
 | 
					`PySide6 <https://doc.qt.io/qtforpython/>`_ instead.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Image.coerce_e
 | 
				
			||||||
 | 
					~~~~~~~~~~~~~~
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. deprecated:: 9.2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This undocumented method has been deprecated and will be removed in Pillow 10
 | 
				
			||||||
 | 
					(2023-07-01).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Removed features
 | 
					Removed features
 | 
				
			||||||
----------------
 | 
					----------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -253,7 +253,7 @@ class DXT1Decoder(ImageFile.PyDecoder):
 | 
				
			||||||
            self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
 | 
					            self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
 | 
				
			||||||
        except struct.error as e:
 | 
					        except struct.error as e:
 | 
				
			||||||
            raise OSError("Truncated DDS file") from e
 | 
					            raise OSError("Truncated DDS file") from e
 | 
				
			||||||
        return 0, 0
 | 
					        return -1, 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DXT5Decoder(ImageFile.PyDecoder):
 | 
					class DXT5Decoder(ImageFile.PyDecoder):
 | 
				
			||||||
| 
						 | 
					@ -264,7 +264,7 @@ class DXT5Decoder(ImageFile.PyDecoder):
 | 
				
			||||||
            self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
 | 
					            self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
 | 
				
			||||||
        except struct.error as e:
 | 
					        except struct.error as e:
 | 
				
			||||||
            raise OSError("Truncated DDS file") from e
 | 
					            raise OSError("Truncated DDS file") from e
 | 
				
			||||||
        return 0, 0
 | 
					        return -1, 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Image.register_decoder("DXT1", DXT1Decoder)
 | 
					Image.register_decoder("DXT1", DXT1Decoder)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,15 +17,13 @@ When an image is opened from a file, only that instance of the image is consider
 | 
				
			||||||
have the format. Copies of the image will contain data loaded from the file, but not
 | 
					have the format. Copies of the image will contain data loaded from the file, but not
 | 
				
			||||||
the file itself, meaning that it can no longer be considered to be in the original
 | 
					the file itself, meaning that it can no longer be considered to be in the original
 | 
				
			||||||
format. So if :py:meth:`~PIL.Image.Image.copy` is called on an image, or another method
 | 
					format. So if :py:meth:`~PIL.Image.Image.copy` is called on an image, or another method
 | 
				
			||||||
internally creates a copy of the image, the ``fp`` (file pointer), along with any
 | 
					internally creates a copy of the image, then any methods or attributes specific to the
 | 
				
			||||||
methods and attributes specific to a format. The :py:attr:`~PIL.Image.Image.format`
 | 
					format will no longer be present. The ``fp`` (file pointer) attribute will no longer be
 | 
				
			||||||
attribute will be ``None``.
 | 
					present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Fully supported formats
 | 
					Fully supported formats
 | 
				
			||||||
-----------------------
 | 
					-----------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. contents::
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
BLP
 | 
					BLP
 | 
				
			||||||
^^^
 | 
					^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,8 +42,9 @@ BMP
 | 
				
			||||||
^^^
 | 
					^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``,
 | 
					Pillow reads and writes Windows and OS/2 BMP files containing ``1``, ``L``, ``P``,
 | 
				
			||||||
or ``RGB`` data. 16-colour images are read as ``P`` images. Run-length encoding
 | 
					or ``RGB`` data. 16-colour images are read as ``P`` images. 4-bit run-length encoding
 | 
				
			||||||
is not supported.
 | 
					is not supported. Support for reading 8-bit run-length encoding was added in Pillow
 | 
				
			||||||
 | 
					9.1.0.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The :py:meth:`~PIL.Image.open` method sets the following
 | 
					The :py:meth:`~PIL.Image.open` method sets the following
 | 
				
			||||||
:py:attr:`~PIL.Image.Image.info` properties:
 | 
					:py:attr:`~PIL.Image.Image.info` properties:
 | 
				
			||||||
| 
						 | 
					@ -102,12 +101,38 @@ GIF
 | 
				
			||||||
^^^
 | 
					^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Pillow reads GIF87a and GIF89a versions of the GIF file format. The library
 | 
					Pillow reads GIF87a and GIF89a versions of the GIF file format. The library
 | 
				
			||||||
writes run-length encoded files in GIF87a by default, unless GIF89a features
 | 
					writes files in GIF87a by default, unless GIF89a features are used or GIF89a is
 | 
				
			||||||
are used or GIF89a is already in use.
 | 
					already in use. Files are written with LZW encoding.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
GIF files are initially read as grayscale (``L``) or palette mode (``P``)
 | 
					GIF files are initially read as grayscale (``L``) or palette mode (``P``)
 | 
				
			||||||
images, but seeking to later frames in an image will change the mode to either
 | 
					images. Seeking to later frames in a ``P`` image will change the image to
 | 
				
			||||||
``RGB`` or ``RGBA``, depending on whether the first frame had transparency.
 | 
					``RGB`` (or ``RGBA`` if the first frame had transparency).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					``P`` mode images are changed to ``RGB`` because each frame of a GIF may contain
 | 
				
			||||||
 | 
					its own individual palette of up to 256 colors. When a new frame is placed onto a
 | 
				
			||||||
 | 
					previous frame, those colors may combine to exceed the ``P`` mode limit of 256
 | 
				
			||||||
 | 
					colors. Instead, the image is converted to ``RGB`` handle this.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you would prefer the first ``P`` image frame to be ``RGB`` as well, so that
 | 
				
			||||||
 | 
					every ``P`` frame is converted to ``RGB`` or ``RGBA`` mode, there is a setting
 | 
				
			||||||
 | 
					available::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from PIL import GifImagePlugin
 | 
				
			||||||
 | 
					    GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GIF frames do not always contain individual palettes however. If there is only
 | 
				
			||||||
 | 
					a global palette, then all of the colors can fit within ``P`` mode. If you would
 | 
				
			||||||
 | 
					prefer the frames to be kept as ``P`` in that case, there is also a setting
 | 
				
			||||||
 | 
					available::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from PIL import GifImagePlugin
 | 
				
			||||||
 | 
					    GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To restore the default behavior, where ``P`` mode images are only converted to
 | 
				
			||||||
 | 
					``RGB`` or ``RGBA`` after the first frame::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from PIL import GifImagePlugin
 | 
				
			||||||
 | 
					    GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The :py:meth:`~PIL.Image.open` method sets the following
 | 
					The :py:meth:`~PIL.Image.open` method sets the following
 | 
				
			||||||
:py:attr:`~PIL.Image.Image.info` properties:
 | 
					:py:attr:`~PIL.Image.Image.info` properties:
 | 
				
			||||||
| 
						 | 
					@ -131,7 +156,8 @@ The :py:meth:`~PIL.Image.open` method sets the following
 | 
				
			||||||
    it will loop forever.
 | 
					    it will loop forever.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**comment**
 | 
					**comment**
 | 
				
			||||||
    May not be present. A comment about the image.
 | 
					    May not be present. A comment about the image. This is the last comment found
 | 
				
			||||||
 | 
					    before the current frame's image.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**extension**
 | 
					**extension**
 | 
				
			||||||
    May not be present. Contains application specific information.
 | 
					    May not be present. Contains application specific information.
 | 
				
			||||||
| 
						 | 
					@ -220,17 +246,14 @@ Reading local images
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The GIF loader creates an image memory the same size as the GIF file’s *logical
 | 
					The GIF loader creates an image memory the same size as the GIF file’s *logical
 | 
				
			||||||
screen size*, and pastes the actual pixel data (the *local image*) into this
 | 
					screen size*, and pastes the actual pixel data (the *local image*) into this
 | 
				
			||||||
image. If you only want the actual pixel rectangle, you can manipulate the
 | 
					image. If you only want the actual pixel rectangle, you can crop the image::
 | 
				
			||||||
:py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.ImageFile.ImageFile.tile`
 | 
					 | 
				
			||||||
attributes before loading the file::
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    im = Image.open(...)
 | 
					    im = Image.open(...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if im.tile[0][0] == "gif":
 | 
					    if im.tile[0][0] == "gif":
 | 
				
			||||||
        # only read the first "local image" from this GIF file
 | 
					        # only read the first "local image" from this GIF file
 | 
				
			||||||
        tag, (x0, y0, x1, y1), offset, extra = im.tile[0]
 | 
					        box = im.tile[0][1]
 | 
				
			||||||
        im.size = (x1 - x0, y1 - y0)
 | 
					        im = im.crop(box)
 | 
				
			||||||
        im.tile = [(tag, (0, 0) + im.size, offset, extra)]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
ICNS
 | 
					ICNS
 | 
				
			||||||
^^^^
 | 
					^^^^
 | 
				
			||||||
| 
						 | 
					@ -364,10 +387,12 @@ The :py:meth:`~PIL.Image.open` method may set the following
 | 
				
			||||||
The :py:meth:`~PIL.Image.Image.save` method supports the following options:
 | 
					The :py:meth:`~PIL.Image.Image.save` method supports the following options:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**quality**
 | 
					**quality**
 | 
				
			||||||
    The image quality, on a scale from 0 (worst) to 95 (best). The default is
 | 
					    The image quality, on a scale from 0 (worst) to 95 (best), or the string
 | 
				
			||||||
    75. Values above 95 should be avoided; 100 disables portions of the JPEG
 | 
					    ``keep``. The default is 75. Values above 95 should be avoided; 100 disables
 | 
				
			||||||
    compression algorithm, and results in large files with hardly any gain in
 | 
					    portions of the JPEG compression algorithm, and results in large files with
 | 
				
			||||||
    image quality.
 | 
					    hardly any gain in image quality. The value ``keep`` is only valid for JPEG
 | 
				
			||||||
 | 
					    files and will retain the original image quality level, subsampling, and
 | 
				
			||||||
 | 
					    qtables.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**optimize**
 | 
					**optimize**
 | 
				
			||||||
    If present and true, indicates that the encoder should make an extra pass
 | 
					    If present and true, indicates that the encoder should make an extra pass
 | 
				
			||||||
| 
						 | 
					@ -475,9 +500,18 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
 | 
				
			||||||
    and must be greater than the code-block size.
 | 
					    and must be greater than the code-block size.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**irreversible**
 | 
					**irreversible**
 | 
				
			||||||
    If ``True``, use the lossy Irreversible Color Transformation
 | 
					    If ``True``, use the lossy discrete waveform transformation DWT 9-7.
 | 
				
			||||||
    followed by DWT 9-7.  Defaults to ``False``, which means to use the
 | 
					    Defaults to ``False``, which uses the lossless DWT 5-3.
 | 
				
			||||||
    Reversible Color Transformation with DWT 5-3.
 | 
					
 | 
				
			||||||
 | 
					**mct**
 | 
				
			||||||
 | 
					    If ``1`` then enable multiple component transformation when encoding,
 | 
				
			||||||
 | 
					    otherwise use ``0`` for no component transformation (default). If MCT is
 | 
				
			||||||
 | 
					    enabled and ``irreversible`` is ``True`` then the Irreversible Color
 | 
				
			||||||
 | 
					    Transformation will be applied, otherwise encoding will use the
 | 
				
			||||||
 | 
					    Reversible Color Transformation. MCT works best with a ``mode`` of
 | 
				
			||||||
 | 
					    ``RGB`` and is only applicable when the image data has 3 components.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. versionadded:: 9.1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**progression**
 | 
					**progression**
 | 
				
			||||||
    Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``,
 | 
					    Controls the progression order; must be one of ``"LRCP"``, ``"RLCP"``,
 | 
				
			||||||
| 
						 | 
					@ -497,6 +531,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
 | 
				
			||||||
    for compliant 4K files, *at least one* of the dimensions must match
 | 
					    for compliant 4K files, *at least one* of the dimensions must match
 | 
				
			||||||
    4096 x 2160.
 | 
					    4096 x 2160.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**no_jp2**
 | 
				
			||||||
 | 
					    If ``True`` then don't wrap the raw codestream in the JP2 file format when
 | 
				
			||||||
 | 
					    saving, otherwise the extension of the filename will be used to determine
 | 
				
			||||||
 | 
					    the format (default).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .. versionadded:: 9.1.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. note::
 | 
					.. note::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   To enable JPEG 2000 support, you need to build and install the OpenJPEG
 | 
					   To enable JPEG 2000 support, you need to build and install the OpenJPEG
 | 
				
			||||||
| 
						 | 
					@ -743,7 +784,7 @@ parameter must be set to ``True``. The following parameters can also be set:
 | 
				
			||||||
PPM
 | 
					PPM
 | 
				
			||||||
^^^
 | 
					^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L`` or
 | 
					Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or
 | 
				
			||||||
``RGB`` data.
 | 
					``RGB`` data.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SGI
 | 
					SGI
 | 
				
			||||||
| 
						 | 
					@ -1192,6 +1233,11 @@ PSD
 | 
				
			||||||
Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
 | 
					Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SUN
 | 
				
			||||||
 | 
					^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Pillow identifies and reads Sun raster files.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WAL
 | 
					WAL
 | 
				
			||||||
^^^
 | 
					^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1206,13 +1252,13 @@ this format.
 | 
				
			||||||
By default, a Quake2 standard palette is attached to the texture. To override
 | 
					By default, a Quake2 standard palette is attached to the texture. To override
 | 
				
			||||||
the palette, use the putpalette method.
 | 
					the palette, use the putpalette method.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WMF
 | 
					WMF, EMF
 | 
				
			||||||
^^^
 | 
					^^^^^^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Pillow can identify WMF files.
 | 
					Pillow can identify WMF and EMF files.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
On Windows, it can read WMF files. By default, it will load the image at 72
 | 
					On Windows, it can read WMF and EMF files. By default, it will load the image
 | 
				
			||||||
dpi. To load it at another resolution:
 | 
					at 72 dpi. To load it at another resolution:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. code-block:: python
 | 
					.. code-block:: python
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1222,7 +1268,8 @@ dpi. To load it at another resolution:
 | 
				
			||||||
        im.load(dpi=144)
 | 
					        im.load(dpi=144)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
To add other read or write support, use
 | 
					To add other read or write support, use
 | 
				
			||||||
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF handler.
 | 
					:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF
 | 
				
			||||||
 | 
					handler.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. code-block:: python
 | 
					.. code-block:: python
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -171,20 +171,37 @@ Rolling an image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
::
 | 
					::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def roll(image, delta):
 | 
					    def roll(im, delta):
 | 
				
			||||||
        """Roll an image sideways."""
 | 
					        """Roll an image sideways."""
 | 
				
			||||||
        xsize, ysize = image.size
 | 
					        xsize, ysize = im.size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        delta = delta % xsize
 | 
					        delta = delta % xsize
 | 
				
			||||||
        if delta == 0:
 | 
					        if delta == 0:
 | 
				
			||||||
            return image
 | 
					            return im
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        part1 = image.crop((0, 0, delta, ysize))
 | 
					        part1 = im.crop((0, 0, delta, ysize))
 | 
				
			||||||
        part2 = image.crop((delta, 0, xsize, ysize))
 | 
					        part2 = im.crop((delta, 0, xsize, ysize))
 | 
				
			||||||
        image.paste(part1, (xsize - delta, 0, xsize, ysize))
 | 
					        im.paste(part1, (xsize - delta, 0, xsize, ysize))
 | 
				
			||||||
        image.paste(part2, (0, 0, xsize - delta, ysize))
 | 
					        im.paste(part2, (0, 0, xsize - delta, ysize))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return image
 | 
					        return im
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Or if you would like to merge two images into a wider image:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Merging images
 | 
				
			||||||
 | 
					^^^^^^^^^^^^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def merge(im1, im2):
 | 
				
			||||||
 | 
					        w = im1.size[0] + im2.size[0]
 | 
				
			||||||
 | 
					        h = max(im1.size[1], im2.size[1])
 | 
				
			||||||
 | 
					        im = Image.new("RGBA", (w, h))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        im.paste(im1)
 | 
				
			||||||
 | 
					        im.paste(im2, (im1.size[0], 0))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return im
 | 
				
			||||||
 | 
					
 | 
				
			||||||
For more advanced tricks, the paste method can also take a transparency mask as
 | 
					For more advanced tricks, the paste method can also take a transparency mask as
 | 
				
			||||||
an optional argument. In this mask, the value 255 indicates that the pasted
 | 
					an optional argument. In this mask, the value 255 indicates that the pasted
 | 
				
			||||||
| 
						 | 
					@ -487,6 +504,17 @@ image header. In addition, seek will also be used when the image data is read
 | 
				
			||||||
tar file, you can use the :py:class:`~PIL.ContainerIO` or
 | 
					tar file, you can use the :py:class:`~PIL.ContainerIO` or
 | 
				
			||||||
:py:class:`~PIL.TarIO` modules to access it.
 | 
					:py:class:`~PIL.TarIO` modules to access it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Reading from URL
 | 
				
			||||||
 | 
					^^^^^^^^^^^^^^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    from PIL import Image
 | 
				
			||||||
 | 
					    from urllib.request import urlopen
 | 
				
			||||||
 | 
					    url = "https://python-pillow.org/images/pillow-logo.png"
 | 
				
			||||||
 | 
					    img = Image.open(urlopen(url))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Reading from a tar archive
 | 
					Reading from a tar archive
 | 
				
			||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
 | 
					^^^^^^^^^^^^^^^^^^^^^^^^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -123,8 +123,12 @@ The ``tile`` attribute
 | 
				
			||||||
To be able to read the file as well as just identifying it, the ``tile``
 | 
					To be able to read the file as well as just identifying it, the ``tile``
 | 
				
			||||||
attribute must also be set. This attribute consists of a list of tile
 | 
					attribute must also be set. This attribute consists of a list of tile
 | 
				
			||||||
descriptors, where each descriptor specifies how data should be loaded to a
 | 
					descriptors, where each descriptor specifies how data should be loaded to a
 | 
				
			||||||
given region in the image. In most cases, only a single descriptor is used,
 | 
					given region in the image.
 | 
				
			||||||
covering the full image.
 | 
					
 | 
				
			||||||
 | 
					In most cases, only a single descriptor is used, covering the full image.
 | 
				
			||||||
 | 
					:py:class:`.PsdImagePlugin.PsdImageFile` uses multiple tiles to combine
 | 
				
			||||||
 | 
					channels within a single layer, given that the channels are stored separately,
 | 
				
			||||||
 | 
					one after the other.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The tile descriptor is a 4-tuple with the following contents::
 | 
					The tile descriptor is a 4-tuple with the following contents::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -324,42 +328,42 @@ The fields are used as follows:
 | 
				
			||||||
    Whether the first line in the image is the top line on the screen (1), or
 | 
					    Whether the first line in the image is the top line on the screen (1), or
 | 
				
			||||||
    the bottom line (-1). If omitted, the orientation defaults to 1.
 | 
					    the bottom line (-1). If omitted, the orientation defaults to 1.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. _file-decoders:
 | 
					.. _file-codecs:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Writing Your Own File Decoder in C
 | 
					Writing Your Own File Codec in C
 | 
				
			||||||
==================================
 | 
					================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
There are 3 stages in a file decoder's lifetime:
 | 
					There are 3 stages in a file codec's lifetime:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. Setup: Pillow looks for a function in the decoder registry, falling
 | 
					1. Setup: Pillow looks for a function in the decoder or encoder registry,
 | 
				
			||||||
   back to a function named ``[decodername]_decoder`` on the internal
 | 
					   falling back to a function named ``[codecname]_decoder`` or
 | 
				
			||||||
   core image object.  That function is called with the ``args`` tuple
 | 
					   ``[codecname]_encoder`` on the internal core image object. That function is
 | 
				
			||||||
   from the ``tile`` setup in the ``_open`` method.
 | 
					   called with the ``args`` tuple from the ``tile``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
2. Decoding: The decoder's decode function is repeatedly called with
 | 
					2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly
 | 
				
			||||||
   chunks of image data.
 | 
					   called with chunks of image data.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
3. Cleanup: If the decoder has registered a cleanup function, it will
 | 
					3. Cleanup: If the codec has registered a cleanup function, it will
 | 
				
			||||||
   be called at the end of the decoding process, even if there was an
 | 
					   be called at the end of the transformation process, even if there was an
 | 
				
			||||||
   exception raised.
 | 
					   exception raised.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Setup
 | 
					Setup
 | 
				
			||||||
-----
 | 
					-----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The current conventions are that the decoder setup function is named
 | 
					The current conventions are that the codec setup function is named
 | 
				
			||||||
``PyImaging_[Decodername]DecoderNew`` and defined in ``decode.c``. The
 | 
					``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew``
 | 
				
			||||||
python binding for it is named ``[decodername]_decoder`` and is setup
 | 
					and defined in ``decode.c`` or ``encode.c``. The Python binding for it is
 | 
				
			||||||
from within the ``_imaging.c`` file in the codecs section of the
 | 
					named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from
 | 
				
			||||||
function array.
 | 
					within the ``_imaging.c`` file in the codecs section of the function array.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The setup function needs to call ``PyImaging_DecoderNew`` and at the
 | 
					The setup function needs to call ``PyImaging_DecoderNew`` or
 | 
				
			||||||
very least, set the ``decode`` function pointer. The fields of
 | 
					``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or
 | 
				
			||||||
interest in this object are:
 | 
					``encode`` function pointer. The fields of interest in this object are:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**decode**
 | 
					**decode**/**encode**
 | 
				
			||||||
  Function pointer to the decode function, which has access to
 | 
					  Function pointer to the decode or encode function, which has access to
 | 
				
			||||||
  ``im``, ``state``, and the buffer of data to be added to the image.
 | 
					  ``im``, ``state``, and the buffer of data to be transformed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**cleanup**
 | 
					**cleanup**
 | 
				
			||||||
  Function pointer to the cleanup function, has access to ``state``.
 | 
					  Function pointer to the cleanup function, has access to ``state``.
 | 
				
			||||||
| 
						 | 
					@ -369,36 +373,34 @@ interest in this object are:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**state**
 | 
					**state**
 | 
				
			||||||
  An ImagingCodecStateInstance, will be set by Pillow. The ``context``
 | 
					  An ImagingCodecStateInstance, will be set by Pillow. The ``context``
 | 
				
			||||||
  member is an opaque struct that can be used by the decoder to store
 | 
					  member is an opaque struct that can be used by the codec to store
 | 
				
			||||||
  any format specific state or options.
 | 
					  any format specific state or options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**pulls_fd**
 | 
					**pulls_fd**/**pushes_fd**
 | 
				
			||||||
  **EXPERIMENTAL** -- **WARNING**, interface may change. If set to 1,
 | 
					  If the decoder has ``pulls_fd`` or the encoder has ``pushes_fd`` set to 1,
 | 
				
			||||||
  ``state->fd`` will be a pointer to the Python file like object.  The
 | 
					  ``state->fd`` will be a pointer to the Python file like object. The codec may
 | 
				
			||||||
  decoder may use the functions in ``codec_fd.c`` to read directly
 | 
					  use the functions in ``codec_fd.c`` to read or write directly with the file
 | 
				
			||||||
  from the file like object rather than have the data pushed through a
 | 
					  like object rather than have the data pushed through a buffer.
 | 
				
			||||||
  buffer.  Note that this implementation may be refactored until this
 | 
					 | 
				
			||||||
  warning is removed.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .. versionadded:: 3.3.0
 | 
					  .. versionadded:: 3.3.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Decoding
 | 
					Transforming
 | 
				
			||||||
--------
 | 
					------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The decode function is called with the target (core) image, the
 | 
					The decode or encode function is called with the target (core) image, the codec
 | 
				
			||||||
decoder state structure, and a buffer of data to be decoded.
 | 
					state structure, and a buffer of data to be transformed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**Experimental** -- If ``pulls_fd`` is set, then the decode function
 | 
					It is the codec's responsibility to pull as much data as possible out of the
 | 
				
			||||||
is called once, with an empty buffer. It is the decoder's
 | 
					buffer and return the number of bytes consumed. The next call to the codec will
 | 
				
			||||||
responsibility to decode the entire tile in that one call.  The rest of
 | 
					include the previous unconsumed tail. The codec function will be called
 | 
				
			||||||
this section only applies if ``pulls_fd`` is not set.
 | 
					multiple times as the data processed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
It is the decoder's responsibility to pull as much data as possible
 | 
					Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or
 | 
				
			||||||
out of the buffer and return the number of bytes consumed. The next
 | 
					encode function is called once, with an empty buffer. It is the codec's
 | 
				
			||||||
call to the decoder will include the previous unconsumed tail. The
 | 
					responsibility to transform the entire tile in that one call.  Using this will
 | 
				
			||||||
decoder function will be called multiple times as the data is read
 | 
					provide a codec with more freedom, but that freedom may mean increased memory
 | 
				
			||||||
from the file like object.
 | 
					usage if the entire tile is held in memory at once by the codec.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
If an error occurs, set ``state->errcode`` and return -1.
 | 
					If an error occurs, set ``state->errcode`` and return -1.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -407,10 +409,9 @@ Return -1 on success, without setting the errcode.
 | 
				
			||||||
Cleanup
 | 
					Cleanup
 | 
				
			||||||
-------
 | 
					-------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The cleanup function is called after the decoder returns a negative
 | 
					The cleanup function is called after the codec returns a negative
 | 
				
			||||||
value, or if there is a read error from the file. This function should
 | 
					value, or if there is an error. This function should free any allocated
 | 
				
			||||||
free any allocated memory and release any resources from external
 | 
					memory and release any resources from external libraries.
 | 
				
			||||||
libraries.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. _file-codecs-py:
 | 
					.. _file-codecs-py:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -425,11 +426,32 @@ They should be registered using :py:meth:`PIL.Image.register_decoder` and
 | 
				
			||||||
the file codecs, there are three stages in the lifetime of a
 | 
					the file codecs, there are three stages in the lifetime of a
 | 
				
			||||||
Python-based file codec:
 | 
					Python-based file codec:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. Setup: Pillow looks for the decoder in the registry, then
 | 
					1. Setup: Pillow looks for the codec in the decoder or encoder registry, then
 | 
				
			||||||
   instantiates the class.
 | 
					   instantiates the class.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
2. Transforming: The instance's ``decode`` method is repeatedly called with
 | 
					2. Transforming: The instance's ``decode`` method is repeatedly called with
 | 
				
			||||||
   a buffer of data to be interpreted, or the ``encode`` method is repeatedly
 | 
					   a buffer of data to be interpreted, or the ``encode`` method is repeatedly
 | 
				
			||||||
   called with the size of data to be output.
 | 
					   called with the size of data to be output.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
3. Cleanup: The instance's ``cleanup`` method is called.
 | 
					   Alternatively, if the decoder's ``_pulls_fd`` property (or the encoder's
 | 
				
			||||||
 | 
					   ``_pushes_fd`` property) is set to ``True``, then ``decode`` and ``encode``
 | 
				
			||||||
 | 
					   will only be called once. In the decoder, ``self.fd`` can be used to access
 | 
				
			||||||
 | 
					   the file-like object. Using this will provide a codec with more freedom, but
 | 
				
			||||||
 | 
					   that freedom may mean increased memory usage if entire file is held in
 | 
				
			||||||
 | 
					   memory at once by the codec.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   In ``decode``, once the data has been interpreted, ``set_as_raw`` can be
 | 
				
			||||||
 | 
					   used to populate the image.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					3. Cleanup: The instance's ``cleanup`` method is called once the transformation
 | 
				
			||||||
 | 
					   is complete. This can be used to clean up any resources used by the codec.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   If you set ``_pulls_fd`` or ``_pushes_fd`` to ``True`` however, then you
 | 
				
			||||||
 | 
					   probably chose to perform any cleanup tasks  at the end of ``decode`` or
 | 
				
			||||||
 | 
					   ``encode``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For an example :py:class:`PIL.ImageFile.PyDecoder`, see `DdsImagePlugin
 | 
				
			||||||
 | 
					<https://github.com/python-pillow/Pillow/blob/main/docs/example/DdsImagePlugin.py>`_.
 | 
				
			||||||
 | 
					For a plugin that uses both :py:class:`PIL.ImageFile.PyDecoder` and
 | 
				
			||||||
 | 
					:py:class:`PIL.ImageFile.PyEncoder`, see `BlpImagePlugin
 | 
				
			||||||
 | 
					<https://github.com/python-pillow/Pillow/blob/main/src/PIL/BlpImagePlugin.py>`_
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||