Merge branch 'main' into codecov
|  | @ -3,7 +3,7 @@ | |||
| # gather the coverage data | ||||
| python3 -m pip install codecov | ||||
| if [[ $MATRIX_DOCKER ]]; then | ||||
|   coverage xml --ignore-errors | ||||
|   python3 -m coverage xml --ignore-errors | ||||
| else | ||||
|   coverage xml | ||||
|   python3 -m coverage xml | ||||
| fi | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| set -e | ||||
| 
 | ||||
| coverage erase | ||||
| python3 -m coverage erase | ||||
| if [ $(uname) == "Darwin" ]; then | ||||
|     export CPPFLAGS="-I/usr/local/miniconda/include"; | ||||
| fi | ||||
|  |  | |||
|  | @ -13,13 +13,17 @@ aptget_update() | |||
|         return 1 | ||||
|     fi | ||||
| } | ||||
| aptget_update || aptget_update retry || aptget_update retry | ||||
| if [[ $(uname) != CYGWIN* ]]; then | ||||
|     aptget_update || aptget_update retry || aptget_update retry | ||||
| fi | ||||
| 
 | ||||
| set -e | ||||
| 
 | ||||
| sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ | ||||
|                          ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ | ||||
|                          cmake meson imagemagick libharfbuzz-dev libfribidi-dev | ||||
| if [[ $(uname) != CYGWIN* ]]; then | ||||
|     sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ | ||||
|                              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 wheel | ||||
|  | @ -31,24 +35,27 @@ python3 -m pip install -U pytest | |||
| python3 -m pip install -U pytest-cov | ||||
| python3 -m pip install -U pytest-timeout | ||||
| python3 -m pip install pyroma | ||||
| python3 -m pip install test-image-results | ||||
| # TODO Remove condition when NumPy supports 3.11 | ||||
| if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi | ||||
| 
 | ||||
| # PyQt6 doesn't support PyPy3 | ||||
| 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 | ||||
| if [[ $(uname) != CYGWIN* ]]; then | ||||
|     python3 -m pip install numpy | ||||
| 
 | ||||
|     # PyQt6 doesn't support PyPy3 | ||||
|     if [[ $GHA_PYTHON_VERSION == 3.* ]]; then | ||||
|         sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 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 | ||||
| 
 | ||||
| # 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 | ||||
|  |  | |||
							
								
								
									
										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. | ||||
| 
 | ||||
| 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. | ||||
| - Create a branch from `main`. | ||||
|  |  | |||
							
								
								
									
										1
									
								
								.github/mergify.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -8,6 +8,7 @@ pull_request_rules: | |||
|       - status-success=Docker Test Successful | ||||
|       - status-success=Windows Test Successful | ||||
|       - status-success=MinGW Test Successful | ||||
|       - status-success=Cygwin Test Successful | ||||
|       - status-success=continuous-integration/appveyor/pr | ||||
|     actions: | ||||
|       merge: | ||||
|  |  | |||
							
								
								
									
										3
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -2,6 +2,9 @@ name: Lint | |||
| 
 | ||||
| on: [push, pull_request, workflow_dispatch] | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										4
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -12,11 +12,9 @@ python3 -m pip install -U pytest | |||
| python3 -m pip install -U pytest-cov | ||||
| python3 -m pip install -U pytest-timeout | ||||
| python3 -m pip install pyroma | ||||
| python3 -m pip install test-image-results | ||||
| 
 | ||||
| echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openblas/lib" >> ~/.numpy-site.cfg | ||||
| # TODO Remove condition when NumPy supports 3.11 | ||||
| if ! [ "$GHA_PYTHON_VERSION" == "3.11-dev" ]; then python3 -m pip install numpy ; fi | ||||
| python3 -m pip install numpy | ||||
| 
 | ||||
| # extra test images | ||||
| pushd depends && ./install_extra_test_images.sh && popd | ||||
|  |  | |||
							
								
								
									
										6
									
								
								.github/workflows/release-drafter.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -7,8 +7,14 @@ on: | |||
|       - main | ||||
|   workflow_dispatch: | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| jobs: | ||||
|   update_release_draft: | ||||
|     permissions: | ||||
|       contents: write  # for release-drafter/release-drafter to create a github release | ||||
|       pull-requests: write  # for release-drafter/release-drafter to add label to PR | ||||
|     if: github.repository == 'python-pillow/Pillow' | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|  |  | |||
							
								
								
									
										109
									
								
								.github/workflows/test-cygwin.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,109 @@ | |||
| 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: | ||||
|     permissions: | ||||
|       contents: none | ||||
|     needs: build | ||||
|     runs-on: ubuntu-latest | ||||
|     name: Cygwin Test Successful | ||||
|     steps: | ||||
|       - name: Success | ||||
|         run: echo Cygwin Test Successful | ||||
							
								
								
									
										18
									
								
								.github/workflows/test-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -2,6 +2,9 @@ name: Test Docker | |||
| 
 | ||||
| on: [push, pull_request, workflow_dispatch] | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
| 
 | ||||
|  | @ -11,9 +14,9 @@ jobs: | |||
|       matrix: | ||||
|         docker: [ | ||||
|           # Run slower jobs first to give them a headstart and reduce waiting time | ||||
|           ubuntu-20.04-focal-arm64v8, | ||||
|           ubuntu-20.04-focal-ppc64le, | ||||
|           ubuntu-20.04-focal-s390x, | ||||
|           ubuntu-22.04-jammy-arm64v8, | ||||
|           ubuntu-22.04-jammy-ppc64le, | ||||
|           ubuntu-22.04-jammy-s390x, | ||||
|           # Then run the remainder | ||||
|           alpine, | ||||
|           amazon-2-amd64, | ||||
|  | @ -24,6 +27,7 @@ jobs: | |||
|           debian-10-buster-x86, | ||||
|           debian-11-bullseye-x86, | ||||
|           fedora-35-amd64, | ||||
|           fedora-36-amd64, | ||||
|           gentoo, | ||||
|           ubuntu-18.04-bionic-amd64, | ||||
|           ubuntu-20.04-focal-amd64, | ||||
|  | @ -31,11 +35,11 @@ jobs: | |||
|         ] | ||||
|         dockerTag: [main] | ||||
|         include: | ||||
|           - docker: "ubuntu-20.04-focal-arm64v8" | ||||
|           - docker: "ubuntu-22.04-jammy-arm64v8" | ||||
|             qemu-arch: "aarch64" | ||||
|           - docker: "ubuntu-20.04-focal-ppc64le" | ||||
|           - docker: "ubuntu-22.04-jammy-ppc64le" | ||||
|             qemu-arch: "ppc64le" | ||||
|           - docker: "ubuntu-20.04-focal-s390x" | ||||
|           - docker: "ubuntu-22.04-jammy-s390x" | ||||
|             qemu-arch: "s390x" | ||||
| 
 | ||||
|     name: ${{ matrix.docker }} | ||||
|  | @ -81,6 +85,8 @@ jobs: | |||
|         name: ${{ matrix.docker }} | ||||
| 
 | ||||
|   success: | ||||
|     permissions: | ||||
|       contents: none | ||||
|     needs: build | ||||
|     runs-on: ubuntu-latest | ||||
|     name: Docker Test Successful | ||||
|  |  | |||
							
								
								
									
										5
									
								
								.github/workflows/test-mingw.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -2,6 +2,9 @@ name: Test MinGW | |||
| 
 | ||||
| on: [push, pull_request, workflow_dispatch] | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: windows-latest | ||||
|  | @ -77,6 +80,8 @@ jobs: | |||
|           CODECOV_NAME: ${{ matrix.name }} | ||||
| 
 | ||||
|   success: | ||||
|     permissions: | ||||
|       contents: none | ||||
|     needs: build | ||||
|     runs-on: ubuntu-latest | ||||
|     name: MinGW Test Successful | ||||
|  |  | |||
							
								
								
									
										5
									
								
								.github/workflows/test-valgrind.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -13,6 +13,9 @@ on: | |||
|       - "**.h" | ||||
|   workflow_dispatch: | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
| 
 | ||||
|  | @ -21,7 +24,7 @@ jobs: | |||
|       fail-fast: false | ||||
|       matrix: | ||||
|         docker: [ | ||||
|           ubuntu-20.04-focal-amd64-valgrind, | ||||
|           ubuntu-22.04-jammy-amd64-valgrind, | ||||
|         ] | ||||
|         dockerTag: [main] | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										11
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -2,6 +2,9 @@ name: Test Windows | |||
| 
 | ||||
| on: [push, pull_request, workflow_dispatch] | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: windows-latest | ||||
|  | @ -41,10 +44,10 @@ jobs: | |||
|         cache-dependency-path: ".github/workflows/test-windows.yml" | ||||
| 
 | ||||
|     - 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 | ||||
|       run: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml | ||||
|     - name: python3 -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 | ||||
|       id: install | ||||
|  | @ -189,6 +192,8 @@ jobs: | |||
|         path: dist\*.whl | ||||
| 
 | ||||
|   success: | ||||
|     permissions: | ||||
|       contents: none | ||||
|     needs: build | ||||
|     runs-on: ubuntu-latest | ||||
|     name: Windows Test Successful | ||||
|  |  | |||
							
								
								
									
										5
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -2,6 +2,9 @@ name: Test | |||
| 
 | ||||
| on: [push, pull_request, workflow_dispatch] | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
| 
 | ||||
|  | @ -106,6 +109,8 @@ jobs: | |||
|         name: ${{ matrix.os }} Python ${{ matrix.python-version }} | ||||
| 
 | ||||
|   success: | ||||
|     permissions: | ||||
|       contents: none | ||||
|     needs: build | ||||
|     runs-on: ubuntu-latest | ||||
|     name: Test Successful | ||||
|  |  | |||
							
								
								
									
										3
									
								
								.github/workflows/tidelift.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -12,6 +12,9 @@ on: | |||
|       - ".github/workflows/tidelift.yml" | ||||
|   workflow_dispatch: | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     if: github.repository_owner == 'python-pillow' | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| repos: | ||||
|   - repo: https://github.com/psf/black | ||||
|     rev: 22.3.0 | ||||
|     rev: 22.6.0 | ||||
|     hooks: | ||||
|       - id: black | ||||
|         args: ["--target-version", "py37"] | ||||
|  | @ -19,13 +19,13 @@ repos: | |||
|       - id: yesqa | ||||
| 
 | ||||
|   - repo: https://github.com/Lucas-C/pre-commit-hooks | ||||
|     rev: v1.1.13 | ||||
|     rev: v1.3.0 | ||||
|     hooks: | ||||
|       - id: remove-tabs | ||||
|         exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) | ||||
| 
 | ||||
|   - repo: https://github.com/PyCQA/flake8 | ||||
|     rev: 4.0.1 | ||||
|     rev: 5.0.2 | ||||
|     hooks: | ||||
|       - id: flake8 | ||||
|         additional_dependencies: [flake8-2020, flake8-implicit-str-concat] | ||||
|  | @ -37,10 +37,15 @@ repos: | |||
|       - id: rst-backticks | ||||
| 
 | ||||
|   - repo: https://github.com/pre-commit/pre-commit-hooks | ||||
|     rev: v4.1.0 | ||||
|     rev: v4.3.0 | ||||
|     hooks: | ||||
|       - id: check-merge-conflict | ||||
|       - id: check-yaml | ||||
| 
 | ||||
|   - repo: https://github.com/sphinx-contrib/sphinx-lint | ||||
|     rev: v0.6.1 | ||||
|     hooks: | ||||
|       - id: sphinx-lint | ||||
| 
 | ||||
| ci: | ||||
|   autoupdate_schedule: quarterly | ||||
|   autoupdate_schedule: monthly | ||||
|  |  | |||
							
								
								
									
										149
									
								
								CHANGES.rst
									
									
									
									
									
								
							
							
						
						|  | @ -2,9 +2,144 @@ | |||
| Changelog (Pillow) | ||||
| ================== | ||||
| 
 | ||||
| 9.2.0 (unreleased) | ||||
| 9.3.0 (unreleased) | ||||
| ------------------ | ||||
| 
 | ||||
| - Allow default ImageDraw font to be set #6484 | ||||
|   [radarhere, hugovk] | ||||
| 
 | ||||
| - Save 1 mode PDF using CCITTFaxDecode filter #6470 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Added support for RGBA PSD images #6481 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Parse orientation from XMP tag contents #6463 | ||||
|   [bigcat88, radarhere] | ||||
| 
 | ||||
| - Added support for reading ATI1/ATI2 (BC4/BC5) DDS images #6457 | ||||
|   [REDxEYE, radarhere] | ||||
| 
 | ||||
| - Do not clear GIF tile when checking number of frames #6455 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Support saving multiple MPO frames #6444 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Do not double quote Pillow version for setuptools >= 60 #6450 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Added ABGR BMP mask mode #6436 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Fixed PSDraw rectangle #6429 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Raise ValueError if PNG sRGB chunk is truncated #6431 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Handle missing Python executable in ImageShow on macOS #6416 | ||||
|   [bryant1410, radarhere] | ||||
| 
 | ||||
| 9.2.0 (2022-07-01) | ||||
| ------------------ | ||||
| 
 | ||||
| - Deprecate ImageFont.getsize and related functions #6381 | ||||
|   [nulano, radarhere] | ||||
| 
 | ||||
| - Fixed null check for fribidi_version_info in FriBiDi shim #6376 | ||||
|   [nulano] | ||||
| 
 | ||||
| - Added GIF decompression bomb check #6402 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Handle PCF fonts files with less than 256 characters #6386 | ||||
|   [dawidcrivelli, radarhere] | ||||
| 
 | ||||
| - Improved GIF optimize condition #6378 | ||||
|   [raygard, radarhere] | ||||
| 
 | ||||
| - Reverted to __array_interface__ with the release of NumPy 1.23 #6394 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Pad PCX palette to 768 bytes when saving #6391 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Fixed bug with rounding pixels to palette colors #6377 | ||||
|   [btrekkie, radarhere] | ||||
| 
 | ||||
| - Use gnome-screenshot on Linux if available #6361 | ||||
|   [radarhere, nulano] | ||||
| 
 | ||||
| - Fixed loading L mode BMP RLE8 images #6384 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Fixed incorrect operator in ImageCms error #6370 | ||||
|   [LostBenjamin, hugovk, radarhere] | ||||
| 
 | ||||
| - Limit FPX tile size to avoid extending outside image #6368 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Added support for decoding plain PPM formats #5242 | ||||
|   [Piolie, radarhere] | ||||
| 
 | ||||
| - Added apply_transparency() #6352 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Fixed behaviour change from endian fix #6197 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Allow remapping P images with RGBA palettes #6350 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Fixed drawing translucent 1px high polygons #6278 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Pad COLORMAP to 768 items when saving TIFF #6232 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Fix P -> PA conversion #6337 | ||||
|   [RedShy, radarhere] | ||||
| 
 | ||||
| - Once exif data is parsed, do not reload unless it changes #6335 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Only try to connect discontiguous corners at the end of edges #6303 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Improve transparency handling when saving GIF images #6176 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Do not update GIF frame position until local image is found #6219 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Netscape GIF extension belongs after the global color table #6211 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Only write GIF comments at the beginning of the file #6300 | ||||
|   [raygard, radarhere] | ||||
| 
 | ||||
| - Separate multiple GIF comment blocks with newlines #6294 | ||||
|   [raygard, radarhere] | ||||
| 
 | ||||
| - Always use GIF89a for comments #6292 | ||||
|   [raygard, radarhere] | ||||
| 
 | ||||
| - Ignore compression value from BMP info dictionary when saving as TIFF #6231 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - If font is file-like object, do not re-read from object to get variant #6234 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Raise ValueError when trying to access internal fp after close #6213 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Support more affine expression forms in im.point() #6254 | ||||
|   [benrg, radarhere] | ||||
| 
 | ||||
| - Populate Python palette in fromarray() #6283 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Raise ValueError if PNG chunks are truncated #6253 | ||||
|   [radarhere] | ||||
| 
 | ||||
|  | @ -14,9 +149,6 @@ Changelog (Pillow) | |||
| - Adjust BITSPERSAMPLE to match SAMPLESPERPIXEL when opening TIFFs #6270 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Do not open images with zero or negative height #6269 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Search pkgconf system libs/cflags #6138 | ||||
|   [jameshilliard, radarhere] | ||||
| 
 | ||||
|  | @ -47,6 +179,15 @@ Changelog (Pillow) | |||
| - Deprecated PhotoImage.paste() box parameter #6178 | ||||
|   [radarhere] | ||||
| 
 | ||||
| 9.1.1 (2022-05-17) | ||||
| ------------------ | ||||
| 
 | ||||
| - When reading past the end of a TGA scan line, reduce bytes left. CVE-2022-30595 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Do not open images with zero or negative height #6269 | ||||
|   [radarhere] | ||||
| 
 | ||||
| 9.1.0 (2022-04-01) | ||||
| ------------------ | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						|  | @ -85,6 +85,8 @@ release-test: | |||
| sdist: | ||||
| 	python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build | ||||
| 	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 | ||||
| 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 | ||||
|                 alt="GitHub Actions build status (Test MinGW)" | ||||
|                 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 | ||||
|                 alt="GitHub Actions build status (Test Docker)" | ||||
|                 src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a> | ||||
|  |  | |||
|  | @ -24,7 +24,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. | |||
| * [ ] Create and check source distribution: | ||||
|   ```bash | ||||
|   make sdist | ||||
|   python3 -m twine check --strict dist/* | ||||
|   ``` | ||||
| * [ ] 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.: | ||||
|  | @ -61,7 +60,6 @@ Released as needed for security, installation or critical bug fixes. | |||
| * [ ] Create and check source distribution: | ||||
|   ```bash | ||||
|   make sdist | ||||
|   python3 -m twine check --strict dist/* | ||||
|   ``` | ||||
| * [ ] 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.: | ||||
|  | @ -91,7 +89,6 @@ Released as needed privately to individual vendors for critical security-related | |||
| * [ ] Create and check source distribution: | ||||
|   ```bash | ||||
|   make sdist | ||||
|   python3 -m twine check --strict dist/* | ||||
|   ``` | ||||
| * [ ] 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) | ||||
|  | @ -99,8 +96,8 @@ Released as needed privately to individual vendors for critical security-related | |||
| ## Binary Distributions | ||||
| 
 | ||||
| ### Windows | ||||
| * [ ] Contact `@cgohlke` for Windows binaries via release ticket e.g. https://github.com/python-pillow/Pillow/issues/1174. | ||||
| * [ ] Download and extract tarball from `@cgohlke` and copy into `dist/` | ||||
| * [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) | ||||
|   and copy into `dist/` | ||||
| 
 | ||||
| ### Mac and Linux | ||||
| * [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels): | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ Dependencies | |||
| 
 | ||||
| Install:: | ||||
| 
 | ||||
|     python3 -m pip install pytest pytest-cov | ||||
|     python3 -m pip install pytest pytest-cov pytest-timeout | ||||
| 
 | ||||
| Execution | ||||
| --------- | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/ati1.dds
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/ati1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 969 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/ati2.dds
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/comment_after_last_frame.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/comment_after_only_frame.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 54 B | 
							
								
								
									
										
											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/decompression_bomb_extents.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 368 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/duplicate_number_of_loops.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_16bit.pgm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										4
									
								
								Tests/images/hopper_16bit_plain.pgm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_1bit.pbm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										14
									
								
								Tests/images/hopper_1bit_plain.pbm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_8bit.pgm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_8bit.ppm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										4
									
								
								Tests/images/hopper_8bit_plain.pgm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_8bit_plain.ppm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_rle8_greyscale.bmp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/imagedraw_polygon_1px_high_translucent.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 76 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/input_bw_one_band.fpx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/input_bw_one_band.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 477 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/issue_6194.j2k
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/multiple_comments.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.5 KiB | 
| Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB | 
| Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB | 
| Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB | 
| Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/rgb32bf-abgr.bmp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 32 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/rgba.psd
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/second_frame_comment.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/tiny.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 16 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/xmp_tags_orientation_exiftool.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.2 KiB | 
|  | @ -33,9 +33,9 @@ def fuzz_font(data): | |||
|         # different font objects. | ||||
|         return | ||||
| 
 | ||||
|     font.getsize_multiline("ABC\nAaaa") | ||||
|     font.getbbox("ABC") | ||||
|     font.getmask("test text") | ||||
|     with Image.new(mode="RGBA", size=(200, 200)) as im: | ||||
|         draw = ImageDraw.Draw(im) | ||||
|         draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) | ||||
|         draw.multiline_textbbox((10, 10), "ABC\nAaaa", font, stroke_width=2) | ||||
|         draw.text((10, 10), "Test Text", font=font, fill="#000") | ||||
|  |  | |||
|  | @ -51,7 +51,6 @@ class TestDecompressionBomb: | |||
|             with Image.open(TEST_FILE): | ||||
|                 pass | ||||
| 
 | ||||
|     @pytest.mark.xfail(reason="different exception") | ||||
|     def test_exception_ico(self): | ||||
|         with pytest.raises(Image.DecompressionBombError): | ||||
|             with Image.open("Tests/images/decompression_bomb.ico"): | ||||
|  | @ -62,6 +61,11 @@ class TestDecompressionBomb: | |||
|             with Image.open("Tests/images/decompression_bomb.gif"): | ||||
|                 pass | ||||
| 
 | ||||
|     def test_exception_gif_extents(self): | ||||
|         with Image.open("Tests/images/decompression_bomb_extents.gif") as im: | ||||
|             with pytest.raises(Image.DecompressionBombError): | ||||
|                 im.seek(1) | ||||
| 
 | ||||
|     def test_exception_bmp(self): | ||||
|         with pytest.raises(Image.DecompressionBombError): | ||||
|             with Image.open("Tests/images/bmp/b/reallybig.bmp"): | ||||
|  |  | |||
|  | @ -325,8 +325,9 @@ def test_apng_syntax_errors(): | |||
|     pytest.warns(UserWarning, open) | ||||
| 
 | ||||
| 
 | ||||
| def test_apng_sequence_errors(): | ||||
|     test_files = [ | ||||
| @pytest.mark.parametrize( | ||||
|     "test_file", | ||||
|     ( | ||||
|         "sequence_start.png", | ||||
|         "sequence_gap.png", | ||||
|         "sequence_repeat.png", | ||||
|  | @ -334,12 +335,13 @@ def test_apng_sequence_errors(): | |||
|         "sequence_reorder.png", | ||||
|         "sequence_reorder_chunk.png", | ||||
|         "sequence_fdat_fctl.png", | ||||
|     ] | ||||
|     for f in test_files: | ||||
|         with pytest.raises(SyntaxError): | ||||
|             with Image.open(f"Tests/images/apng/{f}") as im: | ||||
|                 im.seek(im.n_frames - 1) | ||||
|                 im.load() | ||||
|     ), | ||||
| ) | ||||
| def test_apng_sequence_errors(test_file): | ||||
|     with pytest.raises(SyntaxError): | ||||
|         with Image.open(f"Tests/images/apng/{test_file}") as im: | ||||
|             im.seek(im.n_frames - 1) | ||||
|             im.load() | ||||
| 
 | ||||
| 
 | ||||
| def test_apng_save(tmp_path): | ||||
|  | @ -637,6 +639,15 @@ def test_apng_save_blend(tmp_path): | |||
|         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(): | ||||
|     for enum, prefix in { | ||||
|         PngImagePlugin.Disposal: "APNG_DISPOSE_", | ||||
|  |  | |||
|  | @ -129,11 +129,21 @@ def test_rgba_bitfields(): | |||
| 
 | ||||
|     assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") | ||||
| 
 | ||||
|     # This test image has been manually hexedited | ||||
|     # to change the bitfield compression in the header from XBGR to ABGR | ||||
|     with Image.open("Tests/images/rgb32bf-abgr.bmp") as im: | ||||
|         assert_image_equal_tofile( | ||||
|             im.convert("RGB"), "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) | ||||
| 
 | ||||
|     with Image.open("Tests/images/hopper_rle8_greyscale.bmp") as im: | ||||
|         assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") | ||||
| 
 | ||||
|     # 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: | ||||
|  |  | |||
|  | @ -1,3 +1,5 @@ | |||
| import pytest | ||||
| 
 | ||||
| from PIL import ContainerIO, Image | ||||
| 
 | ||||
| from .helper import hopper | ||||
|  | @ -59,89 +61,89 @@ def test_seek_mode_2(): | |||
|         assert container.tell() == 100 | ||||
| 
 | ||||
| 
 | ||||
| def test_read_n0(): | ||||
| @pytest.mark.parametrize("bytesmode", (True, False)) | ||||
| def test_read_n0(bytesmode): | ||||
|     # Arrange | ||||
|     for bytesmode in (True, False): | ||||
|         with open(TEST_FILE, "rb" if bytesmode else "r") as fh: | ||||
|             container = ContainerIO.ContainerIO(fh, 22, 100) | ||||
|     with open(TEST_FILE, "rb" if bytesmode else "r") as fh: | ||||
|         container = ContainerIO.ContainerIO(fh, 22, 100) | ||||
| 
 | ||||
|             # Act | ||||
|             container.seek(81) | ||||
|             data = container.read() | ||||
|         # Act | ||||
|         container.seek(81) | ||||
|         data = container.read() | ||||
| 
 | ||||
|             # Assert | ||||
|             if bytesmode: | ||||
|                 data = data.decode() | ||||
|             assert data == "7\nThis is line 8\n" | ||||
|         # Assert | ||||
|         if bytesmode: | ||||
|             data = data.decode() | ||||
|         assert data == "7\nThis is line 8\n" | ||||
| 
 | ||||
| 
 | ||||
| def test_read_n(): | ||||
| @pytest.mark.parametrize("bytesmode", (True, False)) | ||||
| def test_read_n(bytesmode): | ||||
|     # Arrange | ||||
|     for bytesmode in (True, False): | ||||
|         with open(TEST_FILE, "rb" if bytesmode else "r") as fh: | ||||
|             container = ContainerIO.ContainerIO(fh, 22, 100) | ||||
|     with open(TEST_FILE, "rb" if bytesmode else "r") as fh: | ||||
|         container = ContainerIO.ContainerIO(fh, 22, 100) | ||||
| 
 | ||||
|             # Act | ||||
|             container.seek(81) | ||||
|             data = container.read(3) | ||||
|         # Act | ||||
|         container.seek(81) | ||||
|         data = container.read(3) | ||||
| 
 | ||||
|             # Assert | ||||
|             if bytesmode: | ||||
|                 data = data.decode() | ||||
|             assert data == "7\nT" | ||||
|         # Assert | ||||
|         if bytesmode: | ||||
|             data = data.decode() | ||||
|         assert data == "7\nT" | ||||
| 
 | ||||
| 
 | ||||
| def test_read_eof(): | ||||
| @pytest.mark.parametrize("bytesmode", (True, False)) | ||||
| def test_read_eof(bytesmode): | ||||
|     # Arrange | ||||
|     for bytesmode in (True, False): | ||||
|         with open(TEST_FILE, "rb" if bytesmode else "r") as fh: | ||||
|             container = ContainerIO.ContainerIO(fh, 22, 100) | ||||
|     with open(TEST_FILE, "rb" if bytesmode else "r") as fh: | ||||
|         container = ContainerIO.ContainerIO(fh, 22, 100) | ||||
| 
 | ||||
|             # Act | ||||
|             container.seek(100) | ||||
|             data = container.read() | ||||
|         # Act | ||||
|         container.seek(100) | ||||
|         data = container.read() | ||||
| 
 | ||||
|             # Assert | ||||
|             if bytesmode: | ||||
|                 data = data.decode() | ||||
|             assert data == "" | ||||
|         # Assert | ||||
|         if bytesmode: | ||||
|             data = data.decode() | ||||
|         assert data == "" | ||||
| 
 | ||||
| 
 | ||||
| def test_readline(): | ||||
| @pytest.mark.parametrize("bytesmode", (True, False)) | ||||
| def test_readline(bytesmode): | ||||
|     # Arrange | ||||
|     for bytesmode in (True, False): | ||||
|         with open(TEST_FILE, "rb" if bytesmode else "r") as fh: | ||||
|             container = ContainerIO.ContainerIO(fh, 0, 120) | ||||
|     with open(TEST_FILE, "rb" if bytesmode else "r") as fh: | ||||
|         container = ContainerIO.ContainerIO(fh, 0, 120) | ||||
| 
 | ||||
|             # Act | ||||
|             data = container.readline() | ||||
|         # Act | ||||
|         data = container.readline() | ||||
| 
 | ||||
|             # Assert | ||||
|             if bytesmode: | ||||
|                 data = data.decode() | ||||
|             assert data == "This is line 1\n" | ||||
|         # Assert | ||||
|         if bytesmode: | ||||
|             data = data.decode() | ||||
|         assert data == "This is line 1\n" | ||||
| 
 | ||||
| 
 | ||||
| def test_readlines(): | ||||
| @pytest.mark.parametrize("bytesmode", (True, False)) | ||||
| def test_readlines(bytesmode): | ||||
|     # Arrange | ||||
|     for bytesmode in (True, False): | ||||
|         expected = [ | ||||
|             "This is line 1\n", | ||||
|             "This is line 2\n", | ||||
|             "This is line 3\n", | ||||
|             "This is line 4\n", | ||||
|             "This is line 5\n", | ||||
|             "This is line 6\n", | ||||
|             "This is line 7\n", | ||||
|             "This is line 8\n", | ||||
|         ] | ||||
|         with open(TEST_FILE, "rb" if bytesmode else "r") as fh: | ||||
|             container = ContainerIO.ContainerIO(fh, 0, 120) | ||||
|     expected = [ | ||||
|         "This is line 1\n", | ||||
|         "This is line 2\n", | ||||
|         "This is line 3\n", | ||||
|         "This is line 4\n", | ||||
|         "This is line 5\n", | ||||
|         "This is line 6\n", | ||||
|         "This is line 7\n", | ||||
|         "This is line 8\n", | ||||
|     ] | ||||
|     with open(TEST_FILE, "rb" if bytesmode else "r") as fh: | ||||
|         container = ContainerIO.ContainerIO(fh, 0, 120) | ||||
| 
 | ||||
|             # Act | ||||
|             data = container.readlines() | ||||
|         # Act | ||||
|         data = container.readlines() | ||||
| 
 | ||||
|             # Assert | ||||
|             if bytesmode: | ||||
|                 data = [line.decode() for line in data] | ||||
|             assert data == expected | ||||
|         # Assert | ||||
|         if bytesmode: | ||||
|             data = [line.decode() for line in data] | ||||
|         assert data == expected | ||||
|  |  | |||
|  | @ -10,6 +10,8 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper | |||
| TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" | ||||
| TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" | ||||
| TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" | ||||
| TEST_FILE_ATI1 = "Tests/images/ati1.dds" | ||||
| TEST_FILE_ATI2 = "Tests/images/ati2.dds" | ||||
| TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds" | ||||
| TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" | ||||
| TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" | ||||
|  | @ -62,6 +64,32 @@ def test_sanity_dxt5(): | |||
|     assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png")) | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity_ati1(): | ||||
|     """Check ATI1 images can be opened""" | ||||
| 
 | ||||
|     with Image.open(TEST_FILE_ATI1) as im: | ||||
|         im.load() | ||||
| 
 | ||||
|         assert im.format == "DDS" | ||||
|         assert im.mode == "L" | ||||
|         assert im.size == (64, 64) | ||||
| 
 | ||||
|         assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity_ati2(): | ||||
|     """Check ATI2 images can be opened""" | ||||
| 
 | ||||
|     with Image.open(TEST_FILE_ATI2) as im: | ||||
|         im.load() | ||||
| 
 | ||||
|         assert im.format == "DDS" | ||||
|         assert im.mode == "RGB" | ||||
|         assert im.size == (256, 256) | ||||
| 
 | ||||
|         assert_image_equal_tofile(im, TEST_FILE_DX10_BC5_UNORM.replace(".dds", ".png")) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     ("image_path", "expected_path"), | ||||
|     ( | ||||
|  |  | |||
|  | @ -46,6 +46,15 @@ def test_closed_file(): | |||
|         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(): | ||||
|     with warnings.catch_warnings(): | ||||
|         with Image.open(static_test_file) as im: | ||||
|  |  | |||
|  | @ -2,11 +2,22 @@ import pytest | |||
| 
 | ||||
| from PIL import Image | ||||
| 
 | ||||
| from .helper import assert_image_equal_tofile | ||||
| 
 | ||||
| FpxImagePlugin = pytest.importorskip( | ||||
|     "PIL.FpxImagePlugin", reason="olefile not installed" | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity(): | ||||
|     with Image.open("Tests/images/input_bw_one_band.fpx") as im: | ||||
|         assert im.mode == "L" | ||||
|         assert im.size == (70, 46) | ||||
|         assert im.format == "FPX" | ||||
| 
 | ||||
|         assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") | ||||
| 
 | ||||
| 
 | ||||
| def test_invalid_file(): | ||||
|     # Test an invalid OLE file | ||||
|     invalid_file = "Tests/images/flower.jpg" | ||||
|  |  | |||
|  | @ -46,6 +46,19 @@ def test_closed_file(): | |||
|         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(): | ||||
|     with warnings.catch_warnings(): | ||||
|         with Image.open(TEST_GIF) as im: | ||||
|  | @ -145,6 +158,9 @@ def test_optimize_correctness(): | |||
|             assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) | ||||
| 
 | ||||
|     # These do optimize the palette | ||||
|     check(256, 511, 256) | ||||
|     check(255, 511, 255) | ||||
|     check(129, 511, 129) | ||||
|     check(128, 511, 128) | ||||
|     check(64, 511, 64) | ||||
|     check(4, 511, 4) | ||||
|  | @ -154,11 +170,6 @@ def test_optimize_correctness(): | |||
|     check(64, 513, 256) | ||||
|     check(4, 513, 256) | ||||
| 
 | ||||
|     # Other limits that don't optimize the palette | ||||
|     check(129, 511, 256) | ||||
|     check(255, 511, 256) | ||||
|     check(256, 511, 256) | ||||
| 
 | ||||
| 
 | ||||
| def test_optimize_full_l(): | ||||
|     im = Image.frombytes("L", (16, 16), bytes(range(256))) | ||||
|  | @ -167,6 +178,19 @@ def test_optimize_full_l(): | |||
|     assert im.mode == "L" | ||||
| 
 | ||||
| 
 | ||||
| def test_optimize_if_palette_can_be_reduced_by_half(): | ||||
|     with Image.open("Tests/images/test.colors.gif") as im: | ||||
|         # Reduce dimensions because original is too big for _get_optimize() | ||||
|         im = im.resize((591, 443)) | ||||
|     im_rgb = im.convert("RGB") | ||||
| 
 | ||||
|     for (optimize, colors) in ((False, 256), (True, 8)): | ||||
|         out = BytesIO() | ||||
|         im_rgb.save(out, "GIF", optimize=optimize) | ||||
|         with Image.open(out) as reloaded: | ||||
|             assert len(reloaded.palette.palette) // 3 == colors | ||||
| 
 | ||||
| 
 | ||||
| def test_roundtrip(tmp_path): | ||||
|     out = str(tmp_path / "temp.gif") | ||||
|     im = hopper() | ||||
|  | @ -341,16 +365,23 @@ def test_seek_rewind(): | |||
|             assert_image_equal(im, expected) | ||||
| 
 | ||||
| 
 | ||||
| def test_n_frames(): | ||||
|     for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]: | ||||
|         # Test is_animated before n_frames | ||||
|         with Image.open(path) as im: | ||||
|             assert im.is_animated == (n_frames != 1) | ||||
| @pytest.mark.parametrize( | ||||
|     "path, n_frames", | ||||
|     ( | ||||
|         (TEST_GIF, 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 | ||||
|         with Image.open(path) as im: | ||||
|             assert im.n_frames == n_frames | ||||
|             assert im.is_animated == (n_frames != 1) | ||||
|     # Test is_animated after n_frames | ||||
|     with Image.open(path) as im: | ||||
|         assert im.n_frames == n_frames | ||||
|         assert im.is_animated == (n_frames != 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_no_change(): | ||||
|  | @ -368,6 +399,11 @@ def test_no_change(): | |||
|         assert im.is_animated | ||||
|         assert_image_equal(im, expected) | ||||
| 
 | ||||
|     with Image.open("Tests/images/comment_after_only_frame.gif") as im: | ||||
|         expected = Image.new("P", (1, 1)) | ||||
|         assert not im.is_animated | ||||
|         assert_image_equal(im, expected) | ||||
| 
 | ||||
| 
 | ||||
| def test_eoferror(): | ||||
|     with Image.open(TEST_GIF) as im: | ||||
|  | @ -619,7 +655,8 @@ def test_dispose2_background(tmp_path): | |||
|         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: | ||||
|         assert im.info["transparency"] == 0 | ||||
| 
 | ||||
|  | @ -629,6 +666,14 @@ def test_transparency_in_second_frame(): | |||
| 
 | ||||
|         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(): | ||||
|     with Image.open("Tests/images/iss634.gif") as img: | ||||
|  | @ -640,6 +685,22 @@ def test_no_transparency_in_second_frame(): | |||
|         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): | ||||
|     duration = 1000 | ||||
| 
 | ||||
|  | @ -759,9 +820,16 @@ def test_number_of_loops(tmp_path): | |||
|     im = Image.new("L", (100, 100), "#000") | ||||
|     im.save(out, loop=number_of_loops) | ||||
|     with Image.open(out) as reread: | ||||
| 
 | ||||
|         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): | ||||
|     out = str(tmp_path / "temp.gif") | ||||
|  | @ -794,6 +862,9 @@ def test_comment(tmp_path): | |||
|     with Image.open(out) as reread: | ||||
|         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): | ||||
|     out = str(tmp_path / "temp.gif") | ||||
|  | @ -804,15 +875,67 @@ def test_comment_over_255(tmp_path): | |||
|     im.info["comment"] = comment | ||||
|     im.save(out) | ||||
|     with Image.open(out) as reread: | ||||
| 
 | ||||
|         assert reread.info["comment"] == comment | ||||
| 
 | ||||
|         # Test that GIF89a is used for comments | ||||
|         assert reread.info["version"] == b"GIF89a" | ||||
| 
 | ||||
| 
 | ||||
| def test_zero_comment_subblocks(): | ||||
|     with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: | ||||
|         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): | ||||
|     out = str(tmp_path / "temp.gif") | ||||
| 
 | ||||
|  | @ -875,8 +998,8 @@ def test_append_images(tmp_path): | |||
| def test_transparent_optimize(tmp_path): | ||||
|     # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses | ||||
|     # transparency. | ||||
|     # Need a palette that isn't using the 0 color, and one that's > 128 items where the | ||||
|     # transparent color is actually the top palette entry to trigger the bug. | ||||
|     # Need a palette that isn't using the 0 color, | ||||
|     # where the transparent color is actually the top palette entry to trigger the bug. | ||||
| 
 | ||||
|     data = bytes(range(1, 254)) | ||||
|     palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) | ||||
|  | @ -886,10 +1009,10 @@ def test_transparent_optimize(tmp_path): | |||
|     im.putpalette(palette) | ||||
| 
 | ||||
|     out = str(tmp_path / "temp.gif") | ||||
|     im.save(out, transparency=253) | ||||
|     with Image.open(out) as reloaded: | ||||
|     im.save(out, transparency=im.getpixel((252, 0))) | ||||
| 
 | ||||
|         assert reloaded.info["transparency"] == 253 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) | ||||
| 
 | ||||
| 
 | ||||
| def test_rgb_transparency(tmp_path): | ||||
|  |  | |||
|  | @ -78,15 +78,12 @@ def test_eoferror(): | |||
|         im.seek(n_frames - 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_roundtrip(tmp_path): | ||||
|     def roundtrip(mode): | ||||
|         out = str(tmp_path / "temp.im") | ||||
|         im = hopper(mode) | ||||
|         im.save(out) | ||||
|         assert_image_equal_tofile(im, out) | ||||
| 
 | ||||
|     for mode in ["RGB", "P", "PA"]: | ||||
|         roundtrip(mode) | ||||
| @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) | ||||
| def test_roundtrip(mode, tmp_path): | ||||
|     out = str(tmp_path / "temp.im") | ||||
|     im = hopper(mode) | ||||
|     im.save(out) | ||||
|     assert_image_equal_tofile(im, out) | ||||
| 
 | ||||
| 
 | ||||
| def test_save_unsupported_mode(tmp_path): | ||||
|  |  | |||
|  | @ -298,6 +298,11 @@ def test_16bit_jp2_roundtrips(): | |||
|         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(): | ||||
|     # prepatch, a malformed jp2 file could cause an UnboundLocalError exception. | ||||
|     with pytest.raises(OSError): | ||||
|  |  | |||
|  | @ -135,50 +135,50 @@ class TestFileLibTiff(LibTiffTestCase): | |||
| 
 | ||||
|             assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") | ||||
| 
 | ||||
|     def test_write_metadata(self, tmp_path): | ||||
|     @pytest.mark.parametrize("legacy_api", (False, True)) | ||||
|     def test_write_metadata(self, legacy_api, tmp_path): | ||||
|         """Test metadata writing through libtiff""" | ||||
|         for legacy_api in [False, True]: | ||||
|             f = str(tmp_path / "temp.tiff") | ||||
|             with Image.open("Tests/images/hopper_g4.tif") as img: | ||||
|                 img.save(f, tiffinfo=img.tag) | ||||
|         f = str(tmp_path / "temp.tiff") | ||||
|         with Image.open("Tests/images/hopper_g4.tif") as img: | ||||
|             img.save(f, tiffinfo=img.tag) | ||||
| 
 | ||||
|                 if legacy_api: | ||||
|                     original = img.tag.named() | ||||
|                 else: | ||||
|                     original = img.tag_v2.named() | ||||
|             if legacy_api: | ||||
|                 original = img.tag.named() | ||||
|             else: | ||||
|                 original = img.tag_v2.named() | ||||
| 
 | ||||
|             # PhotometricInterpretation is set from SAVE_INFO, | ||||
|             # not the original image. | ||||
|             ignored = [ | ||||
|                 "StripByteCounts", | ||||
|                 "RowsPerStrip", | ||||
|                 "PageNumber", | ||||
|                 "PhotometricInterpretation", | ||||
|             ] | ||||
|         # PhotometricInterpretation is set from SAVE_INFO, | ||||
|         # not the original image. | ||||
|         ignored = [ | ||||
|             "StripByteCounts", | ||||
|             "RowsPerStrip", | ||||
|             "PageNumber", | ||||
|             "PhotometricInterpretation", | ||||
|         ] | ||||
| 
 | ||||
|             with Image.open(f) as loaded: | ||||
|                 if legacy_api: | ||||
|                     reloaded = loaded.tag.named() | ||||
|                 else: | ||||
|                     reloaded = loaded.tag_v2.named() | ||||
|         with Image.open(f) as loaded: | ||||
|             if legacy_api: | ||||
|                 reloaded = loaded.tag.named() | ||||
|             else: | ||||
|                 reloaded = loaded.tag_v2.named() | ||||
| 
 | ||||
|             for tag, value in itertools.chain(reloaded.items(), original.items()): | ||||
|                 if tag not in ignored: | ||||
|                     val = original[tag] | ||||
|                     if tag.endswith("Resolution"): | ||||
|                         if legacy_api: | ||||
|                             assert val[0][0] / val[0][1] == ( | ||||
|                                 4294967295 / 113653537 | ||||
|                             ), f"{tag} didn't roundtrip" | ||||
|                         else: | ||||
|                             assert val == 37.79000115940079, f"{tag} didn't roundtrip" | ||||
|         for tag, value in itertools.chain(reloaded.items(), original.items()): | ||||
|             if tag not in ignored: | ||||
|                 val = original[tag] | ||||
|                 if tag.endswith("Resolution"): | ||||
|                     if legacy_api: | ||||
|                         assert val[0][0] / val[0][1] == ( | ||||
|                             4294967295 / 113653537 | ||||
|                         ), f"{tag} didn't roundtrip" | ||||
|                     else: | ||||
|                         assert val == value, f"{tag} didn't roundtrip" | ||||
|                         assert val == 37.79000115940079, f"{tag} didn't roundtrip" | ||||
|                 else: | ||||
|                     assert val == value, f"{tag} didn't roundtrip" | ||||
| 
 | ||||
|             # https://github.com/python-pillow/Pillow/issues/1561 | ||||
|             requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"] | ||||
|             for field in requested_fields: | ||||
|                 assert field in reloaded, f"{field} not in metadata" | ||||
|         # https://github.com/python-pillow/Pillow/issues/1561 | ||||
|         requested_fields = ["StripByteCounts", "RowsPerStrip", "StripOffsets"] | ||||
|         for field in requested_fields: | ||||
|             assert field in reloaded, f"{field} not in metadata" | ||||
| 
 | ||||
|     @pytest.mark.valgrind_known_error(reason="Known invalid metadata") | ||||
|     def test_additional_metadata(self, tmp_path): | ||||
|  | @ -497,8 +497,8 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|         im.save(out, compression="tiff_adobe_deflate") | ||||
|         assert_image_equal_tofile(im, out) | ||||
| 
 | ||||
|     def test_palette_save(self, tmp_path): | ||||
|         im = hopper("P") | ||||
|     @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) | ||||
|     def test_palette_save(self, im, tmp_path): | ||||
|         out = str(tmp_path / "temp.tif") | ||||
| 
 | ||||
|         TiffImagePlugin.WRITE_LIBTIFF = True | ||||
|  | @ -856,7 +856,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|     def test_strip_ycbcr_jpeg_2x2_sampling(self): | ||||
|         infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" | ||||
|         with Image.open(infile) as im: | ||||
|             assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) | ||||
|             assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2) | ||||
| 
 | ||||
|     @mark_if_feature_version( | ||||
|         pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" | ||||
|  | @ -864,7 +864,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|     def test_strip_ycbcr_jpeg_1x1_sampling(self): | ||||
|         infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" | ||||
|         with Image.open(infile) as im: | ||||
|             assert_image_equal_tofile(im, "Tests/images/flower2.jpg") | ||||
|             assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) | ||||
| 
 | ||||
|     def test_tiled_cmyk_jpeg(self): | ||||
|         infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif" | ||||
|  | @ -877,7 +877,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|     def test_tiled_ycbcr_jpeg_1x1_sampling(self): | ||||
|         infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" | ||||
|         with Image.open(infile) as im: | ||||
|             assert_image_equal_tofile(im, "Tests/images/flower2.jpg") | ||||
|             assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) | ||||
| 
 | ||||
|     @mark_if_feature_version( | ||||
|         pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" | ||||
|  | @ -885,7 +885,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|     def test_tiled_ycbcr_jpeg_2x2_sampling(self): | ||||
|         infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" | ||||
|         with Image.open(infile) as im: | ||||
|             assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) | ||||
|             assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5) | ||||
| 
 | ||||
|     def test_strip_planar_rgb(self): | ||||
|         # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ | ||||
|  | @ -1011,14 +1011,18 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|             # Assert that there are multiple strips | ||||
|             assert len(im.tag_v2[STRIPOFFSETS]) > 1 | ||||
| 
 | ||||
|     def test_save_single_strip(self, tmp_path): | ||||
|     @pytest.mark.parametrize("argument", (True, False)) | ||||
|     def test_save_single_strip(self, argument, tmp_path): | ||||
|         im = hopper("RGB").resize((256, 256)) | ||||
|         out = str(tmp_path / "temp.tif") | ||||
| 
 | ||||
|         TiffImagePlugin.STRIP_SIZE = 2**18 | ||||
|         if not argument: | ||||
|             TiffImagePlugin.STRIP_SIZE = 2**18 | ||||
|         try: | ||||
| 
 | ||||
|             im.save(out, compression="tiff_adobe_deflate") | ||||
|             arguments = {"compression": "tiff_adobe_deflate"} | ||||
|             if argument: | ||||
|                 arguments["strip_size"] = 2**18 | ||||
|             im.save(out, **arguments) | ||||
| 
 | ||||
|             with Image.open(out) as im: | ||||
|                 assert len(im.tag_v2[STRIPOFFSETS]) == 1 | ||||
|  |  | |||
|  | @ -5,15 +5,19 @@ import pytest | |||
| 
 | ||||
| from PIL import Image | ||||
| 
 | ||||
| from .helper import assert_image_similar, is_pypy, skip_unless_feature | ||||
| from .helper import ( | ||||
|     assert_image_equal, | ||||
|     assert_image_similar, | ||||
|     is_pypy, | ||||
|     skip_unless_feature, | ||||
| ) | ||||
| 
 | ||||
| test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] | ||||
| 
 | ||||
| pytestmark = skip_unless_feature("jpg") | ||||
| 
 | ||||
| 
 | ||||
| def frame_roundtrip(im, **options): | ||||
|     # Note that for now, there is no MPO saving functionality | ||||
| def roundtrip(im, **options): | ||||
|     out = BytesIO() | ||||
|     im.save(out, "MPO", **options) | ||||
|     test_bytes = out.tell() | ||||
|  | @ -23,13 +27,13 @@ def frame_roundtrip(im, **options): | |||
|     return im | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity(): | ||||
|     for test_file in test_files: | ||||
|         with Image.open(test_file) as im: | ||||
|             im.load() | ||||
|             assert im.mode == "RGB" | ||||
|             assert im.size == (640, 480) | ||||
|             assert im.format == "MPO" | ||||
| @pytest.mark.parametrize("test_file", test_files) | ||||
| def test_sanity(test_file): | ||||
|     with Image.open(test_file) as im: | ||||
|         im.load() | ||||
|         assert im.mode == "RGB" | ||||
|         assert im.size == (640, 480) | ||||
|         assert im.format == "MPO" | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(is_pypy(), reason="Requires CPython") | ||||
|  | @ -48,32 +52,39 @@ def test_closed_file(): | |||
|         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(): | ||||
|     with warnings.catch_warnings(): | ||||
|         with Image.open(test_files[0]) as im: | ||||
|             im.load() | ||||
| 
 | ||||
| 
 | ||||
| def test_app(): | ||||
|     for test_file in test_files: | ||||
|         # Test APP/COM reader (@PIL135) | ||||
|         with Image.open(test_file) as im: | ||||
|             assert im.applist[0][0] == "APP1" | ||||
|             assert im.applist[1][0] == "APP2" | ||||
|             assert ( | ||||
|                 im.applist[1][1][:16] | ||||
|                 == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" | ||||
|             ) | ||||
|             assert len(im.applist) == 2 | ||||
| @pytest.mark.parametrize("test_file", test_files) | ||||
| def test_app(test_file): | ||||
|     # Test APP/COM reader (@PIL135) | ||||
|     with Image.open(test_file) as im: | ||||
|         assert im.applist[0][0] == "APP1" | ||||
|         assert im.applist[1][0] == "APP2" | ||||
|         assert ( | ||||
|             im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00" | ||||
|         ) | ||||
|         assert len(im.applist) == 2 | ||||
| 
 | ||||
| 
 | ||||
| def test_exif(): | ||||
|     for test_file in test_files: | ||||
|         with Image.open(test_file) as im: | ||||
|             info = im._getexif() | ||||
|             assert info[272] == "Nintendo 3DS" | ||||
|             assert info[296] == 2 | ||||
|             assert info[34665] == 188 | ||||
| @pytest.mark.parametrize("test_file", test_files) | ||||
| def test_exif(test_file): | ||||
|     with Image.open(test_file) as im: | ||||
|         info = im._getexif() | ||||
|         assert info[272] == "Nintendo 3DS" | ||||
|         assert info[296] == 2 | ||||
|         assert info[34665] == 188 | ||||
| 
 | ||||
| 
 | ||||
| def test_frame_size(): | ||||
|  | @ -116,12 +127,21 @@ def test_parallax(): | |||
|         assert exif.get_ifd(0x927C)[0xB211] == -3.125 | ||||
| 
 | ||||
| 
 | ||||
| def test_mp(): | ||||
|     for test_file in test_files: | ||||
|         with Image.open(test_file) as im: | ||||
|             mpinfo = im._getmp() | ||||
|             assert mpinfo[45056] == b"0100" | ||||
|             assert mpinfo[45057] == 2 | ||||
| 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 | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("test_file", test_files) | ||||
| def test_mp(test_file): | ||||
|     with Image.open(test_file) as im: | ||||
|         mpinfo = im._getmp() | ||||
|         assert mpinfo[45056] == b"0100" | ||||
|         assert mpinfo[45057] == 2 | ||||
| 
 | ||||
| 
 | ||||
| def test_mp_offset(): | ||||
|  | @ -141,48 +161,48 @@ def test_mp_no_data(): | |||
|             im.seek(1) | ||||
| 
 | ||||
| 
 | ||||
| def test_mp_attribute(): | ||||
|     for test_file in test_files: | ||||
|         with Image.open(test_file) as im: | ||||
|             mpinfo = im._getmp() | ||||
|         frame_number = 0 | ||||
|         for mpentry in mpinfo[0xB002]: | ||||
|             mpattr = mpentry["Attribute"] | ||||
|             if frame_number: | ||||
|                 assert not mpattr["RepresentativeImageFlag"] | ||||
|             else: | ||||
|                 assert mpattr["RepresentativeImageFlag"] | ||||
|             assert not mpattr["DependentParentImageFlag"] | ||||
|             assert not mpattr["DependentChildImageFlag"] | ||||
|             assert mpattr["ImageDataFormat"] == "JPEG" | ||||
|             assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" | ||||
|             assert mpattr["Reserved"] == 0 | ||||
|             frame_number += 1 | ||||
| @pytest.mark.parametrize("test_file", test_files) | ||||
| def test_mp_attribute(test_file): | ||||
|     with Image.open(test_file) as im: | ||||
|         mpinfo = im._getmp() | ||||
|     frame_number = 0 | ||||
|     for mpentry in mpinfo[0xB002]: | ||||
|         mpattr = mpentry["Attribute"] | ||||
|         if frame_number: | ||||
|             assert not mpattr["RepresentativeImageFlag"] | ||||
|         else: | ||||
|             assert mpattr["RepresentativeImageFlag"] | ||||
|         assert not mpattr["DependentParentImageFlag"] | ||||
|         assert not mpattr["DependentChildImageFlag"] | ||||
|         assert mpattr["ImageDataFormat"] == "JPEG" | ||||
|         assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" | ||||
|         assert mpattr["Reserved"] == 0 | ||||
|         frame_number += 1 | ||||
| 
 | ||||
| 
 | ||||
| def test_seek(): | ||||
|     for test_file in test_files: | ||||
|         with Image.open(test_file) as im: | ||||
|             assert im.tell() == 0 | ||||
|             # prior to first image raises an error, both blatant and borderline | ||||
|             with pytest.raises(EOFError): | ||||
|                 im.seek(-1) | ||||
|             with pytest.raises(EOFError): | ||||
|                 im.seek(-523) | ||||
|             # after the final image raises an error, | ||||
|             # both blatant and borderline | ||||
|             with pytest.raises(EOFError): | ||||
|                 im.seek(2) | ||||
|             with pytest.raises(EOFError): | ||||
|                 im.seek(523) | ||||
|             # bad calls shouldn't change the frame | ||||
|             assert im.tell() == 0 | ||||
|             # this one will work | ||||
|             im.seek(1) | ||||
|             assert im.tell() == 1 | ||||
|             # and this one, too | ||||
|             im.seek(0) | ||||
|             assert im.tell() == 0 | ||||
| @pytest.mark.parametrize("test_file", test_files) | ||||
| def test_seek(test_file): | ||||
|     with Image.open(test_file) as im: | ||||
|         assert im.tell() == 0 | ||||
|         # prior to first image raises an error, both blatant and borderline | ||||
|         with pytest.raises(EOFError): | ||||
|             im.seek(-1) | ||||
|         with pytest.raises(EOFError): | ||||
|             im.seek(-523) | ||||
|         # after the final image raises an error, | ||||
|         # both blatant and borderline | ||||
|         with pytest.raises(EOFError): | ||||
|             im.seek(2) | ||||
|         with pytest.raises(EOFError): | ||||
|             im.seek(523) | ||||
|         # bad calls shouldn't change the frame | ||||
|         assert im.tell() == 0 | ||||
|         # this one will work | ||||
|         im.seek(1) | ||||
|         assert im.tell() == 1 | ||||
|         # and this one, too | ||||
|         im.seek(0) | ||||
|         assert im.tell() == 0 | ||||
| 
 | ||||
| 
 | ||||
| def test_n_frames(): | ||||
|  | @ -204,29 +224,54 @@ def test_eoferror(): | |||
|         im.seek(n_frames - 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_image_grab(): | ||||
| @pytest.mark.parametrize("test_file", test_files) | ||||
| def test_image_grab(test_file): | ||||
|     with Image.open(test_file) as im: | ||||
|         assert im.tell() == 0 | ||||
|         im0 = im.tobytes() | ||||
|         im.seek(1) | ||||
|         assert im.tell() == 1 | ||||
|         im1 = im.tobytes() | ||||
|         im.seek(0) | ||||
|         assert im.tell() == 0 | ||||
|         im02 = im.tobytes() | ||||
|         assert im0 == im02 | ||||
|         assert im0 != im1 | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("test_file", test_files) | ||||
| def test_save(test_file): | ||||
|     with Image.open(test_file) as im: | ||||
|         assert im.tell() == 0 | ||||
|         jpg0 = roundtrip(im) | ||||
|         assert_image_similar(im, jpg0, 30) | ||||
|         im.seek(1) | ||||
|         assert im.tell() == 1 | ||||
|         jpg1 = roundtrip(im) | ||||
|         assert_image_similar(im, jpg1, 30) | ||||
| 
 | ||||
| 
 | ||||
| def test_save_all(): | ||||
|     for test_file in test_files: | ||||
|         with Image.open(test_file) as im: | ||||
|             assert im.tell() == 0 | ||||
|             im0 = im.tobytes() | ||||
|             im.seek(1) | ||||
|             assert im.tell() == 1 | ||||
|             im1 = im.tobytes() | ||||
|             im_reloaded = roundtrip(im, save_all=True) | ||||
| 
 | ||||
|             im.seek(0) | ||||
|             assert im.tell() == 0 | ||||
|             im02 = im.tobytes() | ||||
|             assert im0 == im02 | ||||
|             assert im0 != im1 | ||||
|             assert_image_similar(im, im_reloaded, 30) | ||||
| 
 | ||||
| 
 | ||||
| def test_save(): | ||||
|     # Note that only individual frames can be saved at present | ||||
|     for test_file in test_files: | ||||
|         with Image.open(test_file) as im: | ||||
|             assert im.tell() == 0 | ||||
|             jpg0 = frame_roundtrip(im) | ||||
|             assert_image_similar(im, jpg0, 30) | ||||
|             im.seek(1) | ||||
|             assert im.tell() == 1 | ||||
|             jpg1 = frame_roundtrip(im) | ||||
|             assert_image_similar(im, jpg1, 30) | ||||
|             im_reloaded.seek(1) | ||||
|             assert_image_similar(im, im_reloaded, 30) | ||||
| 
 | ||||
|     im = Image.new("RGB", (1, 1)) | ||||
|     im2 = Image.new("RGB", (1, 1), "#f00") | ||||
|     im_reloaded = roundtrip(im, save_all=True, append_images=[im2]) | ||||
| 
 | ||||
|     assert_image_equal(im, im_reloaded) | ||||
| 
 | ||||
|     im_reloaded.seek(1) | ||||
|     assert_image_similar(im2, im_reloaded, 1) | ||||
| 
 | ||||
|     # Test that a single frame image will not be saved as an MPO | ||||
|     jpg = roundtrip(im, save_all=True) | ||||
|     assert "mp" not in jpg.info | ||||
|  |  | |||
|  | @ -20,6 +20,11 @@ def test_sanity(tmp_path): | |||
|     for mode in ("1", "L", "P", "RGB"): | ||||
|         _roundtrip(tmp_path, hopper(mode)) | ||||
| 
 | ||||
|     # Test a palette with less than 256 colors | ||||
|     im = Image.new("P", (1, 1)) | ||||
|     im.putpalette((255, 0, 0)) | ||||
|     _roundtrip(tmp_path, im) | ||||
| 
 | ||||
|     # Test an unsupported mode | ||||
|     f = str(tmp_path / "temp.pcx") | ||||
|     im = hopper("RGBA") | ||||
|  |  | |||
|  | @ -37,13 +37,14 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs): | |||
|     return outfile | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.valgrind_known_error(reason="Temporary skip") | ||||
| def test_monochrome(tmp_path): | ||||
|     # Arrange | ||||
|     mode = "1" | ||||
| 
 | ||||
|     # Act / Assert | ||||
|     outfile = helper_save_as_pdf(tmp_path, mode) | ||||
|     assert os.path.getsize(outfile) < 15000 | ||||
|     assert os.path.getsize(outfile) < 5000 | ||||
| 
 | ||||
| 
 | ||||
| def test_greyscale(tmp_path): | ||||
|  |  | |||
|  | @ -635,7 +635,9 @@ class TestFilePng: | |||
| 
 | ||||
|             assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") | ||||
| 
 | ||||
|     @pytest.mark.parametrize("cid", (b"IHDR", b"pHYs", b"acTL", b"fcTL", b"fdAT")) | ||||
|     @pytest.mark.parametrize( | ||||
|         "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") | ||||
|     ) | ||||
|     def test_truncated_chunks(self, cid): | ||||
|         fp = BytesIO() | ||||
|         with PngImagePlugin.PngStream(fp) as png: | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ from io import BytesIO | |||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, UnidentifiedImageError | ||||
| from PIL import Image, PpmImagePlugin | ||||
| 
 | ||||
| from .helper import assert_image_equal_tofile, assert_image_similar, hopper | ||||
| 
 | ||||
|  | @ -22,6 +22,21 @@ def test_sanity(): | |||
| @pytest.mark.parametrize( | ||||
|     "data, mode, pixels", | ||||
|     ( | ||||
|         (b"P2 3 1 4 0 2 4", "L", (0, 128, 255)), | ||||
|         (b"P2 3 1 257 0 128 257", "I", (0, 32640, 65535)), | ||||
|         # P3 with maxval < 255 | ||||
|         ( | ||||
|             b"P3 3 1 17 0 1 2 8 9 10 15 16 17", | ||||
|             "RGB", | ||||
|             ((0, 15, 30), (120, 135, 150), (225, 240, 255)), | ||||
|         ), | ||||
|         # P3 with maxval > 255 | ||||
|         # Scale down to 255, since there is no RGB mode with more than 8-bit | ||||
|         ( | ||||
|             b"P3 3 1 257 0 1 2 128 129 130 256 257 257", | ||||
|             "RGB", | ||||
|             ((0, 1, 2), (127, 128, 129), (254, 255, 255)), | ||||
|         ), | ||||
|         (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 | ||||
|  | @ -35,7 +50,6 @@ def test_sanity(): | |||
|             ), | ||||
|         ), | ||||
|         # 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", | ||||
|  | @ -85,14 +99,111 @@ def test_pnm(tmp_path): | |||
|         assert_image_equal_tofile(im, f) | ||||
| 
 | ||||
| 
 | ||||
| def test_magic(tmp_path): | ||||
| @pytest.mark.parametrize( | ||||
|     "plain_path, raw_path", | ||||
|     ( | ||||
|         ( | ||||
|             "Tests/images/hopper_1bit_plain.pbm",  # P1 | ||||
|             "Tests/images/hopper_1bit.pbm",  # P4 | ||||
|         ), | ||||
|         ( | ||||
|             "Tests/images/hopper_8bit_plain.pgm",  # P2 | ||||
|             "Tests/images/hopper_8bit.pgm",  # P5 | ||||
|         ), | ||||
|         ( | ||||
|             "Tests/images/hopper_8bit_plain.ppm",  # P3 | ||||
|             "Tests/images/hopper_8bit.ppm",  # P6 | ||||
|         ), | ||||
|     ), | ||||
| ) | ||||
| def test_plain(plain_path, raw_path): | ||||
|     with Image.open(plain_path) as im: | ||||
|         assert_image_equal_tofile(im, raw_path) | ||||
| 
 | ||||
| 
 | ||||
| def test_16bit_plain_pgm(): | ||||
|     # P2 with maxval 2 ** 16 - 1 | ||||
|     with Image.open("Tests/images/hopper_16bit_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 ** 16 - 1 | ||||
|         assert_image_equal_tofile(im, "Tests/images/hopper_16bit.pgm") | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "header, data, comment_count", | ||||
|     ( | ||||
|         (b"P1\n2 2", b"1010", 10**6), | ||||
|         (b"P2\n3 1\n4", b"0 2 4", 1), | ||||
|         (b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6), | ||||
|     ), | ||||
| ) | ||||
| def test_plain_data_with_comment(tmp_path, header, data, comment_count): | ||||
|     path1 = str(tmp_path / "temp1.ppm") | ||||
|     path2 = str(tmp_path / "temp2.ppm") | ||||
|     comment = b"# comment" * comment_count | ||||
|     with open(path1, "wb") as f1, open(path2, "wb") as f2: | ||||
|         f1.write(header + b"\n\n" + data) | ||||
|         f2.write(header + b"\n" + comment + b"\n" + data + comment) | ||||
| 
 | ||||
|     with Image.open(path1) as im: | ||||
|         assert_image_equal_tofile(im, path2) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) | ||||
| def test_plain_truncated_data(tmp_path, data): | ||||
|     path = str(tmp_path / "temp.ppm") | ||||
|     with open(path, "wb") as f: | ||||
|         f.write(b"PyInvalid") | ||||
|         f.write(data) | ||||
| 
 | ||||
|     with pytest.raises(UnidentifiedImageError): | ||||
|         with Image.open(path): | ||||
|             pass | ||||
|     with Image.open(path) as im: | ||||
|         with pytest.raises(ValueError): | ||||
|             im.load() | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) | ||||
| def test_plain_invalid_data(tmp_path, data): | ||||
|     path = str(tmp_path / "temp.ppm") | ||||
|     with open(path, "wb") as f: | ||||
|         f.write(data) | ||||
| 
 | ||||
|     with Image.open(path) as im: | ||||
|         with pytest.raises(ValueError): | ||||
|             im.load() | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "data", | ||||
|     ( | ||||
|         b"P3\n128 128\n255\n012345678910",  # half token too long | ||||
|         b"P3\n128 128\n255\n012345678910 0",  # token too long | ||||
|     ), | ||||
| ) | ||||
| def test_plain_ppm_token_too_long(tmp_path, data): | ||||
|     path = str(tmp_path / "temp.ppm") | ||||
|     with open(path, "wb") as f: | ||||
|         f.write(data) | ||||
| 
 | ||||
|     with Image.open(path) as im: | ||||
|         with pytest.raises(ValueError): | ||||
|             im.load() | ||||
| 
 | ||||
| 
 | ||||
| def test_plain_ppm_value_too_large(tmp_path): | ||||
|     path = str(tmp_path / "temp.ppm") | ||||
|     with open(path, "wb") as f: | ||||
|         f.write(b"P3\n128 128\n255\n256") | ||||
| 
 | ||||
|     with Image.open(path) as im: | ||||
|         with pytest.raises(ValueError): | ||||
|             im.load() | ||||
| 
 | ||||
| 
 | ||||
| def test_magic(): | ||||
|     with pytest.raises(SyntaxError): | ||||
|         PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid")) | ||||
| 
 | ||||
| 
 | ||||
| def test_header_with_comments(tmp_path): | ||||
|  | @ -114,7 +225,7 @@ def test_non_integer_token(tmp_path): | |||
|             pass | ||||
| 
 | ||||
| 
 | ||||
| def test_token_too_long(tmp_path): | ||||
| def test_header_token_too_long(tmp_path): | ||||
|     path = str(tmp_path / "temp.ppm") | ||||
|     with open(path, "wb") as f: | ||||
|         f.write(b"P6\n 01234567890") | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ import pytest | |||
| 
 | ||||
| from PIL import Image, PsdImagePlugin | ||||
| 
 | ||||
| from .helper import assert_image_similar, hopper, is_pypy | ||||
| from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy | ||||
| 
 | ||||
| test_file = "Tests/images/hopper.psd" | ||||
| 
 | ||||
|  | @ -107,6 +107,11 @@ def test_open_after_exclusive_load(): | |||
|         im.load() | ||||
| 
 | ||||
| 
 | ||||
| def test_rgba(): | ||||
|     with Image.open("Tests/images/rgba.psd") as im: | ||||
|         assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") | ||||
| 
 | ||||
| 
 | ||||
| def test_icc_profile(): | ||||
|     with Image.open(test_file) as im: | ||||
|         assert "icc_profile" in im.info | ||||
|  |  | |||
|  | @ -18,51 +18,48 @@ _ORIGINS = ("tl", "bl") | |||
| _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity(tmp_path): | ||||
|     for mode in _MODES: | ||||
| @pytest.mark.parametrize("mode", _MODES) | ||||
| def test_sanity(mode, tmp_path): | ||||
|     def roundtrip(original_im): | ||||
|         out = str(tmp_path / "temp.tga") | ||||
| 
 | ||||
|         def roundtrip(original_im): | ||||
|             out = str(tmp_path / "temp.tga") | ||||
|         original_im.save(out, rle=rle) | ||||
|         with Image.open(out) as saved_im: | ||||
|             if rle: | ||||
|                 assert saved_im.info["compression"] == original_im.info["compression"] | ||||
|             assert saved_im.info["orientation"] == original_im.info["orientation"] | ||||
|             if mode == "P": | ||||
|                 assert saved_im.getpalette() == original_im.getpalette() | ||||
| 
 | ||||
|             original_im.save(out, rle=rle) | ||||
|             with Image.open(out) as saved_im: | ||||
|                 if rle: | ||||
|             assert_image_equal(saved_im, original_im) | ||||
| 
 | ||||
|     png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) | ||||
| 
 | ||||
|     for png_path in png_paths: | ||||
|         with Image.open(png_path) as reference_im: | ||||
|             assert reference_im.mode == mode | ||||
| 
 | ||||
|             path_no_ext = os.path.splitext(png_path)[0] | ||||
|             for origin, rle in product(_ORIGINS, (True, False)): | ||||
|                 tga_path = "{}_{}_{}.tga".format( | ||||
|                     path_no_ext, origin, "rle" if rle else "raw" | ||||
|                 ) | ||||
| 
 | ||||
|                 with Image.open(tga_path) as original_im: | ||||
|                     assert original_im.format == "TGA" | ||||
|                     assert original_im.get_format_mimetype() == "image/x-tga" | ||||
|                     if rle: | ||||
|                         assert original_im.info["compression"] == "tga_rle" | ||||
|                     assert ( | ||||
|                         saved_im.info["compression"] == original_im.info["compression"] | ||||
|                         original_im.info["orientation"] | ||||
|                         == _ORIGIN_TO_ORIENTATION[origin] | ||||
|                     ) | ||||
|                 assert saved_im.info["orientation"] == original_im.info["orientation"] | ||||
|                 if mode == "P": | ||||
|                     assert saved_im.getpalette() == original_im.getpalette() | ||||
|                     if mode == "P": | ||||
|                         assert original_im.getpalette() == reference_im.getpalette() | ||||
| 
 | ||||
|                 assert_image_equal(saved_im, original_im) | ||||
|                     assert_image_equal(original_im, reference_im) | ||||
| 
 | ||||
|         png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) | ||||
| 
 | ||||
|         for png_path in png_paths: | ||||
|             with Image.open(png_path) as reference_im: | ||||
|                 assert reference_im.mode == mode | ||||
| 
 | ||||
|                 path_no_ext = os.path.splitext(png_path)[0] | ||||
|                 for origin, rle in product(_ORIGINS, (True, False)): | ||||
|                     tga_path = "{}_{}_{}.tga".format( | ||||
|                         path_no_ext, origin, "rle" if rle else "raw" | ||||
|                     ) | ||||
| 
 | ||||
|                     with Image.open(tga_path) as original_im: | ||||
|                         assert original_im.format == "TGA" | ||||
|                         assert original_im.get_format_mimetype() == "image/x-tga" | ||||
|                         if rle: | ||||
|                             assert original_im.info["compression"] == "tga_rle" | ||||
|                         assert ( | ||||
|                             original_im.info["orientation"] | ||||
|                             == _ORIGIN_TO_ORIENTATION[origin] | ||||
|                         ) | ||||
|                         if mode == "P": | ||||
|                             assert original_im.getpalette() == reference_im.getpalette() | ||||
| 
 | ||||
|                         assert_image_equal(original_im, reference_im) | ||||
| 
 | ||||
|                         roundtrip(original_im) | ||||
|                     roundtrip(original_im) | ||||
| 
 | ||||
| 
 | ||||
| def test_palette_depth_16(tmp_path): | ||||
|  | @ -101,6 +98,10 @@ def test_cross_scan_line(): | |||
|     with Image.open("Tests/images/cross_scan_line.tga") as im: | ||||
|         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): | ||||
|     test_file = "Tests/images/tga_id_field.tga" | ||||
|  |  | |||
|  | @ -70,6 +70,15 @@ class TestFileTiff: | |||
|             im.load() | ||||
|             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): | ||||
|         with warnings.catch_warnings(): | ||||
|             with Image.open("Tests/images/multipage.tiff") as im: | ||||
|  | @ -488,6 +497,26 @@ class TestFileTiff: | |||
|             exif = im.getexif() | ||||
|             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): | ||||
|         # Test that EXIF data can change across frames | ||||
|         with Image.open("Tests/images/g4-multi.tiff") as im: | ||||
|  | @ -706,6 +735,13 @@ class TestFileTiff: | |||
|         with Image.open(outfile) as reloaded: | ||||
|             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): | ||||
|         outfile = str(tmp_path / "temp.tif") | ||||
| 
 | ||||
|  |  | |||
|  | @ -66,10 +66,10 @@ def test_load_set_dpi(): | |||
|             assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1) | ||||
| 
 | ||||
| 
 | ||||
| def test_save(tmp_path): | ||||
| @pytest.mark.parametrize("ext", (".wmf", ".emf")) | ||||
| def test_save(ext, tmp_path): | ||||
|     im = hopper() | ||||
| 
 | ||||
|     for ext in [".wmf", ".emf"]: | ||||
|         tmpfile = str(tmp_path / ("temp" + ext)) | ||||
|         with pytest.raises(OSError): | ||||
|             im.save(tmpfile) | ||||
|     tmpfile = str(tmp_path / ("temp" + ext)) | ||||
|     with pytest.raises(OSError): | ||||
|         im.save(tmpfile) | ||||
|  |  | |||
|  | @ -49,6 +49,14 @@ def test_sanity(request, tmp_path): | |||
|     save_font(request, tmp_path) | ||||
| 
 | ||||
| 
 | ||||
| def test_less_than_256_characters(): | ||||
|     with open("Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf", "rb") as test_file: | ||||
|         font = PcfFontFile.PcfFontFile(test_file) | ||||
|     assert isinstance(font, FontFile.FontFile) | ||||
|     # check the number of characters in the font | ||||
|     assert len([_f for _f in font.glyph if _f]) == 127 | ||||
| 
 | ||||
| 
 | ||||
| def test_invalid_file(): | ||||
|     with open("Tests/images/flower.jpg", "rb") as fp: | ||||
|         with pytest.raises(SyntaxError): | ||||
|  | @ -68,12 +76,19 @@ def test_textsize(request, tmp_path): | |||
|     tempname = save_font(request, tmp_path) | ||||
|     font = ImageFont.load(tempname) | ||||
|     for i in range(255): | ||||
|         (dx, dy) = font.getsize(chr(i)) | ||||
|         (ox, oy, dx, dy) = font.getbbox(chr(i)) | ||||
|         assert ox == 0 | ||||
|         assert oy == 0 | ||||
|         assert dy == 20 | ||||
|         assert dx in (0, 10) | ||||
|         assert font.getlength(chr(i)) == dx | ||||
|         with pytest.warns(DeprecationWarning) as log: | ||||
|             assert font.getsize(chr(i)) == (dx, dy) | ||||
|         assert len(log) == 1 | ||||
|     for i in range(len(message)): | ||||
|         msg = message[: i + 1] | ||||
|         assert font.getsize(msg) == (len(msg) * 10, 20) | ||||
|         assert font.getlength(msg) == len(msg) * 10 | ||||
|         assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) | ||||
| 
 | ||||
| 
 | ||||
| def _test_high_characters(request, tmp_path, message): | ||||
|  |  | |||
|  | @ -101,13 +101,17 @@ def _test_textsize(request, tmp_path, encoding): | |||
|     tempname = save_font(request, tmp_path, encoding) | ||||
|     font = ImageFont.load(tempname) | ||||
|     for i in range(255): | ||||
|         (dx, dy) = font.getsize(bytearray([i])) | ||||
|         (ox, oy, dx, dy) = font.getbbox(bytearray([i])) | ||||
|         assert ox == 0 | ||||
|         assert oy == 0 | ||||
|         assert dy == 20 | ||||
|         assert dx in (0, 10) | ||||
|         assert font.getlength(bytearray([i])) == dx | ||||
|     message = charsets[encoding]["message"].encode(encoding) | ||||
|     for i in range(len(message)): | ||||
|         msg = message[: i + 1] | ||||
|         assert font.getsize(msg) == (len(msg) * 10, 20) | ||||
|         assert font.getlength(msg) == len(msg) * 10 | ||||
|         assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) | ||||
| 
 | ||||
| 
 | ||||
| def test_textsize_iso8859_1(request, tmp_path): | ||||
|  |  | |||
|  | @ -22,8 +22,9 @@ from .helper import ( | |||
| 
 | ||||
| 
 | ||||
| class TestImage: | ||||
|     def test_image_modes_success(self): | ||||
|         for mode in [ | ||||
|     @pytest.mark.parametrize( | ||||
|         "mode", | ||||
|         ( | ||||
|             "1", | ||||
|             "P", | ||||
|             "PA", | ||||
|  | @ -44,22 +45,18 @@ class TestImage: | |||
|             "YCbCr", | ||||
|             "LAB", | ||||
|             "HSV", | ||||
|         ]: | ||||
|             Image.new(mode, (1, 1)) | ||||
|         ), | ||||
|     ) | ||||
|     def test_image_modes_success(self, mode): | ||||
|         Image.new(mode, (1, 1)) | ||||
| 
 | ||||
|     def test_image_modes_fail(self): | ||||
|         for mode in [ | ||||
|             "", | ||||
|             "bad", | ||||
|             "very very long", | ||||
|             "BGR;15", | ||||
|             "BGR;16", | ||||
|             "BGR;24", | ||||
|             "BGR;32", | ||||
|         ]: | ||||
|             with pytest.raises(ValueError) as e: | ||||
|                 Image.new(mode, (1, 1)) | ||||
|             assert str(e.value) == "unrecognized image mode" | ||||
|     @pytest.mark.parametrize( | ||||
|         "mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24", "BGR;32") | ||||
|     ) | ||||
|     def test_image_modes_fail(self, mode): | ||||
|         with pytest.raises(ValueError) as e: | ||||
|             Image.new(mode, (1, 1)) | ||||
|         assert str(e.value) == "unrecognized image mode" | ||||
| 
 | ||||
|     def test_exception_inheritance(self): | ||||
|         assert issubclass(UnidentifiedImageError, OSError) | ||||
|  | @ -539,23 +536,22 @@ class TestImage: | |||
|         with pytest.raises(ValueError): | ||||
|             Image.linear_gradient(wrong_mode) | ||||
| 
 | ||||
|     def test_linear_gradient(self): | ||||
| 
 | ||||
|     @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) | ||||
|     def test_linear_gradient(self, mode): | ||||
|         # Arrange | ||||
|         target_file = "Tests/images/linear_gradient.png" | ||||
|         for mode in ["L", "P", "I", "F"]: | ||||
| 
 | ||||
|             # Act | ||||
|             im = Image.linear_gradient(mode) | ||||
|         # Act | ||||
|         im = Image.linear_gradient(mode) | ||||
| 
 | ||||
|             # Assert | ||||
|             assert im.size == (256, 256) | ||||
|             assert im.mode == mode | ||||
|             assert im.getpixel((0, 0)) == 0 | ||||
|             assert im.getpixel((255, 255)) == 255 | ||||
|             with Image.open(target_file) as target: | ||||
|                 target = target.convert(mode) | ||||
|             assert_image_equal(im, target) | ||||
|         # Assert | ||||
|         assert im.size == (256, 256) | ||||
|         assert im.mode == mode | ||||
|         assert im.getpixel((0, 0)) == 0 | ||||
|         assert im.getpixel((255, 255)) == 255 | ||||
|         with Image.open(target_file) as target: | ||||
|             target = target.convert(mode) | ||||
|         assert_image_equal(im, target) | ||||
| 
 | ||||
|     def test_radial_gradient_wrong_mode(self): | ||||
|         # Arrange | ||||
|  | @ -565,23 +561,22 @@ class TestImage: | |||
|         with pytest.raises(ValueError): | ||||
|             Image.radial_gradient(wrong_mode) | ||||
| 
 | ||||
|     def test_radial_gradient(self): | ||||
| 
 | ||||
|     @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) | ||||
|     def test_radial_gradient(self, mode): | ||||
|         # Arrange | ||||
|         target_file = "Tests/images/radial_gradient.png" | ||||
|         for mode in ["L", "P", "I", "F"]: | ||||
| 
 | ||||
|             # Act | ||||
|             im = Image.radial_gradient(mode) | ||||
|         # Act | ||||
|         im = Image.radial_gradient(mode) | ||||
| 
 | ||||
|             # Assert | ||||
|             assert im.size == (256, 256) | ||||
|             assert im.mode == mode | ||||
|             assert im.getpixel((0, 0)) == 255 | ||||
|             assert im.getpixel((128, 128)) == 0 | ||||
|             with Image.open(target_file) as target: | ||||
|                 target = target.convert(mode) | ||||
|             assert_image_equal(im, target) | ||||
|         # Assert | ||||
|         assert im.size == (256, 256) | ||||
|         assert im.mode == mode | ||||
|         assert im.getpixel((0, 0)) == 255 | ||||
|         assert im.getpixel((128, 128)) == 0 | ||||
|         with Image.open(target_file) as target: | ||||
|             target = target.convert(mode) | ||||
|         assert_image_equal(im, target) | ||||
| 
 | ||||
|     def test_register_extensions(self): | ||||
|         test_format = "a" | ||||
|  | @ -604,11 +599,34 @@ class TestImage: | |||
|         with Image.open("Tests/images/hopper.gif") as im: | ||||
|             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 | ||||
|         with hopper() as im: | ||||
|             with pytest.raises(ValueError): | ||||
|                 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): | ||||
|         im = hopper("RGB") | ||||
|         im_p = hopper("P") | ||||
|  | @ -826,6 +844,35 @@ class TestImage: | |||
|         im = Image.new("RGB", size) | ||||
|         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): | ||||
|         with pytest.warns(DeprecationWarning): | ||||
|             assert hopper().category == 0 | ||||
|  |  | |||
|  | @ -184,8 +184,9 @@ class TestImageGetPixel(AccessTest): | |||
|         with pytest.raises(error): | ||||
|             im.getpixel((-1, -1)) | ||||
| 
 | ||||
|     def test_basic(self): | ||||
|         for mode in ( | ||||
|     @pytest.mark.parametrize( | ||||
|         "mode", | ||||
|         ( | ||||
|             "1", | ||||
|             "L", | ||||
|             "LA", | ||||
|  | @ -200,23 +201,25 @@ class TestImageGetPixel(AccessTest): | |||
|             "RGBX", | ||||
|             "CMYK", | ||||
|             "YCbCr", | ||||
|         ): | ||||
|             self.check(mode) | ||||
|         ), | ||||
|     ) | ||||
|     def test_basic(self, mode): | ||||
|         self.check(mode) | ||||
| 
 | ||||
|     def test_signedness(self): | ||||
|     @pytest.mark.parametrize("mode", ("I;16", "I;16B")) | ||||
|     def test_signedness(self, mode): | ||||
|         # see https://github.com/python-pillow/Pillow/issues/452 | ||||
|         # pixelaccess is using signed int* instead of uint* | ||||
|         for mode in ("I;16", "I;16B"): | ||||
|             self.check(mode, 2**15 - 1) | ||||
|             self.check(mode, 2**15) | ||||
|             self.check(mode, 2**15 + 1) | ||||
|             self.check(mode, 2**16 - 1) | ||||
|         self.check(mode, 2**15 - 1) | ||||
|         self.check(mode, 2**15) | ||||
|         self.check(mode, 2**15 + 1) | ||||
|         self.check(mode, 2**16 - 1) | ||||
| 
 | ||||
|     def test_p_putpixel_rgb_rgba(self): | ||||
|         for color in [(255, 0, 0), (255, 0, 0, 255)]: | ||||
|             im = Image.new("P", (1, 1), 0) | ||||
|             im.putpixel((0, 0), color) | ||||
|             assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) | ||||
|     @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) | ||||
|     def test_p_putpixel_rgb_rgba(self, color): | ||||
|         im = Image.new("P", (1, 1), 0) | ||||
|         im.putpixel((0, 0), color) | ||||
|         assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(cffi is None, reason="No CFFI") | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| import pytest | ||||
| from packaging.version import parse as parse_version | ||||
| 
 | ||||
| from PIL import Image | ||||
| 
 | ||||
|  | @ -34,9 +35,10 @@ def test_toarray(): | |||
|     test_with_dtype(numpy.float64) | ||||
|     test_with_dtype(numpy.uint8) | ||||
| 
 | ||||
|     with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: | ||||
|         with pytest.raises(OSError): | ||||
|             numpy.array(im_truncated) | ||||
|     if parse_version(numpy.__version__) >= parse_version("1.23"): | ||||
|         with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: | ||||
|             with pytest.raises(OSError): | ||||
|                 numpy.array(im_truncated) | ||||
| 
 | ||||
| 
 | ||||
| def test_fromarray(): | ||||
|  | @ -80,3 +82,15 @@ def test_fromarray(): | |||
|     with pytest.raises(TypeError): | ||||
|         wrapped = Wrapper(test("L"), {"shape": (100, 128)}) | ||||
|         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 | ||||
|  |  | |||
|  | @ -222,6 +222,20 @@ def test_p_la(): | |||
|     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(): | ||||
|     # Arrange | ||||
|     im = hopper("CMYK") | ||||
|  | @ -254,36 +268,33 @@ def test_matrix_wrong_mode(): | |||
|         im.convert(mode="L", matrix=matrix) | ||||
| 
 | ||||
| 
 | ||||
| def test_matrix_xyz(): | ||||
|     def matrix_convert(mode): | ||||
|         # Arrange | ||||
|         im = hopper("RGB") | ||||
|         im.info["transparency"] = (255, 0, 0) | ||||
|         # fmt: off | ||||
|         matrix = ( | ||||
|             0.412453, 0.357580, 0.180423, 0, | ||||
|             0.212671, 0.715160, 0.072169, 0, | ||||
|             0.019334, 0.119193, 0.950227, 0) | ||||
|         # fmt: on | ||||
|         assert im.mode == "RGB" | ||||
| @pytest.mark.parametrize("mode", ("RGB", "L")) | ||||
| def test_matrix_xyz(mode): | ||||
|     # Arrange | ||||
|     im = hopper("RGB") | ||||
|     im.info["transparency"] = (255, 0, 0) | ||||
|     # fmt: off | ||||
|     matrix = ( | ||||
|         0.412453, 0.357580, 0.180423, 0, | ||||
|         0.212671, 0.715160, 0.072169, 0, | ||||
|         0.019334, 0.119193, 0.950227, 0) | ||||
|     # fmt: on | ||||
|     assert im.mode == "RGB" | ||||
| 
 | ||||
|         # Act | ||||
|         # Convert an RGB image to the CIE XYZ colour space | ||||
|         converted_im = im.convert(mode=mode, matrix=matrix) | ||||
|     # Act | ||||
|     # Convert an RGB image to the CIE XYZ colour space | ||||
|     converted_im = im.convert(mode=mode, matrix=matrix) | ||||
| 
 | ||||
|         # Assert | ||||
|         assert converted_im.mode == mode | ||||
|         assert converted_im.size == im.size | ||||
|         with Image.open("Tests/images/hopper-XYZ.png") as target: | ||||
|             if converted_im.mode == "RGB": | ||||
|                 assert_image_similar(converted_im, target, 3) | ||||
|                 assert converted_im.info["transparency"] == (105, 54, 4) | ||||
|             else: | ||||
|                 assert_image_similar(converted_im, target.getchannel(0), 1) | ||||
|                 assert converted_im.info["transparency"] == 105 | ||||
| 
 | ||||
|     matrix_convert("RGB") | ||||
|     matrix_convert("L") | ||||
|     # Assert | ||||
|     assert converted_im.mode == mode | ||||
|     assert converted_im.size == im.size | ||||
|     with Image.open("Tests/images/hopper-XYZ.png") as target: | ||||
|         if converted_im.mode == "RGB": | ||||
|             assert_image_similar(converted_im, target, 3) | ||||
|             assert converted_im.info["transparency"] == (105, 54, 4) | ||||
|         else: | ||||
|             assert_image_similar(converted_im, target.getchannel(0), 1) | ||||
|             assert converted_im.info["transparency"] == 105 | ||||
| 
 | ||||
| 
 | ||||
| def test_matrix_identity(): | ||||
|  |  | |||
|  | @ -1,37 +1,40 @@ | |||
| import copy | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image | ||||
| 
 | ||||
| from .helper import hopper | ||||
| 
 | ||||
| 
 | ||||
| def test_copy(): | ||||
| @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) | ||||
| def test_copy(mode): | ||||
|     cropped_coordinates = (10, 10, 20, 20) | ||||
|     cropped_size = (10, 10) | ||||
|     for mode in "1", "P", "L", "RGB", "I", "F": | ||||
|         # Internal copy method | ||||
|         im = hopper(mode) | ||||
|         out = im.copy() | ||||
|         assert out.mode == im.mode | ||||
|         assert out.size == im.size | ||||
| 
 | ||||
|         # Python's copy method | ||||
|         im = hopper(mode) | ||||
|         out = copy.copy(im) | ||||
|         assert out.mode == im.mode | ||||
|         assert out.size == im.size | ||||
|     # Internal copy method | ||||
|     im = hopper(mode) | ||||
|     out = im.copy() | ||||
|     assert out.mode == im.mode | ||||
|     assert out.size == im.size | ||||
| 
 | ||||
|         # Internal copy method on a cropped image | ||||
|         im = hopper(mode) | ||||
|         out = im.crop(cropped_coordinates).copy() | ||||
|         assert out.mode == im.mode | ||||
|         assert out.size == cropped_size | ||||
|     # Python's copy method | ||||
|     im = hopper(mode) | ||||
|     out = copy.copy(im) | ||||
|     assert out.mode == im.mode | ||||
|     assert out.size == im.size | ||||
| 
 | ||||
|         # Python's copy method on a cropped image | ||||
|         im = hopper(mode) | ||||
|         out = copy.copy(im.crop(cropped_coordinates)) | ||||
|         assert out.mode == im.mode | ||||
|         assert out.size == cropped_size | ||||
|     # Internal copy method on a cropped image | ||||
|     im = hopper(mode) | ||||
|     out = im.crop(cropped_coordinates).copy() | ||||
|     assert out.mode == im.mode | ||||
|     assert out.size == cropped_size | ||||
| 
 | ||||
|     # Python's copy method on a cropped image | ||||
|     im = hopper(mode) | ||||
|     out = copy.copy(im.crop(cropped_coordinates)) | ||||
|     assert out.mode == im.mode | ||||
|     assert out.size == cropped_size | ||||
| 
 | ||||
| 
 | ||||
| def test_copy_zero(): | ||||
|  |  | |||
|  | @ -5,17 +5,14 @@ from PIL import Image | |||
| from .helper import assert_image_equal, hopper | ||||
| 
 | ||||
| 
 | ||||
| def test_crop(): | ||||
|     def crop(mode): | ||||
|         im = hopper(mode) | ||||
|         assert_image_equal(im.crop(), im) | ||||
| @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) | ||||
| def test_crop(mode): | ||||
|     im = hopper(mode) | ||||
|     assert_image_equal(im.crop(), im) | ||||
| 
 | ||||
|         cropped = im.crop((50, 50, 100, 100)) | ||||
|         assert cropped.mode == mode | ||||
|         assert cropped.size == (50, 50) | ||||
| 
 | ||||
|     for mode in "1", "P", "L", "RGB", "I", "F": | ||||
|         crop(mode) | ||||
|     cropped = im.crop((50, 50, 100, 100)) | ||||
|     assert cropped.mode == mode | ||||
|     assert cropped.size == (50, 50) | ||||
| 
 | ||||
| 
 | ||||
| def test_wide_crop(): | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ def test_entropy(): | |||
|     assert round(abs(entropy("L") - 7.063008716585465), 7) == 0 | ||||
|     assert round(abs(entropy("I") - 7.063008716585465), 7) == 0 | ||||
|     assert round(abs(entropy("F") - 7.063008716585465), 7) == 0 | ||||
|     assert round(abs(entropy("P") - 5.0530452472519745), 7) == 0 | ||||
|     assert round(abs(entropy("P") - 5.082506854662517), 7) == 0 | ||||
|     assert round(abs(entropy("RGB") - 8.821286587714319), 7) == 0 | ||||
|     assert round(abs(entropy("RGBA") - 7.42724306524488), 7) == 0 | ||||
|     assert round(abs(entropy("CMYK") - 7.4272430652448795), 7) == 0 | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ def test_getcolors(): | |||
|     assert getcolors("L") == 255 | ||||
|     assert getcolors("I") == 255 | ||||
|     assert getcolors("F") == 255 | ||||
|     assert getcolors("P") == 90  # fixed palette | ||||
|     assert getcolors("P") == 96  # fixed palette | ||||
|     assert getcolors("RGB") is None | ||||
|     assert getcolors("RGBA") is None | ||||
|     assert getcolors("CMYK") is None | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ def test_histogram(): | |||
|     assert histogram("L") == (256, 0, 662) | ||||
|     assert histogram("I") == (256, 0, 662) | ||||
|     assert histogram("F") == (256, 0, 662) | ||||
|     assert histogram("P") == (256, 0, 1871) | ||||
|     assert histogram("P") == (256, 0, 1551) | ||||
|     assert histogram("RGB") == (768, 4, 675) | ||||
|     assert histogram("RGBA") == (1024, 0, 16384) | ||||
|     assert histogram("CMYK") == (1024, 0, 16384) | ||||
|  |  | |||
|  | @ -1,3 +1,5 @@ | |||
| import pytest | ||||
| 
 | ||||
| from PIL import Image | ||||
| 
 | ||||
| from .helper import CachedProperty, assert_image_equal | ||||
|  | @ -101,226 +103,226 @@ class TestImagingPaste: | |||
|             ], | ||||
|         ) | ||||
| 
 | ||||
|     def test_image_solid(self): | ||||
|         for mode in ("RGBA", "RGB", "L"): | ||||
|             im = Image.new(mode, (200, 200), "red") | ||||
|             im2 = getattr(self, "gradient_" + mode) | ||||
|     @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) | ||||
|     def test_image_solid(self, mode): | ||||
|         im = Image.new(mode, (200, 200), "red") | ||||
|         im2 = getattr(self, "gradient_" + mode) | ||||
| 
 | ||||
|             im.paste(im2, (12, 23)) | ||||
|         im.paste(im2, (12, 23)) | ||||
| 
 | ||||
|             im = im.crop((12, 23, im2.width + 12, im2.height + 23)) | ||||
|             assert_image_equal(im, im2) | ||||
|         im = im.crop((12, 23, im2.width + 12, im2.height + 23)) | ||||
|         assert_image_equal(im, im2) | ||||
| 
 | ||||
|     def test_image_mask_1(self): | ||||
|         for mode in ("RGBA", "RGB", "L"): | ||||
|             im = Image.new(mode, (200, 200), "white") | ||||
|             im2 = getattr(self, "gradient_" + mode) | ||||
|     @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) | ||||
|     def test_image_mask_1(self, mode): | ||||
|         im = Image.new(mode, (200, 200), "white") | ||||
|         im2 = getattr(self, "gradient_" + mode) | ||||
| 
 | ||||
|             self.assert_9points_paste( | ||||
|                 im, | ||||
|                 im2, | ||||
|                 self.mask_1, | ||||
|                 [ | ||||
|                     (255, 255, 255, 255), | ||||
|                     (255, 255, 255, 255), | ||||
|                     (127, 254, 127, 0), | ||||
|                     (255, 255, 255, 255), | ||||
|                     (255, 255, 255, 255), | ||||
|                     (191, 190, 63, 64), | ||||
|                     (127, 0, 127, 254), | ||||
|                     (191, 64, 63, 190), | ||||
|                     (255, 255, 255, 255), | ||||
|                 ], | ||||
|             ) | ||||
|         self.assert_9points_paste( | ||||
|             im, | ||||
|             im2, | ||||
|             self.mask_1, | ||||
|             [ | ||||
|                 (255, 255, 255, 255), | ||||
|                 (255, 255, 255, 255), | ||||
|                 (127, 254, 127, 0), | ||||
|                 (255, 255, 255, 255), | ||||
|                 (255, 255, 255, 255), | ||||
|                 (191, 190, 63, 64), | ||||
|                 (127, 0, 127, 254), | ||||
|                 (191, 64, 63, 190), | ||||
|                 (255, 255, 255, 255), | ||||
|             ], | ||||
|         ) | ||||
| 
 | ||||
|     def test_image_mask_L(self): | ||||
|         for mode in ("RGBA", "RGB", "L"): | ||||
|             im = Image.new(mode, (200, 200), "white") | ||||
|             im2 = getattr(self, "gradient_" + mode) | ||||
|     @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) | ||||
|     def test_image_mask_L(self, mode): | ||||
|         im = Image.new(mode, (200, 200), "white") | ||||
|         im2 = getattr(self, "gradient_" + mode) | ||||
| 
 | ||||
|             self.assert_9points_paste( | ||||
|                 im, | ||||
|                 im2, | ||||
|                 self.mask_L, | ||||
|                 [ | ||||
|                     (128, 191, 255, 191), | ||||
|                     (208, 239, 239, 208), | ||||
|                     (255, 255, 255, 255), | ||||
|                     (112, 111, 206, 207), | ||||
|                     (192, 191, 191, 191), | ||||
|                     (239, 239, 207, 207), | ||||
|                     (128, 1, 128, 254), | ||||
|                     (207, 113, 112, 207), | ||||
|                     (255, 191, 128, 191), | ||||
|                 ], | ||||
|             ) | ||||
|         self.assert_9points_paste( | ||||
|             im, | ||||
|             im2, | ||||
|             self.mask_L, | ||||
|             [ | ||||
|                 (128, 191, 255, 191), | ||||
|                 (208, 239, 239, 208), | ||||
|                 (255, 255, 255, 255), | ||||
|                 (112, 111, 206, 207), | ||||
|                 (192, 191, 191, 191), | ||||
|                 (239, 239, 207, 207), | ||||
|                 (128, 1, 128, 254), | ||||
|                 (207, 113, 112, 207), | ||||
|                 (255, 191, 128, 191), | ||||
|             ], | ||||
|         ) | ||||
| 
 | ||||
|     def test_image_mask_LA(self): | ||||
|         for mode in ("RGBA", "RGB", "L"): | ||||
|             im = Image.new(mode, (200, 200), "white") | ||||
|             im2 = getattr(self, "gradient_" + mode) | ||||
|     @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) | ||||
|     def test_image_mask_LA(self, mode): | ||||
|         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), | ||||
|                 ], | ||||
|             ) | ||||
|         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): | ||||
|         for mode in ("RGBA", "RGB", "L"): | ||||
|             im = Image.new(mode, (200, 200), "white") | ||||
|             im2 = getattr(self, "gradient_" + mode) | ||||
|     @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) | ||||
|     def test_image_mask_RGBA(self, mode): | ||||
|         im = Image.new(mode, (200, 200), "white") | ||||
|         im2 = getattr(self, "gradient_" + mode) | ||||
| 
 | ||||
|             self.assert_9points_paste( | ||||
|                 im, | ||||
|                 im2, | ||||
|                 self.gradient_RGBA, | ||||
|                 [ | ||||
|                     (128, 191, 255, 191), | ||||
|                     (208, 239, 239, 208), | ||||
|                     (255, 255, 255, 255), | ||||
|                     (112, 111, 206, 207), | ||||
|                     (192, 191, 191, 191), | ||||
|                     (239, 239, 207, 207), | ||||
|                     (128, 1, 128, 254), | ||||
|                     (207, 113, 112, 207), | ||||
|                     (255, 191, 128, 191), | ||||
|                 ], | ||||
|             ) | ||||
|         self.assert_9points_paste( | ||||
|             im, | ||||
|             im2, | ||||
|             self.gradient_RGBA, | ||||
|             [ | ||||
|                 (128, 191, 255, 191), | ||||
|                 (208, 239, 239, 208), | ||||
|                 (255, 255, 255, 255), | ||||
|                 (112, 111, 206, 207), | ||||
|                 (192, 191, 191, 191), | ||||
|                 (239, 239, 207, 207), | ||||
|                 (128, 1, 128, 254), | ||||
|                 (207, 113, 112, 207), | ||||
|                 (255, 191, 128, 191), | ||||
|             ], | ||||
|         ) | ||||
| 
 | ||||
|     def test_image_mask_RGBa(self): | ||||
|         for mode in ("RGBA", "RGB", "L"): | ||||
|             im = Image.new(mode, (200, 200), "white") | ||||
|             im2 = getattr(self, "gradient_" + mode) | ||||
|     @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) | ||||
|     def test_image_mask_RGBa(self, mode): | ||||
|         im = Image.new(mode, (200, 200), "white") | ||||
|         im2 = getattr(self, "gradient_" + mode) | ||||
| 
 | ||||
|             self.assert_9points_paste( | ||||
|                 im, | ||||
|                 im2, | ||||
|                 self.gradient_RGBa, | ||||
|                 [ | ||||
|                     (128, 255, 126, 255), | ||||
|                     (0, 127, 126, 255), | ||||
|                     (126, 253, 126, 255), | ||||
|                     (128, 127, 254, 255), | ||||
|                     (0, 255, 254, 255), | ||||
|                     (126, 125, 254, 255), | ||||
|                     (128, 1, 128, 255), | ||||
|                     (0, 129, 128, 255), | ||||
|                     (126, 255, 128, 255), | ||||
|                 ], | ||||
|             ) | ||||
|         self.assert_9points_paste( | ||||
|             im, | ||||
|             im2, | ||||
|             self.gradient_RGBa, | ||||
|             [ | ||||
|                 (128, 255, 126, 255), | ||||
|                 (0, 127, 126, 255), | ||||
|                 (126, 253, 126, 255), | ||||
|                 (128, 127, 254, 255), | ||||
|                 (0, 255, 254, 255), | ||||
|                 (126, 125, 254, 255), | ||||
|                 (128, 1, 128, 255), | ||||
|                 (0, 129, 128, 255), | ||||
|                 (126, 255, 128, 255), | ||||
|             ], | ||||
|         ) | ||||
| 
 | ||||
|     def test_color_solid(self): | ||||
|         for mode in ("RGBA", "RGB", "L"): | ||||
|             im = Image.new(mode, (200, 200), "black") | ||||
|     @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) | ||||
|     def test_color_solid(self, mode): | ||||
|         im = Image.new(mode, (200, 200), "black") | ||||
| 
 | ||||
|             rect = (12, 23, 128 + 12, 128 + 23) | ||||
|             im.paste("white", rect) | ||||
|         rect = (12, 23, 128 + 12, 128 + 23) | ||||
|         im.paste("white", rect) | ||||
| 
 | ||||
|             hist = im.crop(rect).histogram() | ||||
|             while hist: | ||||
|                 head, hist = hist[:256], hist[256:] | ||||
|                 assert head[255] == 128 * 128 | ||||
|                 assert sum(head[:255]) == 0 | ||||
|         hist = im.crop(rect).histogram() | ||||
|         while hist: | ||||
|             head, hist = hist[:256], hist[256:] | ||||
|             assert head[255] == 128 * 128 | ||||
|             assert sum(head[:255]) == 0 | ||||
| 
 | ||||
|     def test_color_mask_1(self): | ||||
|         for mode in ("RGBA", "RGB", "L"): | ||||
|             im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) | ||||
|             color = (10, 20, 30, 40)[: len(mode)] | ||||
|     @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) | ||||
|     def test_color_mask_1(self, mode): | ||||
|         im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) | ||||
|         color = (10, 20, 30, 40)[: len(mode)] | ||||
| 
 | ||||
|             self.assert_9points_paste( | ||||
|                 im, | ||||
|                 color, | ||||
|                 self.mask_1, | ||||
|                 [ | ||||
|                     (50, 60, 70, 80), | ||||
|                     (50, 60, 70, 80), | ||||
|                     (10, 20, 30, 40), | ||||
|                     (50, 60, 70, 80), | ||||
|                     (50, 60, 70, 80), | ||||
|                     (10, 20, 30, 40), | ||||
|                     (10, 20, 30, 40), | ||||
|                     (10, 20, 30, 40), | ||||
|                     (50, 60, 70, 80), | ||||
|                 ], | ||||
|             ) | ||||
|         self.assert_9points_paste( | ||||
|             im, | ||||
|             color, | ||||
|             self.mask_1, | ||||
|             [ | ||||
|                 (50, 60, 70, 80), | ||||
|                 (50, 60, 70, 80), | ||||
|                 (10, 20, 30, 40), | ||||
|                 (50, 60, 70, 80), | ||||
|                 (50, 60, 70, 80), | ||||
|                 (10, 20, 30, 40), | ||||
|                 (10, 20, 30, 40), | ||||
|                 (10, 20, 30, 40), | ||||
|                 (50, 60, 70, 80), | ||||
|             ], | ||||
|         ) | ||||
| 
 | ||||
|     def test_color_mask_L(self): | ||||
|         for mode in ("RGBA", "RGB", "L"): | ||||
|             im = getattr(self, "gradient_" + mode).copy() | ||||
|             color = "white" | ||||
|     @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) | ||||
|     def test_color_mask_L(self, mode): | ||||
|         im = getattr(self, "gradient_" + mode).copy() | ||||
|         color = "white" | ||||
| 
 | ||||
|             self.assert_9points_paste( | ||||
|                 im, | ||||
|                 color, | ||||
|                 self.mask_L, | ||||
|                 [ | ||||
|                     (127, 191, 254, 191), | ||||
|                     (111, 207, 206, 110), | ||||
|                     (127, 254, 127, 0), | ||||
|                     (207, 207, 239, 239), | ||||
|                     (191, 191, 190, 191), | ||||
|                     (207, 206, 111, 112), | ||||
|                     (254, 254, 254, 255), | ||||
|                     (239, 206, 206, 238), | ||||
|                     (254, 191, 127, 191), | ||||
|                 ], | ||||
|             ) | ||||
|         self.assert_9points_paste( | ||||
|             im, | ||||
|             color, | ||||
|             self.mask_L, | ||||
|             [ | ||||
|                 (127, 191, 254, 191), | ||||
|                 (111, 207, 206, 110), | ||||
|                 (127, 254, 127, 0), | ||||
|                 (207, 207, 239, 239), | ||||
|                 (191, 191, 190, 191), | ||||
|                 (207, 206, 111, 112), | ||||
|                 (254, 254, 254, 255), | ||||
|                 (239, 206, 206, 238), | ||||
|                 (254, 191, 127, 191), | ||||
|             ], | ||||
|         ) | ||||
| 
 | ||||
|     def test_color_mask_RGBA(self): | ||||
|         for mode in ("RGBA", "RGB", "L"): | ||||
|             im = getattr(self, "gradient_" + mode).copy() | ||||
|             color = "white" | ||||
|     @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) | ||||
|     def test_color_mask_RGBA(self, mode): | ||||
|         im = getattr(self, "gradient_" + mode).copy() | ||||
|         color = "white" | ||||
| 
 | ||||
|             self.assert_9points_paste( | ||||
|                 im, | ||||
|                 color, | ||||
|                 self.gradient_RGBA, | ||||
|                 [ | ||||
|                     (127, 191, 254, 191), | ||||
|                     (111, 207, 206, 110), | ||||
|                     (127, 254, 127, 0), | ||||
|                     (207, 207, 239, 239), | ||||
|                     (191, 191, 190, 191), | ||||
|                     (207, 206, 111, 112), | ||||
|                     (254, 254, 254, 255), | ||||
|                     (239, 206, 206, 238), | ||||
|                     (254, 191, 127, 191), | ||||
|                 ], | ||||
|             ) | ||||
|         self.assert_9points_paste( | ||||
|             im, | ||||
|             color, | ||||
|             self.gradient_RGBA, | ||||
|             [ | ||||
|                 (127, 191, 254, 191), | ||||
|                 (111, 207, 206, 110), | ||||
|                 (127, 254, 127, 0), | ||||
|                 (207, 207, 239, 239), | ||||
|                 (191, 191, 190, 191), | ||||
|                 (207, 206, 111, 112), | ||||
|                 (254, 254, 254, 255), | ||||
|                 (239, 206, 206, 238), | ||||
|                 (254, 191, 127, 191), | ||||
|             ], | ||||
|         ) | ||||
| 
 | ||||
|     def test_color_mask_RGBa(self): | ||||
|         for mode in ("RGBA", "RGB", "L"): | ||||
|             im = getattr(self, "gradient_" + mode).copy() | ||||
|             color = "white" | ||||
|     @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) | ||||
|     def test_color_mask_RGBa(self, mode): | ||||
|         im = getattr(self, "gradient_" + mode).copy() | ||||
|         color = "white" | ||||
| 
 | ||||
|             self.assert_9points_paste( | ||||
|                 im, | ||||
|                 color, | ||||
|                 self.gradient_RGBa, | ||||
|                 [ | ||||
|                     (255, 63, 126, 63), | ||||
|                     (47, 143, 142, 46), | ||||
|                     (126, 253, 126, 255), | ||||
|                     (15, 15, 47, 47), | ||||
|                     (63, 63, 62, 63), | ||||
|                     (142, 141, 46, 47), | ||||
|                     (255, 255, 255, 0), | ||||
|                     (48, 15, 15, 47), | ||||
|                     (126, 63, 255, 63), | ||||
|                 ], | ||||
|             ) | ||||
|         self.assert_9points_paste( | ||||
|             im, | ||||
|             color, | ||||
|             self.gradient_RGBa, | ||||
|             [ | ||||
|                 (255, 63, 126, 63), | ||||
|                 (47, 143, 142, 46), | ||||
|                 (126, 253, 126, 255), | ||||
|                 (15, 15, 47, 47), | ||||
|                 (63, 63, 62, 63), | ||||
|                 (142, 141, 46, 47), | ||||
|                 (255, 255, 255, 0), | ||||
|                 (48, 15, 15, 47), | ||||
|                 (126, 63, 255, 63), | ||||
|             ], | ||||
|         ) | ||||
| 
 | ||||
|     def test_different_sizes(self): | ||||
|         im = Image.new("RGB", (100, 100)) | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| import pytest | ||||
| 
 | ||||
| from PIL import Image | ||||
| 
 | ||||
| from .helper import assert_image_equal, hopper | ||||
| 
 | ||||
| 
 | ||||
|  | @ -17,11 +19,24 @@ def test_sanity(): | |||
|         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 + 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): | ||||
|         im.point(lambda x: x - 1) | ||||
|         im.point(lambda x: x * x) | ||||
|     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(): | ||||
|  | @ -47,3 +62,8 @@ def test_f_mode(): | |||
|     im = hopper("F") | ||||
|     with pytest.raises(ValueError): | ||||
|         im.point(None) | ||||
| 
 | ||||
| 
 | ||||
| def test_coerce_e_deprecation(): | ||||
|     with pytest.warns(DeprecationWarning): | ||||
|         assert Image.coerce_e(2).data == 2 | ||||
|  |  | |||
|  | @ -65,6 +65,22 @@ def test_quantize_no_dither(): | |||
|     assert converted.palette.palette == palette.palette.palette | ||||
| 
 | ||||
| 
 | ||||
| def test_quantize_no_dither2(): | ||||
|     im = Image.new("RGB", (9, 1)) | ||||
|     im.putdata(list((p,) * 3 for p in range(0, 36, 4))) | ||||
| 
 | ||||
|     palette = Image.new("P", (1, 1)) | ||||
|     data = (0, 0, 0, 32, 32, 32) | ||||
|     palette.putpalette(data) | ||||
|     quantized = im.quantize(dither=Image.Dither.NONE, palette=palette) | ||||
| 
 | ||||
|     assert tuple(quantized.palette.palette) == data | ||||
| 
 | ||||
|     px = quantized.load() | ||||
|     for x in range(9): | ||||
|         assert px[x, 0] == (0 if x < 5 else 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_quantize_dither_diff(): | ||||
|     image = hopper() | ||||
|     with Image.open("Tests/images/caption_6_33_22.png") as palette: | ||||
|  |  | |||
|  | @ -100,40 +100,41 @@ class TestImagingCoreResampleAccuracy: | |||
|             for y in range(image.size[1]) | ||||
|         ) | ||||
| 
 | ||||
|     def test_reduce_box(self): | ||||
|         for mode in ["RGBX", "RGB", "La", "L"]: | ||||
|             case = self.make_case(mode, (8, 8), 0xE1) | ||||
|             case = case.resize((4, 4), Image.Resampling.BOX) | ||||
|             # fmt: off | ||||
|             data = ("e1 e1" | ||||
|                     "e1 e1") | ||||
|             # fmt: on | ||||
|             for channel in case.split(): | ||||
|                 self.check_case(channel, self.make_sample(data, (4, 4))) | ||||
|     @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) | ||||
|     def test_reduce_box(self, mode): | ||||
|         case = self.make_case(mode, (8, 8), 0xE1) | ||||
|         case = case.resize((4, 4), Image.Resampling.BOX) | ||||
|         # fmt: off | ||||
|         data = ("e1 e1" | ||||
|                 "e1 e1") | ||||
|         # fmt: on | ||||
|         for channel in case.split(): | ||||
|             self.check_case(channel, self.make_sample(data, (4, 4))) | ||||
| 
 | ||||
|     def test_reduce_bilinear(self): | ||||
|         for mode in ["RGBX", "RGB", "La", "L"]: | ||||
|             case = self.make_case(mode, (8, 8), 0xE1) | ||||
|             case = case.resize((4, 4), Image.Resampling.BILINEAR) | ||||
|             # fmt: off | ||||
|             data = ("e1 c9" | ||||
|                     "c9 b7") | ||||
|             # fmt: on | ||||
|             for channel in case.split(): | ||||
|                 self.check_case(channel, self.make_sample(data, (4, 4))) | ||||
|     @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) | ||||
|     def test_reduce_bilinear(self, mode): | ||||
|         case = self.make_case(mode, (8, 8), 0xE1) | ||||
|         case = case.resize((4, 4), Image.Resampling.BILINEAR) | ||||
|         # fmt: off | ||||
|         data = ("e1 c9" | ||||
|                 "c9 b7") | ||||
|         # fmt: on | ||||
|         for channel in case.split(): | ||||
|             self.check_case(channel, self.make_sample(data, (4, 4))) | ||||
| 
 | ||||
|     def test_reduce_hamming(self): | ||||
|         for mode in ["RGBX", "RGB", "La", "L"]: | ||||
|             case = self.make_case(mode, (8, 8), 0xE1) | ||||
|             case = case.resize((4, 4), Image.Resampling.HAMMING) | ||||
|             # fmt: off | ||||
|             data = ("e1 da" | ||||
|                     "da d3") | ||||
|             # fmt: on | ||||
|             for channel in case.split(): | ||||
|                 self.check_case(channel, self.make_sample(data, (4, 4))) | ||||
|     @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) | ||||
|     def test_reduce_hamming(self, mode): | ||||
|         case = self.make_case(mode, (8, 8), 0xE1) | ||||
|         case = case.resize((4, 4), Image.Resampling.HAMMING) | ||||
|         # fmt: off | ||||
|         data = ("e1 da" | ||||
|                 "da d3") | ||||
|         # fmt: on | ||||
|         for channel in case.split(): | ||||
|             self.check_case(channel, self.make_sample(data, (4, 4))) | ||||
| 
 | ||||
|     def test_reduce_bicubic(self): | ||||
|     @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) | ||||
|     def test_reduce_bicubic(self, mode): | ||||
|         for mode in ["RGBX", "RGB", "La", "L"]: | ||||
|             case = self.make_case(mode, (12, 12), 0xE1) | ||||
|             case = case.resize((6, 6), Image.Resampling.BICUBIC) | ||||
|  | @ -145,79 +146,79 @@ class TestImagingCoreResampleAccuracy: | |||
|             for channel in case.split(): | ||||
|                 self.check_case(channel, self.make_sample(data, (6, 6))) | ||||
| 
 | ||||
|     def test_reduce_lanczos(self): | ||||
|         for mode in ["RGBX", "RGB", "La", "L"]: | ||||
|             case = self.make_case(mode, (16, 16), 0xE1) | ||||
|             case = case.resize((8, 8), Image.Resampling.LANCZOS) | ||||
|             # fmt: off | ||||
|             data = ("e1 e0 e4 d7" | ||||
|                     "e0 df e3 d6" | ||||
|                     "e4 e3 e7 da" | ||||
|                     "d7 d6 d9 ce") | ||||
|             # fmt: on | ||||
|             for channel in case.split(): | ||||
|                 self.check_case(channel, self.make_sample(data, (8, 8))) | ||||
|     @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) | ||||
|     def test_reduce_lanczos(self, mode): | ||||
|         case = self.make_case(mode, (16, 16), 0xE1) | ||||
|         case = case.resize((8, 8), Image.Resampling.LANCZOS) | ||||
|         # fmt: off | ||||
|         data = ("e1 e0 e4 d7" | ||||
|                 "e0 df e3 d6" | ||||
|                 "e4 e3 e7 da" | ||||
|                 "d7 d6 d9 ce") | ||||
|         # fmt: on | ||||
|         for channel in case.split(): | ||||
|             self.check_case(channel, self.make_sample(data, (8, 8))) | ||||
| 
 | ||||
|     def test_enlarge_box(self): | ||||
|         for mode in ["RGBX", "RGB", "La", "L"]: | ||||
|             case = self.make_case(mode, (2, 2), 0xE1) | ||||
|             case = case.resize((4, 4), Image.Resampling.BOX) | ||||
|             # fmt: off | ||||
|             data = ("e1 e1" | ||||
|                     "e1 e1") | ||||
|             # fmt: on | ||||
|             for channel in case.split(): | ||||
|                 self.check_case(channel, self.make_sample(data, (4, 4))) | ||||
|     @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) | ||||
|     def test_enlarge_box(self, mode): | ||||
|         case = self.make_case(mode, (2, 2), 0xE1) | ||||
|         case = case.resize((4, 4), Image.Resampling.BOX) | ||||
|         # fmt: off | ||||
|         data = ("e1 e1" | ||||
|                 "e1 e1") | ||||
|         # fmt: on | ||||
|         for channel in case.split(): | ||||
|             self.check_case(channel, self.make_sample(data, (4, 4))) | ||||
| 
 | ||||
|     def test_enlarge_bilinear(self): | ||||
|         for mode in ["RGBX", "RGB", "La", "L"]: | ||||
|             case = self.make_case(mode, (2, 2), 0xE1) | ||||
|             case = case.resize((4, 4), Image.Resampling.BILINEAR) | ||||
|             # fmt: off | ||||
|             data = ("e1 b0" | ||||
|                     "b0 98") | ||||
|             # fmt: on | ||||
|             for channel in case.split(): | ||||
|                 self.check_case(channel, self.make_sample(data, (4, 4))) | ||||
|     @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) | ||||
|     def test_enlarge_bilinear(self, mode): | ||||
|         case = self.make_case(mode, (2, 2), 0xE1) | ||||
|         case = case.resize((4, 4), Image.Resampling.BILINEAR) | ||||
|         # fmt: off | ||||
|         data = ("e1 b0" | ||||
|                 "b0 98") | ||||
|         # fmt: on | ||||
|         for channel in case.split(): | ||||
|             self.check_case(channel, self.make_sample(data, (4, 4))) | ||||
| 
 | ||||
|     def test_enlarge_hamming(self): | ||||
|         for mode in ["RGBX", "RGB", "La", "L"]: | ||||
|             case = self.make_case(mode, (2, 2), 0xE1) | ||||
|             case = case.resize((4, 4), Image.Resampling.HAMMING) | ||||
|             # fmt: off | ||||
|             data = ("e1 d2" | ||||
|                     "d2 c5") | ||||
|             # fmt: on | ||||
|             for channel in case.split(): | ||||
|                 self.check_case(channel, self.make_sample(data, (4, 4))) | ||||
|     @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) | ||||
|     def test_enlarge_hamming(self, mode): | ||||
|         case = self.make_case(mode, (2, 2), 0xE1) | ||||
|         case = case.resize((4, 4), Image.Resampling.HAMMING) | ||||
|         # fmt: off | ||||
|         data = ("e1 d2" | ||||
|                 "d2 c5") | ||||
|         # fmt: on | ||||
|         for channel in case.split(): | ||||
|             self.check_case(channel, self.make_sample(data, (4, 4))) | ||||
| 
 | ||||
|     def test_enlarge_bicubic(self): | ||||
|         for mode in ["RGBX", "RGB", "La", "L"]: | ||||
|             case = self.make_case(mode, (4, 4), 0xE1) | ||||
|             case = case.resize((8, 8), Image.Resampling.BICUBIC) | ||||
|             # fmt: off | ||||
|             data = ("e1 e5 ee b9" | ||||
|                     "e5 e9 f3 bc" | ||||
|                     "ee f3 fd c1" | ||||
|                     "b9 bc c1 a2") | ||||
|             # fmt: on | ||||
|             for channel in case.split(): | ||||
|                 self.check_case(channel, self.make_sample(data, (8, 8))) | ||||
|     @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) | ||||
|     def test_enlarge_bicubic(self, mode): | ||||
|         case = self.make_case(mode, (4, 4), 0xE1) | ||||
|         case = case.resize((8, 8), Image.Resampling.BICUBIC) | ||||
|         # fmt: off | ||||
|         data = ("e1 e5 ee b9" | ||||
|                 "e5 e9 f3 bc" | ||||
|                 "ee f3 fd c1" | ||||
|                 "b9 bc c1 a2") | ||||
|         # fmt: on | ||||
|         for channel in case.split(): | ||||
|             self.check_case(channel, self.make_sample(data, (8, 8))) | ||||
| 
 | ||||
|     def test_enlarge_lanczos(self): | ||||
|         for mode in ["RGBX", "RGB", "La", "L"]: | ||||
|             case = self.make_case(mode, (6, 6), 0xE1) | ||||
|             case = case.resize((12, 12), Image.Resampling.LANCZOS) | ||||
|             data = ( | ||||
|                 "e1 e0 db ed f5 b8" | ||||
|                 "e0 df da ec f3 b7" | ||||
|                 "db db d6 e7 ee b5" | ||||
|                 "ed ec e6 fb ff bf" | ||||
|                 "f5 f4 ee ff ff c4" | ||||
|                 "b8 b7 b4 bf c4 a0" | ||||
|             ) | ||||
|             for channel in case.split(): | ||||
|                 self.check_case(channel, self.make_sample(data, (12, 12))) | ||||
|     @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) | ||||
|     def test_enlarge_lanczos(self, mode): | ||||
|         case = self.make_case(mode, (6, 6), 0xE1) | ||||
|         case = case.resize((12, 12), Image.Resampling.LANCZOS) | ||||
|         data = ( | ||||
|             "e1 e0 db ed f5 b8" | ||||
|             "e0 df da ec f3 b7" | ||||
|             "db db d6 e7 ee b5" | ||||
|             "ed ec e6 fb ff bf" | ||||
|             "f5 f4 ee ff ff c4" | ||||
|             "b8 b7 b4 bf c4 a0" | ||||
|         ) | ||||
|         for channel in case.split(): | ||||
|             self.check_case(channel, self.make_sample(data, (12, 12))) | ||||
| 
 | ||||
|     def test_box_filter_correct_range(self): | ||||
|         im = Image.new("RGB", (8, 8), "#1688ff").resize( | ||||
|  | @ -419,40 +420,43 @@ class TestCoreResampleCoefficients: | |||
| 
 | ||||
| 
 | ||||
| class TestCoreResampleBox: | ||||
|     def test_wrong_arguments(self): | ||||
|         im = hopper() | ||||
|         for resample in ( | ||||
|     @pytest.mark.parametrize( | ||||
|         "resample", | ||||
|         ( | ||||
|             Image.Resampling.NEAREST, | ||||
|             Image.Resampling.BOX, | ||||
|             Image.Resampling.BILINEAR, | ||||
|             Image.Resampling.HAMMING, | ||||
|             Image.Resampling.BICUBIC, | ||||
|             Image.Resampling.LANCZOS, | ||||
|         ): | ||||
|             im.resize((32, 32), resample, (0, 0, im.width, im.height)) | ||||
|             im.resize((32, 32), resample, (20, 20, im.width, im.height)) | ||||
|             im.resize((32, 32), resample, (20, 20, 20, 100)) | ||||
|             im.resize((32, 32), resample, (20, 20, 100, 20)) | ||||
|         ), | ||||
|     ) | ||||
|     def test_wrong_arguments(self, resample): | ||||
|         im = hopper() | ||||
|         im.resize((32, 32), resample, (0, 0, im.width, im.height)) | ||||
|         im.resize((32, 32), resample, (20, 20, im.width, im.height)) | ||||
|         im.resize((32, 32), resample, (20, 20, 20, 100)) | ||||
|         im.resize((32, 32), resample, (20, 20, 100, 20)) | ||||
| 
 | ||||
|             with pytest.raises(TypeError, match="must be sequence of length 4"): | ||||
|                 im.resize((32, 32), resample, (im.width, im.height)) | ||||
|         with pytest.raises(TypeError, match="must be sequence of length 4"): | ||||
|             im.resize((32, 32), resample, (im.width, im.height)) | ||||
| 
 | ||||
|             with pytest.raises(ValueError, match="can't be negative"): | ||||
|                 im.resize((32, 32), resample, (-20, 20, 100, 100)) | ||||
|             with pytest.raises(ValueError, match="can't be negative"): | ||||
|                 im.resize((32, 32), resample, (20, -20, 100, 100)) | ||||
|         with pytest.raises(ValueError, match="can't be negative"): | ||||
|             im.resize((32, 32), resample, (-20, 20, 100, 100)) | ||||
|         with pytest.raises(ValueError, match="can't be negative"): | ||||
|             im.resize((32, 32), resample, (20, -20, 100, 100)) | ||||
| 
 | ||||
|             with pytest.raises(ValueError, match="can't be empty"): | ||||
|                 im.resize((32, 32), resample, (20.1, 20, 20, 100)) | ||||
|             with pytest.raises(ValueError, match="can't be empty"): | ||||
|                 im.resize((32, 32), resample, (20, 20.1, 100, 20)) | ||||
|             with pytest.raises(ValueError, match="can't be empty"): | ||||
|                 im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) | ||||
|         with pytest.raises(ValueError, match="can't be empty"): | ||||
|             im.resize((32, 32), resample, (20.1, 20, 20, 100)) | ||||
|         with pytest.raises(ValueError, match="can't be empty"): | ||||
|             im.resize((32, 32), resample, (20, 20.1, 100, 20)) | ||||
|         with pytest.raises(ValueError, match="can't be empty"): | ||||
|             im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) | ||||
| 
 | ||||
|             with pytest.raises(ValueError, match="can't exceed"): | ||||
|                 im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) | ||||
|             with pytest.raises(ValueError, match="can't exceed"): | ||||
|                 im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) | ||||
|         with pytest.raises(ValueError, match="can't exceed"): | ||||
|             im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) | ||||
|         with pytest.raises(ValueError, match="can't exceed"): | ||||
|             im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) | ||||
| 
 | ||||
|     def resize_tiled(self, im, dst_size, xtiles, ytiles): | ||||
|         def split_range(size, tiles): | ||||
|  | @ -509,14 +513,16 @@ class TestCoreResampleBox: | |||
|         with pytest.raises(AssertionError, match=r"difference 29\."): | ||||
|             assert_image_similar(reference, without_box, 5) | ||||
| 
 | ||||
|     def test_formats(self): | ||||
|         for resample in [Image.Resampling.NEAREST, Image.Resampling.BILINEAR]: | ||||
|             for mode in ["RGB", "L", "RGBA", "LA", "I", ""]: | ||||
|                 im = hopper(mode) | ||||
|                 box = (20, 20, im.size[0] - 20, im.size[1] - 20) | ||||
|                 with_box = im.resize((32, 32), resample, box) | ||||
|                 cropped = im.crop(box).resize((32, 32), resample) | ||||
|                 assert_image_similar(cropped, with_box, 0.4) | ||||
|     @pytest.mark.parametrize("mode", ("RGB", "L", "RGBA", "LA", "I", "")) | ||||
|     @pytest.mark.parametrize( | ||||
|         "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) | ||||
|     ) | ||||
|     def test_formats(self, mode, resample): | ||||
|         im = hopper(mode) | ||||
|         box = (20, 20, im.size[0] - 20, im.size[1] - 20) | ||||
|         with_box = im.resize((32, 32), resample, box) | ||||
|         cropped = im.crop(box).resize((32, 32), resample) | ||||
|         assert_image_similar(cropped, with_box, 0.4) | ||||
| 
 | ||||
|     def test_passthrough(self): | ||||
|         # When no resize is required | ||||
|  |  | |||
|  | @ -22,24 +22,15 @@ class TestImagingCoreResize: | |||
|         im.load() | ||||
|         return im._new(im.im.resize(size, f)) | ||||
| 
 | ||||
|     def test_nearest_mode(self): | ||||
|         for mode in [ | ||||
|             "1", | ||||
|             "P", | ||||
|             "L", | ||||
|             "I", | ||||
|             "F", | ||||
|             "RGB", | ||||
|             "RGBA", | ||||
|             "CMYK", | ||||
|             "YCbCr", | ||||
|             "I;16", | ||||
|         ]:  # exotic mode | ||||
|             im = hopper(mode) | ||||
|             r = self.resize(im, (15, 12), Image.Resampling.NEAREST) | ||||
|             assert r.mode == mode | ||||
|             assert r.size == (15, 12) | ||||
|             assert r.im.bands == im.im.bands | ||||
|     @pytest.mark.parametrize( | ||||
|         "mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16") | ||||
|     ) | ||||
|     def test_nearest_mode(self, mode): | ||||
|         im = hopper(mode) | ||||
|         r = self.resize(im, (15, 12), Image.Resampling.NEAREST) | ||||
|         assert r.mode == mode | ||||
|         assert r.size == (15, 12) | ||||
|         assert r.im.bands == im.im.bands | ||||
| 
 | ||||
|     def test_convolution_modes(self): | ||||
|         with pytest.raises(ValueError): | ||||
|  | @ -55,33 +46,58 @@ class TestImagingCoreResize: | |||
|             assert r.size == (15, 12) | ||||
|             assert r.im.bands == im.im.bands | ||||
| 
 | ||||
|     def test_reduce_filters(self): | ||||
|         for f in [ | ||||
|     @pytest.mark.parametrize( | ||||
|         "resample", | ||||
|         ( | ||||
|             Image.Resampling.NEAREST, | ||||
|             Image.Resampling.BOX, | ||||
|             Image.Resampling.BILINEAR, | ||||
|             Image.Resampling.HAMMING, | ||||
|             Image.Resampling.BICUBIC, | ||||
|             Image.Resampling.LANCZOS, | ||||
|         ]: | ||||
|             r = self.resize(hopper("RGB"), (15, 12), f) | ||||
|             assert r.mode == "RGB" | ||||
|             assert r.size == (15, 12) | ||||
|         ), | ||||
|     ) | ||||
|     def test_reduce_filters(self, resample): | ||||
|         r = self.resize(hopper("RGB"), (15, 12), resample) | ||||
|         assert r.mode == "RGB" | ||||
|         assert r.size == (15, 12) | ||||
| 
 | ||||
|     def test_enlarge_filters(self): | ||||
|         for f in [ | ||||
|     @pytest.mark.parametrize( | ||||
|         "resample", | ||||
|         ( | ||||
|             Image.Resampling.NEAREST, | ||||
|             Image.Resampling.BOX, | ||||
|             Image.Resampling.BILINEAR, | ||||
|             Image.Resampling.HAMMING, | ||||
|             Image.Resampling.BICUBIC, | ||||
|             Image.Resampling.LANCZOS, | ||||
|         ]: | ||||
|             r = self.resize(hopper("RGB"), (212, 195), f) | ||||
|             assert r.mode == "RGB" | ||||
|             assert r.size == (212, 195) | ||||
|         ), | ||||
|     ) | ||||
|     def test_enlarge_filters(self, resample): | ||||
|         r = self.resize(hopper("RGB"), (212, 195), resample) | ||||
|         assert r.mode == "RGB" | ||||
|         assert r.size == (212, 195) | ||||
| 
 | ||||
|     def test_endianness(self): | ||||
|     @pytest.mark.parametrize( | ||||
|         "resample", | ||||
|         ( | ||||
|             Image.Resampling.NEAREST, | ||||
|             Image.Resampling.BOX, | ||||
|             Image.Resampling.BILINEAR, | ||||
|             Image.Resampling.HAMMING, | ||||
|             Image.Resampling.BICUBIC, | ||||
|             Image.Resampling.LANCZOS, | ||||
|         ), | ||||
|     ) | ||||
|     @pytest.mark.parametrize( | ||||
|         "mode, channels_set", | ||||
|         ( | ||||
|             ("RGB", ("blank", "filled", "dirty")), | ||||
|             ("RGBA", ("blank", "blank", "filled", "dirty")), | ||||
|             ("LA", ("filled", "dirty")), | ||||
|         ), | ||||
|     ) | ||||
|     def test_endianness(self, resample, mode, channels_set): | ||||
|         # Make an image with one colored pixel, in one channel. | ||||
|         # When resized, that channel should be the same as a GS image. | ||||
|         # Other channels should be unaffected. | ||||
|  | @ -95,47 +111,37 @@ class TestImagingCoreResize: | |||
|         } | ||||
|         samples["dirty"].putpixel((1, 1), 128) | ||||
| 
 | ||||
|         for f in [ | ||||
|         # samples resized with current filter | ||||
|         references = { | ||||
|             name: self.resize(ch, (4, 4), resample) for name, ch in samples.items() | ||||
|         } | ||||
| 
 | ||||
|         for channels in set(permutations(channels_set)): | ||||
|             # compile image from different channels permutations | ||||
|             im = Image.merge(mode, [samples[ch] for ch in channels]) | ||||
|             resized = self.resize(im, (4, 4), resample) | ||||
| 
 | ||||
|             for i, ch in enumerate(resized.split()): | ||||
|                 # check what resized channel in image is the same | ||||
|                 # as separately resized channel | ||||
|                 assert_image_equal(ch, references[channels[i]]) | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         "resample", | ||||
|         ( | ||||
|             Image.Resampling.NEAREST, | ||||
|             Image.Resampling.BOX, | ||||
|             Image.Resampling.BILINEAR, | ||||
|             Image.Resampling.HAMMING, | ||||
|             Image.Resampling.BICUBIC, | ||||
|             Image.Resampling.LANCZOS, | ||||
|         ]: | ||||
|             # samples resized with current filter | ||||
|             references = { | ||||
|                 name: self.resize(ch, (4, 4), f) for name, ch in samples.items() | ||||
|             } | ||||
| 
 | ||||
|             for mode, channels_set in [ | ||||
|                 ("RGB", ("blank", "filled", "dirty")), | ||||
|                 ("RGBA", ("blank", "blank", "filled", "dirty")), | ||||
|                 ("LA", ("filled", "dirty")), | ||||
|             ]: | ||||
|                 for channels in set(permutations(channels_set)): | ||||
|                     # compile image from different channels permutations | ||||
|                     im = Image.merge(mode, [samples[ch] for ch in channels]) | ||||
|                     resized = self.resize(im, (4, 4), f) | ||||
| 
 | ||||
|                     for i, ch in enumerate(resized.split()): | ||||
|                         # check what resized channel in image is the same | ||||
|                         # as separately resized channel | ||||
|                         assert_image_equal(ch, references[channels[i]]) | ||||
| 
 | ||||
|     def test_enlarge_zero(self): | ||||
|         for f in [ | ||||
|             Image.Resampling.NEAREST, | ||||
|             Image.Resampling.BOX, | ||||
|             Image.Resampling.BILINEAR, | ||||
|             Image.Resampling.HAMMING, | ||||
|             Image.Resampling.BICUBIC, | ||||
|             Image.Resampling.LANCZOS, | ||||
|         ]: | ||||
|             r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), f) | ||||
|             assert r.mode == "RGB" | ||||
|             assert r.size == (212, 195) | ||||
|             assert r.getdata()[0] == (0, 0, 0) | ||||
|         ), | ||||
|     ) | ||||
|     def test_enlarge_zero(self, resample): | ||||
|         r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample) | ||||
|         assert r.mode == "RGB" | ||||
|         assert r.size == (212, 195) | ||||
|         assert r.getdata()[0] == (0, 0, 0) | ||||
| 
 | ||||
|     def test_unknown_filter(self): | ||||
|         with pytest.raises(ValueError): | ||||
|  | @ -179,74 +185,71 @@ class TestReducingGapResize: | |||
|                 (52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99 | ||||
|             ) | ||||
| 
 | ||||
|     def test_reducing_gap_1(self, gradients_image): | ||||
|         for box, epsilon in [ | ||||
|             (None, 4), | ||||
|             ((1.1, 2.2, 510.8, 510.9), 4), | ||||
|             ((3, 10, 410, 256), 10), | ||||
|         ]: | ||||
|             ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) | ||||
|             im = gradients_image.resize( | ||||
|                 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 | ||||
|             ) | ||||
| 
 | ||||
|             with pytest.raises(AssertionError): | ||||
|                 assert_image_equal(ref, im) | ||||
| 
 | ||||
|             assert_image_similar(ref, im, epsilon) | ||||
| 
 | ||||
|     def test_reducing_gap_2(self, gradients_image): | ||||
|         for box, epsilon in [ | ||||
|             (None, 1.5), | ||||
|             ((1.1, 2.2, 510.8, 510.9), 1.5), | ||||
|             ((3, 10, 410, 256), 1), | ||||
|         ]: | ||||
|             ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) | ||||
|             im = gradients_image.resize( | ||||
|                 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 | ||||
|             ) | ||||
| 
 | ||||
|             with pytest.raises(AssertionError): | ||||
|                 assert_image_equal(ref, im) | ||||
| 
 | ||||
|             assert_image_similar(ref, im, epsilon) | ||||
| 
 | ||||
|     def test_reducing_gap_3(self, gradients_image): | ||||
|         for box, epsilon in [ | ||||
|             (None, 1), | ||||
|             ((1.1, 2.2, 510.8, 510.9), 1), | ||||
|             ((3, 10, 410, 256), 0.5), | ||||
|         ]: | ||||
|             ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) | ||||
|             im = gradients_image.resize( | ||||
|                 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 | ||||
|             ) | ||||
| 
 | ||||
|             with pytest.raises(AssertionError): | ||||
|                 assert_image_equal(ref, im) | ||||
| 
 | ||||
|             assert_image_similar(ref, im, epsilon) | ||||
| 
 | ||||
|     def test_reducing_gap_8(self, gradients_image): | ||||
|         for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]: | ||||
|             ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) | ||||
|             im = gradients_image.resize( | ||||
|                 (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 | ||||
|             ) | ||||
|     @pytest.mark.parametrize( | ||||
|         "box, epsilon", | ||||
|         ((None, 4), ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10)), | ||||
|     ) | ||||
|     def test_reducing_gap_1(self, gradients_image, box, epsilon): | ||||
|         ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) | ||||
|         im = gradients_image.resize( | ||||
|             (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 | ||||
|         ) | ||||
| 
 | ||||
|         with pytest.raises(AssertionError): | ||||
|             assert_image_equal(ref, im) | ||||
| 
 | ||||
|     def test_box_filter(self, gradients_image): | ||||
|         for box, epsilon in [ | ||||
|             ((0, 0, 512, 512), 5.5), | ||||
|             ((0.9, 1.7, 128, 128), 9.5), | ||||
|         ]: | ||||
|             ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) | ||||
|             im = gradients_image.resize( | ||||
|                 (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 | ||||
|             ) | ||||
|         assert_image_similar(ref, im, epsilon) | ||||
| 
 | ||||
|             assert_image_similar(ref, im, epsilon) | ||||
|     @pytest.mark.parametrize( | ||||
|         "box, epsilon", | ||||
|         ((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)), | ||||
|     ) | ||||
|     def test_reducing_gap_2(self, gradients_image, box, epsilon): | ||||
|         ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) | ||||
|         im = gradients_image.resize( | ||||
|             (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 | ||||
|         ) | ||||
| 
 | ||||
|         with pytest.raises(AssertionError): | ||||
|             assert_image_equal(ref, im) | ||||
| 
 | ||||
|         assert_image_similar(ref, im, epsilon) | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         "box, epsilon", | ||||
|         ((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)), | ||||
|     ) | ||||
|     def test_reducing_gap_3(self, gradients_image, box, epsilon): | ||||
|         ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) | ||||
|         im = gradients_image.resize( | ||||
|             (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 | ||||
|         ) | ||||
| 
 | ||||
|         with pytest.raises(AssertionError): | ||||
|             assert_image_equal(ref, im) | ||||
| 
 | ||||
|         assert_image_similar(ref, im, epsilon) | ||||
| 
 | ||||
|     @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) | ||||
|     def test_reducing_gap_8(self, gradients_image, box): | ||||
|         ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) | ||||
|         im = gradients_image.resize( | ||||
|             (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 | ||||
|         ) | ||||
| 
 | ||||
|         assert_image_equal(ref, im) | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         "box, epsilon", | ||||
|         (((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)), | ||||
|     ) | ||||
|     def test_box_filter(self, gradients_image, box, epsilon): | ||||
|         ref = gradients_image.resize((52, 34), Image.Resampling.BOX, box=box) | ||||
|         im = gradients_image.resize( | ||||
|             (52, 34), Image.Resampling.BOX, box=box, reducing_gap=1.0 | ||||
|         ) | ||||
| 
 | ||||
|         assert_image_similar(ref, im, epsilon) | ||||
| 
 | ||||
| 
 | ||||
| class TestImageResize: | ||||
|  | @ -273,15 +276,14 @@ class TestImageResize: | |||
|             im = im.resize((64, 64)) | ||||
|             assert im.size == (64, 64) | ||||
| 
 | ||||
|     def test_default_filter(self): | ||||
|         for mode in "L", "RGB", "I", "F": | ||||
|             im = hopper(mode) | ||||
|             assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) | ||||
|     @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F")) | ||||
|     def test_default_filter_bicubic(self, mode): | ||||
|         im = hopper(mode) | ||||
|         assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) | ||||
| 
 | ||||
|         for mode in "1", "P": | ||||
|             im = hopper(mode) | ||||
|             assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) | ||||
| 
 | ||||
|         for mode in "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16": | ||||
|             im = hopper(mode) | ||||
|             assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) | ||||
|     @pytest.mark.parametrize( | ||||
|         "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16") | ||||
|     ) | ||||
|     def test_default_filter_nearest(self, mode): | ||||
|         im = hopper(mode) | ||||
|         assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) | ||||
|  |  | |||
|  | @ -1,3 +1,5 @@ | |||
| import pytest | ||||
| 
 | ||||
| from PIL import Image | ||||
| 
 | ||||
| from .helper import ( | ||||
|  | @ -22,26 +24,26 @@ def rotate(im, mode, angle, center=None, translate=None): | |||
|         assert out.size != im.size | ||||
| 
 | ||||
| 
 | ||||
| def test_mode(): | ||||
|     for mode in ("1", "P", "L", "RGB", "I", "F"): | ||||
|         im = hopper(mode) | ||||
|         rotate(im, mode, 45) | ||||
| @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) | ||||
| def test_mode(mode): | ||||
|     im = hopper(mode) | ||||
|     rotate(im, mode, 45) | ||||
| 
 | ||||
| 
 | ||||
| def test_angle(): | ||||
|     for angle in (0, 90, 180, 270): | ||||
|         with Image.open("Tests/images/test-card.png") as im: | ||||
|             rotate(im, im.mode, angle) | ||||
| 
 | ||||
|         im = hopper() | ||||
|         assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) | ||||
| 
 | ||||
| 
 | ||||
| def test_zero(): | ||||
|     for angle in (0, 45, 90, 180, 270): | ||||
|         im = Image.new("RGB", (0, 0)) | ||||
| @pytest.mark.parametrize("angle", (0, 90, 180, 270)) | ||||
| def test_angle(angle): | ||||
|     with Image.open("Tests/images/test-card.png") as im: | ||||
|         rotate(im, im.mode, angle) | ||||
| 
 | ||||
|     im = hopper() | ||||
|     assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) | ||||
| def test_zero(angle): | ||||
|     im = Image.new("RGB", (0, 0)) | ||||
|     rotate(im, im.mode, angle) | ||||
| 
 | ||||
| 
 | ||||
| def test_resample(): | ||||
|     # Target image creation, inspected by eye. | ||||
|  |  | |||
|  | @ -1,3 +1,5 @@ | |||
| import pytest | ||||
| 
 | ||||
| from PIL.Image import Transpose | ||||
| 
 | ||||
| from . import helper | ||||
|  | @ -9,157 +11,136 @@ HOPPER = { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| def test_flip_left_right(): | ||||
|     def transpose(mode): | ||||
|         im = HOPPER[mode] | ||||
|         out = im.transpose(Transpose.FLIP_LEFT_RIGHT) | ||||
|         assert out.mode == mode | ||||
|         assert out.size == im.size | ||||
| @pytest.mark.parametrize("mode", HOPPER) | ||||
| def test_flip_left_right(mode): | ||||
|     im = HOPPER[mode] | ||||
|     out = im.transpose(Transpose.FLIP_LEFT_RIGHT) | ||||
|     assert out.mode == mode | ||||
|     assert out.size == im.size | ||||
| 
 | ||||
|         x, y = im.size | ||||
|         assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1)) | ||||
|         assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) | ||||
|         assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2)) | ||||
|         assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2)) | ||||
| 
 | ||||
|     for mode in HOPPER: | ||||
|         transpose(mode) | ||||
|     x, y = im.size | ||||
|     assert im.getpixel((1, 1)) == out.getpixel((x - 2, 1)) | ||||
|     assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) | ||||
|     assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, y - 2)) | ||||
|     assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, y - 2)) | ||||
| 
 | ||||
| 
 | ||||
| def test_flip_top_bottom(): | ||||
|     def transpose(mode): | ||||
|         im = HOPPER[mode] | ||||
|         out = im.transpose(Transpose.FLIP_TOP_BOTTOM) | ||||
|         assert out.mode == mode | ||||
|         assert out.size == im.size | ||||
| @pytest.mark.parametrize("mode", HOPPER) | ||||
| def test_flip_top_bottom(mode): | ||||
|     im = HOPPER[mode] | ||||
|     out = im.transpose(Transpose.FLIP_TOP_BOTTOM) | ||||
|     assert out.mode == mode | ||||
|     assert out.size == im.size | ||||
| 
 | ||||
|         x, y = im.size | ||||
|         assert im.getpixel((1, 1)) == out.getpixel((1, y - 2)) | ||||
|         assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2)) | ||||
|         assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) | ||||
|         assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1)) | ||||
| 
 | ||||
|     for mode in HOPPER: | ||||
|         transpose(mode) | ||||
|     x, y = im.size | ||||
|     assert im.getpixel((1, 1)) == out.getpixel((1, y - 2)) | ||||
|     assert im.getpixel((x - 2, 1)) == out.getpixel((x - 2, y - 2)) | ||||
|     assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) | ||||
|     assert im.getpixel((x - 2, y - 2)) == out.getpixel((x - 2, 1)) | ||||
| 
 | ||||
| 
 | ||||
| def test_rotate_90(): | ||||
|     def transpose(mode): | ||||
|         im = HOPPER[mode] | ||||
|         out = im.transpose(Transpose.ROTATE_90) | ||||
|         assert out.mode == mode | ||||
|         assert out.size == im.size[::-1] | ||||
| @pytest.mark.parametrize("mode", HOPPER) | ||||
| def test_rotate_90(mode): | ||||
|     im = HOPPER[mode] | ||||
|     out = im.transpose(Transpose.ROTATE_90) | ||||
|     assert out.mode == mode | ||||
|     assert out.size == im.size[::-1] | ||||
| 
 | ||||
|         x, y = im.size | ||||
|         assert im.getpixel((1, 1)) == out.getpixel((1, x - 2)) | ||||
|         assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) | ||||
|         assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2)) | ||||
|         assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1)) | ||||
| 
 | ||||
|     for mode in HOPPER: | ||||
|         transpose(mode) | ||||
|     x, y = im.size | ||||
|     assert im.getpixel((1, 1)) == out.getpixel((1, x - 2)) | ||||
|     assert im.getpixel((x - 2, 1)) == out.getpixel((1, 1)) | ||||
|     assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, x - 2)) | ||||
|     assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, 1)) | ||||
| 
 | ||||
| 
 | ||||
| def test_rotate_180(): | ||||
|     def transpose(mode): | ||||
|         im = HOPPER[mode] | ||||
|         out = im.transpose(Transpose.ROTATE_180) | ||||
|         assert out.mode == mode | ||||
|         assert out.size == im.size | ||||
| @pytest.mark.parametrize("mode", HOPPER) | ||||
| def test_rotate_180(mode): | ||||
|     im = HOPPER[mode] | ||||
|     out = im.transpose(Transpose.ROTATE_180) | ||||
|     assert out.mode == mode | ||||
|     assert out.size == im.size | ||||
| 
 | ||||
|         x, y = im.size | ||||
|         assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2)) | ||||
|         assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2)) | ||||
|         assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1)) | ||||
|         assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) | ||||
| 
 | ||||
|     for mode in HOPPER: | ||||
|         transpose(mode) | ||||
|     x, y = im.size | ||||
|     assert im.getpixel((1, 1)) == out.getpixel((x - 2, y - 2)) | ||||
|     assert im.getpixel((x - 2, 1)) == out.getpixel((1, y - 2)) | ||||
|     assert im.getpixel((1, y - 2)) == out.getpixel((x - 2, 1)) | ||||
|     assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) | ||||
| 
 | ||||
| 
 | ||||
| def test_rotate_270(): | ||||
|     def transpose(mode): | ||||
|         im = HOPPER[mode] | ||||
|         out = im.transpose(Transpose.ROTATE_270) | ||||
|         assert out.mode == mode | ||||
|         assert out.size == im.size[::-1] | ||||
| @pytest.mark.parametrize("mode", HOPPER) | ||||
| def test_rotate_270(mode): | ||||
|     im = HOPPER[mode] | ||||
|     out = im.transpose(Transpose.ROTATE_270) | ||||
|     assert out.mode == mode | ||||
|     assert out.size == im.size[::-1] | ||||
| 
 | ||||
|         x, y = im.size | ||||
|         assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1)) | ||||
|         assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2)) | ||||
|         assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) | ||||
|         assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2)) | ||||
| 
 | ||||
|     for mode in HOPPER: | ||||
|         transpose(mode) | ||||
|     x, y = im.size | ||||
|     assert im.getpixel((1, 1)) == out.getpixel((y - 2, 1)) | ||||
|     assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, x - 2)) | ||||
|     assert im.getpixel((1, y - 2)) == out.getpixel((1, 1)) | ||||
|     assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, x - 2)) | ||||
| 
 | ||||
| 
 | ||||
| def test_transpose(): | ||||
|     def transpose(mode): | ||||
|         im = HOPPER[mode] | ||||
|         out = im.transpose(Transpose.TRANSPOSE) | ||||
|         assert out.mode == mode | ||||
|         assert out.size == im.size[::-1] | ||||
| @pytest.mark.parametrize("mode", HOPPER) | ||||
| def test_transpose(mode): | ||||
|     im = HOPPER[mode] | ||||
|     out = im.transpose(Transpose.TRANSPOSE) | ||||
|     assert out.mode == mode | ||||
|     assert out.size == im.size[::-1] | ||||
| 
 | ||||
|         x, y = im.size | ||||
|         assert im.getpixel((1, 1)) == out.getpixel((1, 1)) | ||||
|         assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2)) | ||||
|         assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1)) | ||||
|         assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2)) | ||||
| 
 | ||||
|     for mode in HOPPER: | ||||
|         transpose(mode) | ||||
|     x, y = im.size | ||||
|     assert im.getpixel((1, 1)) == out.getpixel((1, 1)) | ||||
|     assert im.getpixel((x - 2, 1)) == out.getpixel((1, x - 2)) | ||||
|     assert im.getpixel((1, y - 2)) == out.getpixel((y - 2, 1)) | ||||
|     assert im.getpixel((x - 2, y - 2)) == out.getpixel((y - 2, x - 2)) | ||||
| 
 | ||||
| 
 | ||||
| def test_tranverse(): | ||||
|     def transpose(mode): | ||||
|         im = HOPPER[mode] | ||||
|         out = im.transpose(Transpose.TRANSVERSE) | ||||
|         assert out.mode == mode | ||||
|         assert out.size == im.size[::-1] | ||||
| @pytest.mark.parametrize("mode", HOPPER) | ||||
| def test_tranverse(mode): | ||||
|     im = HOPPER[mode] | ||||
|     out = im.transpose(Transpose.TRANSVERSE) | ||||
|     assert out.mode == mode | ||||
|     assert out.size == im.size[::-1] | ||||
| 
 | ||||
|         x, y = im.size | ||||
|         assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2)) | ||||
|         assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1)) | ||||
|         assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2)) | ||||
|         assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) | ||||
| 
 | ||||
|     for mode in HOPPER: | ||||
|         transpose(mode) | ||||
|     x, y = im.size | ||||
|     assert im.getpixel((1, 1)) == out.getpixel((y - 2, x - 2)) | ||||
|     assert im.getpixel((x - 2, 1)) == out.getpixel((y - 2, 1)) | ||||
|     assert im.getpixel((1, y - 2)) == out.getpixel((1, x - 2)) | ||||
|     assert im.getpixel((x - 2, y - 2)) == out.getpixel((1, 1)) | ||||
| 
 | ||||
| 
 | ||||
| def test_roundtrip(): | ||||
|     for mode in HOPPER: | ||||
|         im = HOPPER[mode] | ||||
| @pytest.mark.parametrize("mode", HOPPER) | ||||
| def test_roundtrip(mode): | ||||
|     im = HOPPER[mode] | ||||
| 
 | ||||
|         def transpose(first, second): | ||||
|             return im.transpose(first).transpose(second) | ||||
|     def transpose(first, second): | ||||
|         return im.transpose(first).transpose(second) | ||||
| 
 | ||||
|         assert_image_equal( | ||||
|             im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT) | ||||
|         ) | ||||
|         assert_image_equal( | ||||
|             im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM) | ||||
|         ) | ||||
|         assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270)) | ||||
|         assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180)) | ||||
|         assert_image_equal( | ||||
|             im.transpose(Transpose.TRANSPOSE), | ||||
|             transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM), | ||||
|         ) | ||||
|         assert_image_equal( | ||||
|             im.transpose(Transpose.TRANSPOSE), | ||||
|             transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT), | ||||
|         ) | ||||
|         assert_image_equal( | ||||
|             im.transpose(Transpose.TRANSVERSE), | ||||
|             transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT), | ||||
|         ) | ||||
|         assert_image_equal( | ||||
|             im.transpose(Transpose.TRANSVERSE), | ||||
|             transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM), | ||||
|         ) | ||||
|         assert_image_equal( | ||||
|             im.transpose(Transpose.TRANSVERSE), | ||||
|             transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE), | ||||
|         ) | ||||
|     assert_image_equal( | ||||
|         im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT) | ||||
|     ) | ||||
|     assert_image_equal( | ||||
|         im, transpose(Transpose.FLIP_TOP_BOTTOM, Transpose.FLIP_TOP_BOTTOM) | ||||
|     ) | ||||
|     assert_image_equal(im, transpose(Transpose.ROTATE_90, Transpose.ROTATE_270)) | ||||
|     assert_image_equal(im, transpose(Transpose.ROTATE_180, Transpose.ROTATE_180)) | ||||
|     assert_image_equal( | ||||
|         im.transpose(Transpose.TRANSPOSE), | ||||
|         transpose(Transpose.ROTATE_90, Transpose.FLIP_TOP_BOTTOM), | ||||
|     ) | ||||
|     assert_image_equal( | ||||
|         im.transpose(Transpose.TRANSPOSE), | ||||
|         transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT), | ||||
|     ) | ||||
|     assert_image_equal( | ||||
|         im.transpose(Transpose.TRANSVERSE), | ||||
|         transpose(Transpose.ROTATE_90, Transpose.FLIP_LEFT_RIGHT), | ||||
|     ) | ||||
|     assert_image_equal( | ||||
|         im.transpose(Transpose.TRANSVERSE), | ||||
|         transpose(Transpose.ROTATE_270, Transpose.FLIP_TOP_BOTTOM), | ||||
|     ) | ||||
|     assert_image_equal( | ||||
|         im.transpose(Transpose.TRANSVERSE), | ||||
|         transpose(Transpose.ROTATE_180, Transpose.TRANSPOSE), | ||||
|     ) | ||||
|  |  | |||
|  | @ -174,19 +174,24 @@ def test_exceptions(): | |||
|     psRGB = ImageCms.createProfile("sRGB") | ||||
|     pLab = ImageCms.createProfile("LAB") | ||||
|     t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") | ||||
|     with pytest.raises(ValueError): | ||||
|     with pytest.raises(ValueError, match="mode mismatch"): | ||||
|         t.apply_in_place(hopper("RGBA")) | ||||
| 
 | ||||
|     # the procedural pyCMS API uses PyCMSError for all sorts of errors | ||||
|     with hopper() as im: | ||||
|         with pytest.raises(ImageCms.PyCMSError): | ||||
|         with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"): | ||||
|             ImageCms.profileToProfile(im, "foo", "bar") | ||||
|     with pytest.raises(ImageCms.PyCMSError): | ||||
| 
 | ||||
|     with pytest.raises(ImageCms.PyCMSError, match="cannot open profile file"): | ||||
|         ImageCms.buildTransform("foo", "bar", "RGB", "RGB") | ||||
|     with pytest.raises(ImageCms.PyCMSError): | ||||
| 
 | ||||
|     with pytest.raises(ImageCms.PyCMSError, match="Invalid type for Profile"): | ||||
|         ImageCms.getProfileName(None) | ||||
|     skip_missing() | ||||
|     with pytest.raises(ImageCms.PyCMSError): | ||||
| 
 | ||||
|     # Python <= 3.9: "an integer is required (got type NoneType)" | ||||
|     # Python > 3.9: "'NoneType' object cannot be interpreted as an integer" | ||||
|     with pytest.raises(ImageCms.PyCMSError, match="integer"): | ||||
|         ImageCms.isIntentSupported(SRGB, None, None) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -201,15 +206,32 @@ def test_lab_color_profile(): | |||
| 
 | ||||
| 
 | ||||
| def test_unsupported_color_space(): | ||||
|     with pytest.raises(ImageCms.PyCMSError): | ||||
|     with pytest.raises( | ||||
|         ImageCms.PyCMSError, | ||||
|         match=re.escape( | ||||
|             "Color space not supported for on-the-fly profile creation (unsupported)" | ||||
|         ), | ||||
|     ): | ||||
|         ImageCms.createProfile("unsupported") | ||||
| 
 | ||||
| 
 | ||||
| def test_invalid_color_temperature(): | ||||
|     with pytest.raises(ImageCms.PyCMSError): | ||||
|     with pytest.raises( | ||||
|         ImageCms.PyCMSError, | ||||
|         match='Color temperature must be numeric, "invalid" not valid', | ||||
|     ): | ||||
|         ImageCms.createProfile("LAB", "invalid") | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("flag", ("my string", -1)) | ||||
| def test_invalid_flag(flag): | ||||
|     with hopper() as im: | ||||
|         with pytest.raises( | ||||
|             ImageCms.PyCMSError, match="flags must be an integer between 0 and " | ||||
|         ): | ||||
|             ImageCms.profileToProfile(im, "foo", "bar", flags=flag) | ||||
| 
 | ||||
| 
 | ||||
| def test_simple_lab(): | ||||
|     i = Image.new("RGB", (10, 10), (128, 128, 128)) | ||||
| 
 | ||||
|  | @ -461,9 +483,9 @@ def test_profile_typesafety(): | |||
|     prepatch, these would segfault, postpatch they should emit a typeerror | ||||
|     """ | ||||
| 
 | ||||
|     with pytest.raises(TypeError): | ||||
|     with pytest.raises(TypeError, match="Invalid type for Profile"): | ||||
|         ImageCms.ImageCmsProfile(0).tobytes() | ||||
|     with pytest.raises(TypeError): | ||||
|     with pytest.raises(TypeError, match="Invalid type for Profile"): | ||||
|         ImageCms.ImageCmsProfile(1).tobytes() | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -625,20 +625,20 @@ def test_polygon2(): | |||
|     helper_polygon(POINTS2) | ||||
| 
 | ||||
| 
 | ||||
| def test_polygon_kite(): | ||||
| @pytest.mark.parametrize("mode", ("RGB", "L")) | ||||
| def test_polygon_kite(mode): | ||||
|     # Test drawing lines of different gradients (dx>dy, dy>dx) and | ||||
|     # vertical (dx==0) and horizontal (dy==0) lines | ||||
|     for mode in ["RGB", "L"]: | ||||
|         # Arrange | ||||
|         im = Image.new(mode, (W, H)) | ||||
|         draw = ImageDraw.Draw(im) | ||||
|         expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" | ||||
|     # Arrange | ||||
|     im = Image.new(mode, (W, H)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" | ||||
| 
 | ||||
|         # Act | ||||
|         draw.polygon(KITE_POINTS, fill="blue", outline="yellow") | ||||
|     # Act | ||||
|     draw.polygon(KITE_POINTS, fill="blue", outline="yellow") | ||||
| 
 | ||||
|         # Assert | ||||
|         assert_image_equal_tofile(im, expected) | ||||
|     # Assert | ||||
|     assert_image_equal_tofile(im, expected) | ||||
| 
 | ||||
| 
 | ||||
| def test_polygon_1px_high(): | ||||
|  | @ -655,6 +655,20 @@ def test_polygon_1px_high(): | |||
|     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(): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|  | @ -1218,21 +1232,39 @@ def test_textsize_empty_string(): | |||
|     # Act | ||||
|     # Should not cause 'SystemError: <built-in method getsize of | ||||
|     # ImagingFont object at 0x...> returned NULL without setting an error' | ||||
|     draw.textsize("") | ||||
|     draw.textsize("\n") | ||||
|     draw.textsize("test\n") | ||||
|     draw.textbbox((0, 0), "") | ||||
|     draw.textbbox((0, 0), "\n") | ||||
|     draw.textbbox((0, 0), "test\n") | ||||
|     draw.textlength("") | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("freetype2") | ||||
| def test_textsize_stroke(): | ||||
| def test_textbbox_stroke(): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) | ||||
| 
 | ||||
|     # Act / Assert | ||||
|     assert draw.textsize("A", font, stroke_width=2) == (16, 20) | ||||
|     assert draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2) == (52, 44) | ||||
|     assert draw.textbbox((2, 2), "A", font, stroke_width=2) == (0, 4, 16, 20) | ||||
|     assert draw.textbbox((2, 2), "A", font, stroke_width=4) == (-2, 2, 18, 22) | ||||
|     assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=2) == (0, 4, 52, 44) | ||||
|     assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 50) | ||||
| 
 | ||||
| 
 | ||||
| def test_textsize_deprecation(): | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|     with pytest.warns(DeprecationWarning) as log: | ||||
|         draw.textsize("Hello") | ||||
|     assert len(log) == 1 | ||||
|     with pytest.warns(DeprecationWarning) as log: | ||||
|         draw.textsize("Hello\nWorld") | ||||
|     assert len(log) == 1 | ||||
|     with pytest.warns(DeprecationWarning) as log: | ||||
|         draw.multiline_textsize("Hello\nWorld") | ||||
|     assert len(log) == 1 | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("freetype2") | ||||
|  | @ -1282,6 +1314,23 @@ def test_stroke_multiline(): | |||
|     assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3) | ||||
| 
 | ||||
| 
 | ||||
| def test_setting_default_font(): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (100, 250)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120) | ||||
| 
 | ||||
|     # Act | ||||
|     ImageDraw.ImageDraw.font = font | ||||
| 
 | ||||
|     # Assert | ||||
|     try: | ||||
|         assert draw.getfont() == font | ||||
|     finally: | ||||
|         ImageDraw.ImageDraw.font = None | ||||
|         assert isinstance(draw.getfont(), ImageFont.ImageFont) | ||||
| 
 | ||||
| 
 | ||||
| def test_same_color_outline(): | ||||
|     # Prepare shape | ||||
|     x0, y0 = 5, 5 | ||||
|  | @ -1452,3 +1501,11 @@ def test_discontiguous_corners_polygon(): | |||
|     ) | ||||
|     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) | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| import os.path | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, ImageDraw, ImageDraw2 | ||||
| 
 | ||||
| from .helper import ( | ||||
|  | @ -205,7 +207,9 @@ def test_textsize(): | |||
|     font = ImageDraw2.Font("white", FONT_PATH) | ||||
| 
 | ||||
|     # Act | ||||
|     size = draw.textsize("ImageDraw2", font) | ||||
|     with pytest.warns(DeprecationWarning) as log: | ||||
|         size = draw.textsize("ImageDraw2", font) | ||||
|     assert len(log) == 1 | ||||
| 
 | ||||
|     # Assert | ||||
|     assert size[1] == 12 | ||||
|  | @ -221,9 +225,10 @@ def test_textsize_empty_string(): | |||
|     # Act | ||||
|     # Should not cause 'SystemError: <built-in method getsize of | ||||
|     # ImagingFont object at 0x...> returned NULL without setting an error' | ||||
|     draw.textsize("", font) | ||||
|     draw.textsize("\n", font) | ||||
|     draw.textsize("test\n", font) | ||||
|     draw.textbbox((0, 0), "", font) | ||||
|     draw.textbbox((0, 0), "\n", font) | ||||
|     draw.textbbox((0, 0), "test\n", font) | ||||
|     draw.textlength("", font) | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("freetype2") | ||||
|  |  | |||
|  | @ -65,9 +65,12 @@ class TestImageFont: | |||
|         return font_bytes | ||||
| 
 | ||||
|     def test_font_with_filelike(self): | ||||
|         ImageFont.truetype( | ||||
|         ttf = ImageFont.truetype( | ||||
|             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()) | ||||
|         # Usage note:  making two fonts from the same buffer fails. | ||||
|         # shared_bytes = self._font_as_bytes() | ||||
|  | @ -91,7 +94,7 @@ class TestImageFont: | |||
|     def _render(self, font): | ||||
|         txt = "Hello World!" | ||||
|         ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE) | ||||
|         ttf.getsize(txt) | ||||
|         ttf.getbbox(txt) | ||||
| 
 | ||||
|         img = Image.new("RGB", (256, 64), "white") | ||||
|         d = ImageDraw.Draw(img) | ||||
|  | @ -132,15 +135,15 @@ class TestImageFont: | |||
|         target = "Tests/images/transparent_background_text_L.png" | ||||
|         assert_image_similar_tofile(im.convert("L"), target, 0.01) | ||||
| 
 | ||||
|     def test_textsize_equal(self): | ||||
|     def test_textbbox_equal(self): | ||||
|         im = Image.new(mode="RGB", size=(300, 100)) | ||||
|         draw = ImageDraw.Draw(im) | ||||
|         ttf = self.get_font() | ||||
| 
 | ||||
|         txt = "Hello World!" | ||||
|         size = draw.textsize(txt, ttf) | ||||
|         bbox = draw.textbbox((10, 10), txt, ttf) | ||||
|         draw.text((10, 10), txt, font=ttf) | ||||
|         draw.rectangle((10, 10, 10 + size[0], 10 + size[1])) | ||||
|         draw.rectangle(bbox) | ||||
| 
 | ||||
|         assert_image_similar_tofile( | ||||
|             im, "Tests/images/rectangle_surrounding_text.png", 2.5 | ||||
|  | @ -181,7 +184,7 @@ class TestImageFont: | |||
|         im = Image.new(mode="RGB", size=(300, 100)) | ||||
|         draw = ImageDraw.Draw(im) | ||||
|         ttf = self.get_font() | ||||
|         line_spacing = draw.textsize("A", font=ttf)[1] + 4 | ||||
|         line_spacing = ttf.getbbox("A")[3] + 4 | ||||
|         lines = TEST_TEXT.split("\n") | ||||
|         y = 0 | ||||
|         for line in lines: | ||||
|  | @ -242,19 +245,39 @@ class TestImageFont: | |||
|         im = Image.new(mode="RGB", size=(300, 100)) | ||||
|         draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|         # Test that textsize() correctly connects to multiline_textsize() | ||||
|         assert draw.textsize(TEST_TEXT, font=ttf) == draw.multiline_textsize( | ||||
|             TEST_TEXT, font=ttf | ||||
|         with pytest.warns(DeprecationWarning) as log: | ||||
|             # Test that textsize() correctly connects to multiline_textsize() | ||||
|             assert draw.textsize(TEST_TEXT, font=ttf) == draw.multiline_textsize( | ||||
|                 TEST_TEXT, font=ttf | ||||
|             ) | ||||
| 
 | ||||
|             # Test that multiline_textsize corresponds to ImageFont.textsize() | ||||
|             # for single line text | ||||
|             assert ttf.getsize("A") == draw.multiline_textsize("A", font=ttf) | ||||
| 
 | ||||
|             # Test that textsize() can pass on additional arguments | ||||
|             # to multiline_textsize() | ||||
|             draw.textsize(TEST_TEXT, font=ttf, spacing=4) | ||||
|             draw.textsize(TEST_TEXT, ttf, 4) | ||||
|         assert len(log) == 6 | ||||
| 
 | ||||
|     def test_multiline_bbox(self): | ||||
|         ttf = self.get_font() | ||||
|         im = Image.new(mode="RGB", size=(300, 100)) | ||||
|         draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|         # Test that textbbox() correctly connects to multiline_textbbox() | ||||
|         assert draw.textbbox((0, 0), TEST_TEXT, font=ttf) == draw.multiline_textbbox( | ||||
|             (0, 0), TEST_TEXT, font=ttf | ||||
|         ) | ||||
| 
 | ||||
|         # Test that multiline_textsize corresponds to ImageFont.textsize() | ||||
|         # Test that multiline_textbbox corresponds to ImageFont.textbbox() | ||||
|         # for single line text | ||||
|         assert ttf.getsize("A") == draw.multiline_textsize("A", font=ttf) | ||||
|         assert ttf.getbbox("A") == draw.multiline_textbbox((0, 0), "A", font=ttf) | ||||
| 
 | ||||
|         # Test that textsize() can pass on additional arguments | ||||
|         # to multiline_textsize() | ||||
|         draw.textsize(TEST_TEXT, font=ttf, spacing=4) | ||||
|         draw.textsize(TEST_TEXT, ttf, 4) | ||||
|         # Test that textbbox() can pass on additional arguments | ||||
|         # to multiline_textbbox() | ||||
|         draw.textbbox((0, 0), TEST_TEXT, font=ttf, spacing=4) | ||||
| 
 | ||||
|     def test_multiline_width(self): | ||||
|         ttf = self.get_font() | ||||
|  | @ -262,9 +285,15 @@ class TestImageFont: | |||
|         draw = ImageDraw.Draw(im) | ||||
| 
 | ||||
|         assert ( | ||||
|             draw.textsize("longest line", font=ttf)[0] | ||||
|             == draw.multiline_textsize("longest line\nline", font=ttf)[0] | ||||
|             draw.textbbox((0, 0), "longest line", font=ttf)[2] | ||||
|             == draw.multiline_textbbox((0, 0), "longest line\nline", font=ttf)[2] | ||||
|         ) | ||||
|         with pytest.warns(DeprecationWarning) as log: | ||||
|             assert ( | ||||
|                 draw.textsize("longest line", font=ttf)[0] | ||||
|                 == draw.multiline_textsize("longest line\nline", font=ttf)[0] | ||||
|             ) | ||||
|         assert len(log) == 2 | ||||
| 
 | ||||
|     def test_multiline_spacing(self): | ||||
|         ttf = self.get_font() | ||||
|  | @ -286,16 +315,33 @@ class TestImageFont: | |||
| 
 | ||||
|         # Original font | ||||
|         draw.font = font | ||||
|         box_size_a = draw.textsize(word) | ||||
|         with pytest.warns(DeprecationWarning) as log: | ||||
|             box_size_a = draw.textsize(word) | ||||
|             assert box_size_a == font.getsize(word) | ||||
|         assert len(log) == 2 | ||||
|         bbox_a = draw.textbbox((10, 10), word) | ||||
| 
 | ||||
|         # Rotated font | ||||
|         draw.font = transposed_font | ||||
|         box_size_b = draw.textsize(word) | ||||
|         with pytest.warns(DeprecationWarning) as log: | ||||
|             box_size_b = draw.textsize(word) | ||||
|             assert box_size_b == transposed_font.getsize(word) | ||||
|         assert len(log) == 2 | ||||
|         bbox_b = draw.textbbox((20, 20), word) | ||||
| 
 | ||||
|         # Check (w,h) of box a is (h,w) of box b | ||||
|         assert box_size_a[0] == box_size_b[1] | ||||
|         assert box_size_a[1] == box_size_b[0] | ||||
| 
 | ||||
|         # Check bbox b is (20, 20, 20 + h, 20 + w) | ||||
|         assert bbox_b[0] == 20 | ||||
|         assert bbox_b[1] == 20 | ||||
|         assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1] | ||||
|         assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] | ||||
| 
 | ||||
|         # text length is undefined for vertical text | ||||
|         pytest.raises(ValueError, draw.textlength, word) | ||||
| 
 | ||||
|     def test_unrotated_transposed_font(self): | ||||
|         img_grey = Image.new("L", (100, 100)) | ||||
|         draw = ImageDraw.Draw(img_grey) | ||||
|  | @ -307,15 +353,31 @@ class TestImageFont: | |||
| 
 | ||||
|         # Original font | ||||
|         draw.font = font | ||||
|         box_size_a = draw.textsize(word) | ||||
|         with pytest.warns(DeprecationWarning) as log: | ||||
|             box_size_a = draw.textsize(word) | ||||
|         assert len(log) == 1 | ||||
|         bbox_a = draw.textbbox((10, 10), word) | ||||
|         length_a = draw.textlength(word) | ||||
| 
 | ||||
|         # Rotated font | ||||
|         draw.font = transposed_font | ||||
|         box_size_b = draw.textsize(word) | ||||
|         with pytest.warns(DeprecationWarning) as log: | ||||
|             box_size_b = draw.textsize(word) | ||||
|         assert len(log) == 1 | ||||
|         bbox_b = draw.textbbox((20, 20), word) | ||||
|         length_b = draw.textlength(word) | ||||
| 
 | ||||
|         # Check boxes a and b are same size | ||||
|         assert box_size_a == box_size_b | ||||
| 
 | ||||
|         # Check bbox b is (20, 20, 20 + w, 20 + h) | ||||
|         assert bbox_b[0] == 20 | ||||
|         assert bbox_b[1] == 20 | ||||
|         assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0] | ||||
|         assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1] | ||||
| 
 | ||||
|         assert length_a == length_b | ||||
| 
 | ||||
|     def test_rotated_transposed_font_get_mask(self): | ||||
|         # Arrange | ||||
|         text = "mask this" | ||||
|  | @ -370,9 +432,11 @@ class TestImageFont: | |||
|         text = "offset this" | ||||
| 
 | ||||
|         # Act | ||||
|         offset = font.getoffset(text) | ||||
|         with pytest.warns(DeprecationWarning) as log: | ||||
|             offset = font.getoffset(text) | ||||
| 
 | ||||
|         # Assert | ||||
|         assert len(log) == 1 | ||||
|         assert offset == (0, 3) | ||||
| 
 | ||||
|     def test_free_type_font_get_mask(self): | ||||
|  | @ -414,11 +478,11 @@ class TestImageFont: | |||
|         # Assert | ||||
|         assert_image_equal_tofile(im, "Tests/images/default_font.png") | ||||
| 
 | ||||
|     def test_getsize_empty(self): | ||||
|     def test_getbbox_empty(self): | ||||
|         # issue #2614 | ||||
|         font = self.get_font() | ||||
|         # should not crash. | ||||
|         assert (0, 0) == font.getsize("") | ||||
|         assert (0, 0, 0, 0) == font.getbbox("") | ||||
| 
 | ||||
|     def test_render_empty(self): | ||||
|         # issue 2666 | ||||
|  | @ -435,7 +499,7 @@ class TestImageFont: | |||
|         # issue #2826 | ||||
|         font = ImageFont.load_default() | ||||
|         with pytest.raises(UnicodeEncodeError): | ||||
|             font.getsize("’") | ||||
|             font.getbbox("’") | ||||
| 
 | ||||
|     def test_unicode_extended(self): | ||||
|         # issue #3777 | ||||
|  | @ -560,17 +624,29 @@ class TestImageFont: | |||
|         assert t.font.x_ppem == 20 | ||||
|         assert t.font.y_ppem == 20 | ||||
|         assert t.font.glyphs == 4177 | ||||
|         assert t.getsize("A") == (12, 16) | ||||
|         assert t.getsize("AB") == (24, 16) | ||||
|         assert t.getsize("M") == (12, 16) | ||||
|         assert t.getsize("y") == (12, 20) | ||||
|         assert t.getsize("a") == (12, 16) | ||||
|         assert t.getsize_multiline("A") == (12, 16) | ||||
|         assert t.getsize_multiline("AB") == (24, 16) | ||||
|         assert t.getsize_multiline("a") == (12, 16) | ||||
|         assert t.getsize_multiline("ABC\n") == (36, 36) | ||||
|         assert t.getsize_multiline("ABC\nA") == (36, 36) | ||||
|         assert t.getsize_multiline("ABC\nAaaa") == (48, 36) | ||||
|         assert t.getbbox("A") == (0, 4, 12, 16) | ||||
|         assert t.getbbox("AB") == (0, 4, 24, 16) | ||||
|         assert t.getbbox("M") == (0, 4, 12, 16) | ||||
|         assert t.getbbox("y") == (0, 7, 12, 20) | ||||
|         assert t.getbbox("a") == (0, 7, 12, 16) | ||||
|         assert t.getlength("A") == 12 | ||||
|         assert t.getlength("AB") == 24 | ||||
|         assert t.getlength("M") == 12 | ||||
|         assert t.getlength("y") == 12 | ||||
|         assert t.getlength("a") == 12 | ||||
|         with pytest.warns(DeprecationWarning) as log: | ||||
|             assert t.getsize("A") == (12, 16) | ||||
|             assert t.getsize("AB") == (24, 16) | ||||
|             assert t.getsize("M") == (12, 16) | ||||
|             assert t.getsize("y") == (12, 20) | ||||
|             assert t.getsize("a") == (12, 16) | ||||
|             assert t.getsize_multiline("A") == (12, 16) | ||||
|             assert t.getsize_multiline("AB") == (24, 16) | ||||
|             assert t.getsize_multiline("a") == (12, 16) | ||||
|             assert t.getsize_multiline("ABC\n") == (36, 36) | ||||
|             assert t.getsize_multiline("ABC\nA") == (36, 36) | ||||
|             assert t.getsize_multiline("ABC\nAaaa") == (48, 36) | ||||
|         assert len(log) == 11 | ||||
| 
 | ||||
|     def test_getsize_stroke(self): | ||||
|         # Arrange | ||||
|  | @ -578,14 +654,22 @@ class TestImageFont: | |||
| 
 | ||||
|         # Act / Assert | ||||
|         for stroke_width in [0, 2]: | ||||
|             assert t.getsize("A", stroke_width=stroke_width) == ( | ||||
|                 12 + stroke_width * 2, | ||||
|                 16 + stroke_width * 2, | ||||
|             ) | ||||
|             assert t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( | ||||
|                 48 + stroke_width * 2, | ||||
|                 36 + stroke_width * 4, | ||||
|             assert t.getbbox("A", stroke_width=stroke_width) == ( | ||||
|                 0 - stroke_width, | ||||
|                 4 - stroke_width, | ||||
|                 12 + stroke_width, | ||||
|                 16 + stroke_width, | ||||
|             ) | ||||
|             with pytest.warns(DeprecationWarning) as log: | ||||
|                 assert t.getsize("A", stroke_width=stroke_width) == ( | ||||
|                     12 + stroke_width * 2, | ||||
|                     16 + stroke_width * 2, | ||||
|                 ) | ||||
|                 assert t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width) == ( | ||||
|                     48 + stroke_width * 2, | ||||
|                     36 + stroke_width * 4, | ||||
|                 ) | ||||
|             assert len(log) == 2 | ||||
| 
 | ||||
|     def test_complex_font_settings(self): | ||||
|         # Arrange | ||||
|  | @ -717,8 +801,11 @@ class TestImageFont: | |||
|         im = Image.new("RGB", (200, 200)) | ||||
|         d = ImageDraw.Draw(im) | ||||
|         default_font = ImageFont.load_default() | ||||
|         with pytest.raises(ValueError): | ||||
|             d.textbbox((0, 0), "test", font=default_font) | ||||
|         with pytest.warns(DeprecationWarning) as log: | ||||
|             width, height = d.textsize("test", font=default_font) | ||||
|         assert len(log) == 1 | ||||
|         assert d.textlength("test", font=default_font) == width | ||||
|         assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, width, height) | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         "anchor, left, top", | ||||
|  | @ -865,7 +952,7 @@ class TestImageFont: | |||
|     def test_standard_embedded_color(self): | ||||
|         txt = "Hello World!" | ||||
|         ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE) | ||||
|         ttf.getsize(txt) | ||||
|         ttf.getbbox(txt) | ||||
| 
 | ||||
|         im = Image.new("RGB", (300, 64), "white") | ||||
|         d = ImageDraw.Draw(im) | ||||
|  |  | |||
|  | @ -140,8 +140,8 @@ def test_ligature_features(): | |||
|     target = "Tests/images/test_ligature_features.png" | ||||
|     assert_image_similar_tofile(im, target, 0.5) | ||||
| 
 | ||||
|     liga_size = ttf.getsize("fi", features=["-liga"]) | ||||
|     assert liga_size == (13, 19) | ||||
|     liga_bbox = ttf.getbbox("fi", features=["-liga"]) | ||||
|     assert liga_bbox == (0, 4, 13, 19) | ||||
| 
 | ||||
| 
 | ||||
| def test_kerning_features(): | ||||
|  |  | |||
|  | @ -345,11 +345,15 @@ def test_exif_transpose(): | |||
|                     check(orientation_im) | ||||
| 
 | ||||
|     # Orientation from "XML:com.adobe.xmp" info key | ||||
|     with Image.open("Tests/images/xmp_tags_orientation.png") as im: | ||||
|         assert im.getexif()[0x0112] == 3 | ||||
|     for suffix in ("", "_exiftool"): | ||||
|         with Image.open("Tests/images/xmp_tags_orientation" + suffix + ".png") as im: | ||||
|             assert im.getexif()[0x0112] == 3 | ||||
| 
 | ||||
|         transposed_im = ImageOps.exif_transpose(im) | ||||
|         assert 0x0112 not in transposed_im.getexif() | ||||
|             transposed_im = ImageOps.exif_transpose(im) | ||||
|             assert 0x0112 not in transposed_im.getexif() | ||||
| 
 | ||||
|             transposed_im._reload_exif() | ||||
|             assert 0x0112 not in transposed_im.getexif() | ||||
| 
 | ||||
|     # Orientation from "Raw profile type exif" info key | ||||
|     # This test image has been manually hexedited from exif_imagemagick.png | ||||
|  |  | |||
|  | @ -16,32 +16,32 @@ if ImageQt.qt_is_installed: | |||
|     from PIL.ImageQt import QImage | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity(tmp_path): | ||||
|     for mode in ("RGB", "RGBA", "L", "P", "1"): | ||||
|         src = hopper(mode) | ||||
|         data = ImageQt.toqimage(src) | ||||
| @pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) | ||||
| def test_sanity(mode, tmp_path): | ||||
|     src = hopper(mode) | ||||
|     data = ImageQt.toqimage(src) | ||||
| 
 | ||||
|         assert isinstance(data, QImage) | ||||
|         assert not data.isNull() | ||||
|     assert isinstance(data, QImage) | ||||
|     assert not data.isNull() | ||||
| 
 | ||||
|         # reload directly from the qimage | ||||
|         rt = ImageQt.fromqimage(data) | ||||
|         if mode in ("L", "P", "1"): | ||||
|             assert_image_equal(rt, src.convert("RGB")) | ||||
|         else: | ||||
|             assert_image_equal(rt, src) | ||||
|     # reload directly from the qimage | ||||
|     rt = ImageQt.fromqimage(data) | ||||
|     if mode in ("L", "P", "1"): | ||||
|         assert_image_equal(rt, src.convert("RGB")) | ||||
|     else: | ||||
|         assert_image_equal(rt, src) | ||||
| 
 | ||||
|         if mode == "1": | ||||
|             # BW appears to not save correctly on QT4 and QT5 | ||||
|             # kicks out errors on console: | ||||
|             #     libpng warning: Invalid color type/bit depth combination | ||||
|             #                     in IHDR | ||||
|             #     libpng error: Invalid IHDR data | ||||
|             continue | ||||
|     if mode == "1": | ||||
|         # BW appears to not save correctly on QT5 | ||||
|         # kicks out errors on console: | ||||
|         #     libpng warning: Invalid color type/bit depth combination | ||||
|         #                     in IHDR | ||||
|         #     libpng error: Invalid IHDR data | ||||
|         return | ||||
| 
 | ||||
|         # Test saving the file | ||||
|         tempfile = str(tmp_path / f"temp_{mode}.png") | ||||
|         data.save(tempfile) | ||||
|     # Test saving the file | ||||
|     tempfile = str(tmp_path / f"temp_{mode}.png") | ||||
|     data.save(tempfile) | ||||
| 
 | ||||
|         # Check that it actually worked. | ||||
|         assert_image_equal_tofile(src, tempfile) | ||||
|     # Check that it actually worked. | ||||
|     assert_image_equal_tofile(src, tempfile) | ||||
|  |  | |||