Merge branch 'main' into convert_mode
|  | @ -25,8 +25,8 @@ install: | |||
| - mv c:\pillow-depends-main c:\pillow-depends | ||||
| - xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images | ||||
| - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ | ||||
| - ..\pillow-depends\gs9561w32.exe /S | ||||
| - path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH% | ||||
| - ..\pillow-depends\gs1000w32.exe /S | ||||
| - path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH% | ||||
| - cd c:\pillow\winbuild\ | ||||
| - ps: | | ||||
|         c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ | ||||
|  |  | |||
|  | @ -35,15 +35,13 @@ 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 | ||||
| 
 | ||||
| if [[ $(uname) != CYGWIN* ]]; then | ||||
|     # 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 | ||||
| 
 | ||||
|     # 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 | ||||
|         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 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										17
									
								
								.github/renovate.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,17 @@ | |||
| { | ||||
|     "$schema": "https://docs.renovatebot.com/renovate-schema.json", | ||||
|     "extends": [ | ||||
|         "config:base" | ||||
|     ], | ||||
|     "labels": [ | ||||
|         "Dependency" | ||||
|     ], | ||||
|     "packageRules": [ | ||||
|         { | ||||
|             "groupName": "github-actions", | ||||
|             "matchManagers": ["github-actions"], | ||||
|             "separateMajorMinor": "false" | ||||
|         } | ||||
|     ], | ||||
|     "schedule": ["on the 3rd day of the month"] | ||||
| } | ||||
							
								
								
									
										7
									
								
								.github/workflows/cifuzz.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -11,6 +11,13 @@ on: | |||
|       - "**.h" | ||||
|   workflow_dispatch: | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| concurrency:  | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| jobs: | ||||
|   Fuzzing: | ||||
|     runs-on: ubuntu-latest | ||||
|  |  | |||
							
								
								
									
										11
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -2,6 +2,13 @@ name: Lint | |||
| 
 | ||||
| on: [push, pull_request, workflow_dispatch] | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| concurrency:  | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
| 
 | ||||
|  | @ -13,7 +20,7 @@ jobs: | |||
|     - uses: actions/checkout@v3 | ||||
| 
 | ||||
|     - name: pre-commit cache | ||||
|       uses: actions/cache@v2 | ||||
|       uses: actions/cache@v3 | ||||
|       with: | ||||
|         path: ~/.cache/pre-commit | ||||
|         key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} | ||||
|  | @ -21,7 +28,7 @@ jobs: | |||
|           lint-pre-commit- | ||||
| 
 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v3 | ||||
|       uses: actions/setup-python@v4 | ||||
|       with: | ||||
|         python-version: "3.10" | ||||
|         cache: pip | ||||
|  |  | |||
							
								
								
									
										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 | ||||
|  |  | |||
							
								
								
									
										10
									
								
								.github/workflows/release-drafter.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -7,8 +7,18 @@ on: | |||
|       - main | ||||
|   workflow_dispatch: | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| concurrency:  | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| 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: | ||||
|  |  | |||
							
								
								
									
										6
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -8,6 +8,10 @@ on: | |||
| permissions: | ||||
|   issues: write | ||||
| 
 | ||||
| concurrency:  | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| jobs: | ||||
|   stale: | ||||
|     if: github.repository_owner == 'python-pillow' | ||||
|  | @ -16,7 +20,7 @@ jobs: | |||
| 
 | ||||
|     steps: | ||||
|     - name: "Check issues" | ||||
|       uses: actions/stale@v5 | ||||
|       uses: actions/stale@v6 | ||||
|       with: | ||||
|         repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|         only-labels: "Awaiting OP Action" | ||||
|  |  | |||
							
								
								
									
										11
									
								
								.github/workflows/test-cygwin.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -2,6 +2,13 @@ name: Test Cygwin | |||
| 
 | ||||
| on: [push, pull_request, workflow_dispatch] | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| concurrency:  | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: windows-latest | ||||
|  | @ -41,7 +48,7 @@ jobs: | |||
|             qt5-devel-tools subversion xorg-server-extra zlib-devel | ||||
| 
 | ||||
|       - name: Add Lapack to PATH | ||||
|         uses: egor-tensin/cleanup-path@v1 | ||||
|         uses: egor-tensin/cleanup-path@v2 | ||||
|         with: | ||||
|           dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' | ||||
| 
 | ||||
|  | @ -99,6 +106,8 @@ jobs: | |||
|           name: Cygwin Python 3.${{ matrix.python-minor-version }} | ||||
| 
 | ||||
|   success: | ||||
|     permissions: | ||||
|       contents: none | ||||
|     needs: build | ||||
|     runs-on: ubuntu-latest | ||||
|     name: Cygwin Test Successful | ||||
|  |  | |||
							
								
								
									
										11
									
								
								.github/workflows/test-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -2,6 +2,13 @@ name: Test Docker | |||
| 
 | ||||
| on: [push, pull_request, workflow_dispatch] | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| concurrency:  | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
| 
 | ||||
|  | @ -76,12 +83,14 @@ jobs: | |||
|         MATRIX_DOCKER: ${{ matrix.docker }} | ||||
| 
 | ||||
|     - name: Upload coverage | ||||
|       uses: codecov/codecov-action@v1 | ||||
|       uses: codecov/codecov-action@v3 | ||||
|       with: | ||||
|         flags: GHA_Docker | ||||
|         name: ${{ matrix.docker }} | ||||
| 
 | ||||
|   success: | ||||
|     permissions: | ||||
|       contents: none | ||||
|     needs: build | ||||
|     runs-on: ubuntu-latest | ||||
|     name: Docker Test Successful | ||||
|  |  | |||
							
								
								
									
										19
									
								
								.github/workflows/test-mingw.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -2,6 +2,13 @@ name: Test MinGW | |||
| 
 | ||||
| on: [push, pull_request, workflow_dispatch] | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| concurrency:  | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: windows-latest | ||||
|  | @ -70,13 +77,15 @@ jobs: | |||
|           python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests | ||||
| 
 | ||||
|       - name: Upload coverage | ||||
|         run: | | ||||
|           python3 -m pip install codecov | ||||
|           bash <(curl -s https://codecov.io/bash) -F GHA_Windows | ||||
|         env: | ||||
|           CODECOV_NAME: ${{ matrix.name }} | ||||
|         uses: codecov/codecov-action@v3 | ||||
|         with: | ||||
|           file: ./coverage.xml | ||||
|           flags: GHA_Windows | ||||
|           name: ${{ matrix.name }} | ||||
| 
 | ||||
|   success: | ||||
|     permissions: | ||||
|       contents: none | ||||
|     needs: build | ||||
|     runs-on: ubuntu-latest | ||||
|     name: MinGW Test Successful | ||||
|  |  | |||
							
								
								
									
										9
									
								
								.github/workflows/test-valgrind.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -13,6 +13,13 @@ on: | |||
|       - "**.h" | ||||
|   workflow_dispatch: | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| concurrency:  | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
| 
 | ||||
|  | @ -21,7 +28,7 @@ jobs: | |||
|       fail-fast: false | ||||
|       matrix: | ||||
|         docker: [ | ||||
|           ubuntu-20.04-focal-amd64-valgrind, | ||||
|           ubuntu-22.04-jammy-amd64-valgrind, | ||||
|         ] | ||||
|         dockerTag: [main] | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										19
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -2,6 +2,13 @@ name: Test Windows | |||
| 
 | ||||
| on: [push, pull_request, workflow_dispatch] | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| concurrency:  | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: windows-latest | ||||
|  | @ -33,7 +40,7 @@ jobs: | |||
| 
 | ||||
|     # sets env: pythonLocation | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v3 | ||||
|       uses: actions/setup-python@v4 | ||||
|       with: | ||||
|         python-version: ${{ matrix.python-version }} | ||||
|         architecture: ${{ matrix.architecture }} | ||||
|  | @ -52,8 +59,8 @@ jobs: | |||
|         7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\" | ||||
|         echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH | ||||
| 
 | ||||
|         winbuild\depends\gs9561w32.exe /S | ||||
|         echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH | ||||
|         winbuild\depends\gs1000w32.exe /S | ||||
|         echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH | ||||
| 
 | ||||
|         xcopy /S /Y winbuild\depends\test_images\* Tests\images\ | ||||
| 
 | ||||
|  | @ -63,7 +70,7 @@ jobs: | |||
| 
 | ||||
|     - name: Cache build | ||||
|       id: build-cache | ||||
|       uses: actions/cache@v2 | ||||
|       uses: actions/cache@v3 | ||||
|       with: | ||||
|         path: winbuild\build | ||||
|         key: | ||||
|  | @ -168,7 +175,7 @@ jobs: | |||
|       shell: pwsh | ||||
| 
 | ||||
|     - name: Upload coverage | ||||
|       uses: codecov/codecov-action@v1 | ||||
|       uses: codecov/codecov-action@v3 | ||||
|       with: | ||||
|         file: ./coverage.xml | ||||
|         flags: GHA_Windows | ||||
|  | @ -189,6 +196,8 @@ jobs: | |||
|         path: dist\*.whl | ||||
| 
 | ||||
|   success: | ||||
|     permissions: | ||||
|       contents: none | ||||
|     needs: build | ||||
|     runs-on: ubuntu-latest | ||||
|     name: Windows Test Successful | ||||
|  |  | |||
							
								
								
									
										25
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -2,6 +2,13 @@ name: Test | |||
| 
 | ||||
| on: [push, pull_request, workflow_dispatch] | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| concurrency:  | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
| 
 | ||||
|  | @ -27,11 +34,6 @@ jobs: | |||
|           REVERSE: "--reverse" | ||||
|         - python-version: "3.8" | ||||
|           PYTHONOPTIMIZE: 2 | ||||
|         # Include new variables for Codecov | ||||
|         - os: ubuntu-latest | ||||
|           codecov-flag: GHA_Ubuntu | ||||
|         - os: macos-latest | ||||
|           codecov-flag: GHA_macOS | ||||
| 
 | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     name: ${{ matrix.os }} Python ${{ matrix.python-version }} | ||||
|  | @ -40,7 +42,7 @@ jobs: | |||
|     - uses: actions/checkout@v3 | ||||
| 
 | ||||
|     - name: Set up Python ${{ matrix.python-version }} | ||||
|       uses: actions/setup-python@v3 | ||||
|       uses: actions/setup-python@v4 | ||||
|       with: | ||||
|         python-version: ${{ matrix.python-version }} | ||||
|         cache: pip | ||||
|  | @ -96,7 +98,6 @@ jobs: | |||
|     - name: Docs | ||||
|       if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10 | ||||
|       run: | | ||||
|         python3 -m pip install furo sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph | ||||
|         make doccheck | ||||
| 
 | ||||
|     - name: After success | ||||
|  | @ -104,11 +105,15 @@ jobs: | |||
|         .ci/after_success.sh | ||||
| 
 | ||||
|     - name: Upload coverage | ||||
|       run: bash <(curl -s https://codecov.io/bash) -F ${{ matrix.codecov-flag }} | ||||
|       env: | ||||
|         CODECOV_NAME: ${{ matrix.os }} Python ${{ matrix.python-version }} | ||||
|       uses: codecov/codecov-action@v3 | ||||
|       with: | ||||
|         file: ./coverage.xml | ||||
|         flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} | ||||
|         name: ${{ matrix.os }} Python ${{ matrix.python-version }} | ||||
| 
 | ||||
|   success: | ||||
|     permissions: | ||||
|       contents: none | ||||
|     needs: build | ||||
|     runs-on: ubuntu-latest | ||||
|     name: Test Successful | ||||
|  |  | |||
							
								
								
									
										8
									
								
								.github/workflows/tidelift.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -1,4 +1,5 @@ | |||
| name: Tidelift Align | ||||
| 
 | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: "30 2 * * *"  # daily at 02:30 UTC | ||||
|  | @ -12,6 +13,13 @@ on: | |||
|       - ".github/workflows/tidelift.yml" | ||||
|   workflow_dispatch: | ||||
| 
 | ||||
| permissions: | ||||
|   contents: read | ||||
| 
 | ||||
| concurrency:  | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| 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.8.0 | ||||
|     hooks: | ||||
|       - id: black | ||||
|         args: ["--target-version", "py37"] | ||||
|  | @ -14,18 +14,18 @@ repos: | |||
|       - id: isort | ||||
| 
 | ||||
|   - repo: https://github.com/asottile/yesqa | ||||
|     rev: v1.3.0 | ||||
|     rev: v1.4.0 | ||||
|     hooks: | ||||
|       - id: yesqa | ||||
| 
 | ||||
|   - repo: https://github.com/Lucas-C/pre-commit-hooks | ||||
|     rev: v1.2.0 | ||||
|     rev: v1.3.1 | ||||
|     hooks: | ||||
|       - id: remove-tabs | ||||
|         exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) | ||||
| 
 | ||||
|   - repo: https://github.com/PyCQA/flake8 | ||||
|     rev: 4.0.1 | ||||
|     rev: 5.0.4 | ||||
|     hooks: | ||||
|       - id: flake8 | ||||
|         additional_dependencies: [flake8-2020, flake8-implicit-str-concat] | ||||
|  | @ -37,13 +37,14 @@ repos: | |||
|       - id: rst-backticks | ||||
| 
 | ||||
|   - repo: https://github.com/pre-commit/pre-commit-hooks | ||||
|     rev: v4.2.0 | ||||
|     rev: v4.3.0 | ||||
|     hooks: | ||||
|       - id: check-merge-conflict | ||||
|       - id: check-json | ||||
|       - id: check-yaml | ||||
| 
 | ||||
|   - repo: https://github.com/sphinx-contrib/sphinx-lint | ||||
|     rev: v0.6 | ||||
|     rev: v0.6.1 | ||||
|     hooks: | ||||
|       - id: sphinx-lint | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										128
									
								
								CHANGES.rst
									
									
									
									
									
								
							
							
						
						|  | @ -2,9 +2,135 @@ | |||
| Changelog (Pillow) | ||||
| ================== | ||||
| 
 | ||||
| 9.2.0 (unreleased) | ||||
| 9.3.0 (unreleased) | ||||
| ------------------ | ||||
| 
 | ||||
| - Added reading of TIFF child images #6569 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Improved ImageOps palette handling #6596 | ||||
|   [PososikTeam, radarhere] | ||||
| 
 | ||||
| - Defer parsing of palette into colors #6567 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Apply transparency to P images in ImageTk.PhotoImage #6559 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Use rounding in ImageOps contain() and pad() #6522 | ||||
|   [bibinhashley, radarhere] | ||||
| 
 | ||||
| - Fixed GIF remapping to palette with duplicate entries #6548 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Allow remap_palette() to return an image with less than 256 palette entries #6543 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Corrected BMP and TGA palette size when saving #6500 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Do not call load() before draft() in Image.thumbnail #6539 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Copy palette when converting from P to PA #6497 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Allow RGB and RGBA values for PA image putpixel #6504 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Removed support for tkinter in PyPy before Python 3.6 #6551 | ||||
|   [nulano] | ||||
| 
 | ||||
| - Do not use CCITTFaxDecode filter if libtiff is not available #6518 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Fallback to not using mmap if buffer is not large enough #6510 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Fixed writing bytes as ASCII tag #6493 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Open 1 bit EPS in mode 1 #6499 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - Removed support for tkinter before Python 1.5.2 #6549 | ||||
|   [radarhere] | ||||
| 
 | ||||
| - 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] | ||||
| 
 | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ exclude .coveragerc | |||
| exclude .editorconfig | ||||
| exclude .readthedocs.yml | ||||
| exclude codecov.yml | ||||
| exclude renovate.json | ||||
| global-exclude .git* | ||||
| global-exclude *.pyc | ||||
| global-exclude *.so | ||||
|  |  | |||
							
								
								
									
										3
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						|  | @ -17,11 +17,12 @@ coverage: | |||
| 
 | ||||
| .PHONY: doc | ||||
| doc: | ||||
| 	python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . | ||||
| 	$(MAKE) -C docs html | ||||
| 
 | ||||
| .PHONY: doccheck | ||||
| doccheck: | ||||
| 	$(MAKE) -C docs html | ||||
| 	$(MAKE) doc | ||||
| # Don't make our tests rely on the links in the docs being up every single build.
 | ||||
| # We don't control them.  But do check, and update them to the target of their redirects.
 | ||||
| 	$(MAKE) -C docs linkcheck || true | ||||
|  |  | |||
|  | @ -74,6 +74,9 @@ As of 2019, Pillow development is | |||
|             <a href="https://pypi.org/project/Pillow/"><img | ||||
|                 alt="Number of PyPI downloads" | ||||
|                 src="https://img.shields.io/pypi/dm/pillow.svg"></a> | ||||
|             <a href="https://bestpractices.coreinfrastructure.org/projects/6331"><img | ||||
|                 alt="OpenSSF Best Practices" | ||||
|                 src="https://bestpractices.coreinfrastructure.org/projects/6331/badge"></a> | ||||
|         </td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|  |  | |||
|  | @ -96,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): | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/1.eps
									
									
									
									
									
										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/child_ifd.tiff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/child_ifd_jpeg.tiff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/comment_after_only_frame.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 54 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/decompression_bomb_extents.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 368 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_rle8_greyscale.bmp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.1 KiB | 
							
								
								
									
										
											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/mmap_error.bmp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.0 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/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") | ||||
|  |  | |||
|  | @ -1,19 +1,18 @@ | |||
| import PIL | ||||
| import PIL.Image | ||||
| from PIL import Image | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity(): | ||||
|     # Make sure we have the binary extension | ||||
|     PIL.Image.core.new("L", (100, 100)) | ||||
|     Image.core.new("L", (100, 100)) | ||||
| 
 | ||||
|     # Create an image and do stuff with it. | ||||
|     im = PIL.Image.new("1", (100, 100)) | ||||
|     im = Image.new("1", (100, 100)) | ||||
|     assert (im.mode, im.size) == ("1", (100, 100)) | ||||
|     assert len(im.tobytes()) == 1300 | ||||
| 
 | ||||
|     # Create images in all remaining major modes. | ||||
|     PIL.Image.new("L", (100, 100)) | ||||
|     PIL.Image.new("P", (100, 100)) | ||||
|     PIL.Image.new("RGB", (100, 100)) | ||||
|     PIL.Image.new("I", (100, 100)) | ||||
|     PIL.Image.new("F", (100, 100)) | ||||
|     Image.new("L", (100, 100)) | ||||
|     Image.new("P", (100, 100)) | ||||
|     Image.new("RGB", (100, 100)) | ||||
|     Image.new("I", (100, 100)) | ||||
|     Image.new("F", (100, 100)) | ||||
|  |  | |||
|  | @ -61,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"): | ||||
|  |  | |||
|  | @ -70,14 +70,14 @@ def test_libimagequant_version(): | |||
|     assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant")) | ||||
| 
 | ||||
| 
 | ||||
| def test_check_modules(): | ||||
|     for feature in features.modules: | ||||
|         assert features.check_module(feature) in [True, False] | ||||
| @pytest.mark.parametrize("feature", features.modules) | ||||
| def test_check_modules(feature): | ||||
|     assert features.check_module(feature) in [True, False] | ||||
| 
 | ||||
| 
 | ||||
| def test_check_codecs(): | ||||
|     for feature in features.codecs: | ||||
|         assert features.check_codec(feature) in [True, False] | ||||
| @pytest.mark.parametrize("feature", features.codecs) | ||||
| def test_check_codecs(feature): | ||||
|     assert features.check_codec(feature) in [True, False] | ||||
| 
 | ||||
| 
 | ||||
| def test_check_warns_on_nonexistent(): | ||||
|  |  | |||
|  | @ -39,13 +39,12 @@ def test_apng_basic(): | |||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
| 
 | ||||
| 
 | ||||
| def test_apng_fdat(): | ||||
|     with Image.open("Tests/images/apng/split_fdat.png") as im: | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/split_fdat_zero_chunk.png") as im: | ||||
| @pytest.mark.parametrize( | ||||
|     "filename", | ||||
|     ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), | ||||
| ) | ||||
| def test_apng_fdat(filename): | ||||
|     with Image.open(filename) as im: | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
|  | @ -325,8 +324,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 +334,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): | ||||
|  |  | |||
|  | @ -39,6 +39,13 @@ def test_invalid_file(): | |||
|             BmpImagePlugin.BmpImageFile(fp) | ||||
| 
 | ||||
| 
 | ||||
| def test_fallback_if_mmap_errors(): | ||||
|     # This image has been truncated, | ||||
|     # so that the buffer is not large enough when using mmap | ||||
|     with Image.open("Tests/images/mmap_error.bmp") as im: | ||||
|         assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp") | ||||
| 
 | ||||
| 
 | ||||
| def test_save_to_bytes(): | ||||
|     output = io.BytesIO() | ||||
|     im = hopper() | ||||
|  | @ -51,6 +58,18 @@ def test_save_to_bytes(): | |||
|         assert reloaded.format == "BMP" | ||||
| 
 | ||||
| 
 | ||||
| def test_small_palette(tmp_path): | ||||
|     im = Image.new("P", (1, 1)) | ||||
|     colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] | ||||
|     im.putpalette(colors) | ||||
| 
 | ||||
|     out = str(tmp_path / "temp.bmp") | ||||
|     im.save(out) | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert reloaded.getpalette() == colors | ||||
| 
 | ||||
| 
 | ||||
| def test_save_too_large(tmp_path): | ||||
|     outfile = str(tmp_path / "temp.bmp") | ||||
|     with Image.new("RGB", (1, 1)) as im: | ||||
|  | @ -129,11 +148,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"), | ||||
|     ( | ||||
|  |  | |||
|  | @ -124,14 +124,6 @@ def test_file_object(tmp_path): | |||
|             image1.save(fh, "EPS") | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") | ||||
| def test_iobase_object(tmp_path): | ||||
|     # issue 479 | ||||
|     with Image.open(FILE1) as image1: | ||||
|         with open(str(tmp_path / "temp_iobase.eps"), "wb") as fh: | ||||
|             image1.save(fh, "EPS") | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") | ||||
| def test_bytesio_object(): | ||||
|     with open(FILE1, "rb") as f: | ||||
|  | @ -146,6 +138,11 @@ def test_bytesio_object(): | |||
|         assert_image_similar(img, image1_scale1_compare, 5) | ||||
| 
 | ||||
| 
 | ||||
| def test_1_mode(): | ||||
|     with Image.open("Tests/images/1.eps") as im: | ||||
|         assert im.mode == "1" | ||||
| 
 | ||||
| 
 | ||||
| def test_image_mode_not_supported(tmp_path): | ||||
|     im = hopper("RGBA") | ||||
|     tmpfile = str(tmp_path / "temp.eps") | ||||
|  | @ -198,25 +195,23 @@ def test_render_scale2(): | |||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") | ||||
| def test_resize(): | ||||
|     files = [FILE1, FILE2, "Tests/images/illu10_preview.eps"] | ||||
|     for fn in files: | ||||
|         with Image.open(fn) as im: | ||||
|             new_size = (100, 100) | ||||
|             im = im.resize(new_size) | ||||
|             assert im.size == new_size | ||||
| @pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) | ||||
| def test_resize(filename): | ||||
|     with Image.open(filename) as im: | ||||
|         new_size = (100, 100) | ||||
|         im = im.resize(new_size) | ||||
|         assert im.size == new_size | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") | ||||
| def test_thumbnail(): | ||||
| @pytest.mark.parametrize("filename", (FILE1, FILE2)) | ||||
| def test_thumbnail(filename): | ||||
|     # Issue #619 | ||||
|     # Arrange | ||||
|     files = [FILE1, FILE2] | ||||
|     for fn in files: | ||||
|         with Image.open(FILE1) as im: | ||||
|             new_size = (100, 100) | ||||
|             im.thumbnail(new_size) | ||||
|             assert max(im.size) == max(new_size) | ||||
|     with Image.open(filename) as im: | ||||
|         new_size = (100, 100) | ||||
|         im.thumbnail(new_size) | ||||
|         assert max(im.size) == max(new_size) | ||||
| 
 | ||||
| 
 | ||||
| def test_read_binary_preview(): | ||||
|  | @ -261,20 +256,19 @@ def test_readline(tmp_path): | |||
|         _test_readline_file_psfile(s, ending) | ||||
| 
 | ||||
| 
 | ||||
| def test_open_eps(): | ||||
|     # https://github.com/python-pillow/Pillow/issues/1104 | ||||
|     # Arrange | ||||
|     FILES = [ | ||||
| @pytest.mark.parametrize( | ||||
|     "filename", | ||||
|     ( | ||||
|         "Tests/images/illu10_no_preview.eps", | ||||
|         "Tests/images/illu10_preview.eps", | ||||
|         "Tests/images/illuCS6_no_preview.eps", | ||||
|         "Tests/images/illuCS6_preview.eps", | ||||
|     ] | ||||
| 
 | ||||
|     # Act / Assert | ||||
|     for filename in FILES: | ||||
|         with Image.open(filename) as img: | ||||
|             assert img.mode == "RGB" | ||||
|     ), | ||||
| ) | ||||
| def test_open_eps(filename): | ||||
|     # https://github.com/python-pillow/Pillow/issues/1104 | ||||
|     with Image.open(filename) as img: | ||||
|         assert img.mode == "RGB" | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -158,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) | ||||
|  | @ -167,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))) | ||||
|  | @ -180,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() | ||||
|  | @ -388,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: | ||||
|  | @ -777,24 +793,24 @@ def test_identical_frames(tmp_path): | |||
|         assert reread.info["duration"] == 4500 | ||||
| 
 | ||||
| 
 | ||||
| def test_identical_frames_to_single_frame(tmp_path): | ||||
|     for duration in ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500): | ||||
|         out = str(tmp_path / "temp.gif") | ||||
|         im_list = [ | ||||
|             Image.new("L", (100, 100), "#000"), | ||||
|             Image.new("L", (100, 100), "#000"), | ||||
|             Image.new("L", (100, 100), "#000"), | ||||
|         ] | ||||
| @pytest.mark.parametrize( | ||||
|     "duration", ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500) | ||||
| ) | ||||
| def test_identical_frames_to_single_frame(duration, tmp_path): | ||||
|     out = str(tmp_path / "temp.gif") | ||||
|     im_list = [ | ||||
|         Image.new("L", (100, 100), "#000"), | ||||
|         Image.new("L", (100, 100), "#000"), | ||||
|         Image.new("L", (100, 100), "#000"), | ||||
|     ] | ||||
| 
 | ||||
|         im_list[0].save( | ||||
|             out, save_all=True, append_images=im_list[1:], duration=duration | ||||
|         ) | ||||
|         with Image.open(out) as reread: | ||||
|             # Assert that all frames were combined | ||||
|             assert reread.n_frames == 1 | ||||
|     im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration) | ||||
|     with Image.open(out) as reread: | ||||
|         # Assert that all frames were combined | ||||
|         assert reread.n_frames == 1 | ||||
| 
 | ||||
|             # Assert that the new duration is the total of the identical frames | ||||
|             assert reread.info["duration"] == 8500 | ||||
|         # Assert that the new duration is the total of the identical frames | ||||
|         assert reread.info["duration"] == 8500 | ||||
| 
 | ||||
| 
 | ||||
| def test_number_of_loops(tmp_path): | ||||
|  | @ -982,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) | ||||
|  | @ -993,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): | ||||
|  | @ -1071,6 +1087,19 @@ def test_palette_save_P(tmp_path): | |||
|         assert_image_equal(reloaded, im) | ||||
| 
 | ||||
| 
 | ||||
| def test_palette_save_duplicate_entries(tmp_path): | ||||
|     im = Image.new("P", (1, 2)) | ||||
|     im.putpixel((0, 1), 1) | ||||
| 
 | ||||
|     im.putpalette((0, 0, 0, 0, 0, 0)) | ||||
| 
 | ||||
|     out = str(tmp_path / "temp.gif") | ||||
|     im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1]) | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0) | ||||
| 
 | ||||
| 
 | ||||
| def test_palette_save_all_P(tmp_path): | ||||
|     frames = [] | ||||
|     colors = ((255, 0, 0), (0, 255, 0)) | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -150,27 +150,30 @@ class TestFileJpeg: | |||
|             assert not im1.info.get("icc_profile") | ||||
|             assert im2.info.get("icc_profile") | ||||
| 
 | ||||
|     def test_icc_big(self): | ||||
|     @pytest.mark.parametrize( | ||||
|         "n", | ||||
|         ( | ||||
|             0, | ||||
|             1, | ||||
|             3, | ||||
|             4, | ||||
|             5, | ||||
|             65533 - 14,  # full JPEG marker block | ||||
|             65533 - 14 + 1,  # full block plus one byte | ||||
|             ImageFile.MAXBLOCK,  # full buffer block | ||||
|             ImageFile.MAXBLOCK + 1,  # full buffer block plus one byte | ||||
|             ImageFile.MAXBLOCK * 4 + 3,  # large block | ||||
|         ), | ||||
|     ) | ||||
|     def test_icc_big(self, n): | ||||
|         # Make sure that the "extra" support handles large blocks | ||||
|         def test(n): | ||||
|             # The ICC APP marker can store 65519 bytes per marker, so | ||||
|             # using a 4-byte test code should allow us to detect out of | ||||
|             # order issues. | ||||
|             icc_profile = (b"Test" * int(n / 4 + 1))[:n] | ||||
|             assert len(icc_profile) == n  # sanity | ||||
|             im1 = self.roundtrip(hopper(), icc_profile=icc_profile) | ||||
|             assert im1.info.get("icc_profile") == (icc_profile or None) | ||||
| 
 | ||||
|         test(0) | ||||
|         test(1) | ||||
|         test(3) | ||||
|         test(4) | ||||
|         test(5) | ||||
|         test(65533 - 14)  # full JPEG marker block | ||||
|         test(65533 - 14 + 1)  # full block plus one byte | ||||
|         test(ImageFile.MAXBLOCK)  # full buffer block | ||||
|         test(ImageFile.MAXBLOCK + 1)  # full buffer block plus one byte | ||||
|         test(ImageFile.MAXBLOCK * 4 + 3)  # large block | ||||
|         # The ICC APP marker can store 65519 bytes per marker, so | ||||
|         # using a 4-byte test code should allow us to detect out of | ||||
|         # order issues. | ||||
|         icc_profile = (b"Test" * int(n / 4 + 1))[:n] | ||||
|         assert len(icc_profile) == n  # sanity | ||||
|         im1 = self.roundtrip(hopper(), icc_profile=icc_profile) | ||||
|         assert im1.info.get("icc_profile") == (icc_profile or None) | ||||
| 
 | ||||
|     @mark_if_feature_version( | ||||
|         pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" | ||||
|  | @ -649,11 +652,11 @@ class TestFileJpeg: | |||
|             # Assert | ||||
|             assert im.format == "JPEG" | ||||
| 
 | ||||
|     def test_save_correct_modes(self): | ||||
|     @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) | ||||
|     def test_save_correct_modes(self, mode): | ||||
|         out = BytesIO() | ||||
|         for mode in ["1", "L", "RGB", "RGBX", "CMYK", "YCbCr"]: | ||||
|             img = Image.new(mode, (20, 20)) | ||||
|             img.save(out, "JPEG") | ||||
|         img = Image.new(mode, (20, 20)) | ||||
|         img.save(out, "JPEG") | ||||
| 
 | ||||
|     def test_save_wrong_modes(self, tmp_path): | ||||
|         # ref https://github.com/python-pillow/Pillow/issues/2005 | ||||
|  |  | |||
|  | @ -126,14 +126,14 @@ def test_prog_res_rt(): | |||
|     assert_image_equal(im, test_card) | ||||
| 
 | ||||
| 
 | ||||
| def test_default_num_resolutions(): | ||||
|     for num_resolutions in range(2, 6): | ||||
|         d = 1 << (num_resolutions - 1) | ||||
|         im = test_card.resize((d - 1, d - 1)) | ||||
|         with pytest.raises(OSError): | ||||
|             roundtrip(im, num_resolutions=num_resolutions) | ||||
|         reloaded = roundtrip(im) | ||||
|         assert_image_equal(im, reloaded) | ||||
| @pytest.mark.parametrize("num_resolutions", range(2, 6)) | ||||
| def test_default_num_resolutions(num_resolutions): | ||||
|     d = 1 << (num_resolutions - 1) | ||||
|     im = test_card.resize((d - 1, d - 1)) | ||||
|     with pytest.raises(OSError): | ||||
|         roundtrip(im, num_resolutions=num_resolutions) | ||||
|     reloaded = roundtrip(im) | ||||
|     assert_image_equal(im, reloaded) | ||||
| 
 | ||||
| 
 | ||||
| def test_reduce(): | ||||
|  | @ -266,14 +266,11 @@ def test_rgba(): | |||
|             assert jp2.mode == "RGBA" | ||||
| 
 | ||||
| 
 | ||||
| def test_16bit_monochrome_has_correct_mode(): | ||||
|     with Image.open("Tests/images/16bit.cropped.j2k") as j2k: | ||||
|         j2k.load() | ||||
|         assert j2k.mode == "I;16" | ||||
| 
 | ||||
|     with Image.open("Tests/images/16bit.cropped.jp2") as jp2: | ||||
|         jp2.load() | ||||
|         assert jp2.mode == "I;16" | ||||
| @pytest.mark.parametrize("ext", (".j2k", ".jp2")) | ||||
| def test_16bit_monochrome_has_correct_mode(ext): | ||||
|     with Image.open("Tests/images/16bit.cropped" + ext) as im: | ||||
|         im.load() | ||||
|         assert im.mode == "I;16" | ||||
| 
 | ||||
| 
 | ||||
| def test_16bit_monochrome_jp2_like_tiff(): | ||||
|  |  | |||
|  | @ -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): | ||||
|  | @ -509,20 +509,13 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|             # colormap/palette tag | ||||
|             assert len(reloaded.tag_v2[320]) == 768 | ||||
| 
 | ||||
|     def xtest_bw_compression_w_rgb(self, tmp_path): | ||||
|         """This test passes, but when running all tests causes a failure due | ||||
|         to output on stderr from the error thrown by libtiff. We need to | ||||
|         capture that but not now""" | ||||
| 
 | ||||
|     @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) | ||||
|     def test_bw_compression_w_rgb(self, compression, tmp_path): | ||||
|         im = hopper("RGB") | ||||
|         out = str(tmp_path / "temp.tif") | ||||
| 
 | ||||
|         with pytest.raises(OSError): | ||||
|             im.save(out, compression="tiff_ccitt") | ||||
|         with pytest.raises(OSError): | ||||
|             im.save(out, compression="group3") | ||||
|         with pytest.raises(OSError): | ||||
|             im.save(out, compression="group4") | ||||
|             im.save(out, compression=compression) | ||||
| 
 | ||||
|     def test_fp_leak(self): | ||||
|         im = Image.open("Tests/images/hopper_g4_500.tif") | ||||
|  | @ -856,7 +849,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 +857,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 +870,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 +878,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 +1004,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") | ||||
|  | @ -62,26 +66,25 @@ def test_context_manager(): | |||
|             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(): | ||||
|  | @ -133,12 +136,12 @@ def test_reload_exif_after_seek(): | |||
|         assert 296 in exif | ||||
| 
 | ||||
| 
 | ||||
| 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 | ||||
| @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(): | ||||
|  | @ -158,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(): | ||||
|  | @ -221,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 | ||||
|  |  | |||
|  | @ -63,19 +63,7 @@ def test_p_mode(tmp_path): | |||
|     roundtrip(tmp_path, mode) | ||||
| 
 | ||||
| 
 | ||||
| def test_l_oserror(tmp_path): | ||||
|     # Arrange | ||||
|     mode = "L" | ||||
| 
 | ||||
|     # Act / Assert | ||||
|     with pytest.raises(OSError): | ||||
|         helper_save_as_palm(tmp_path, mode) | ||||
| 
 | ||||
| 
 | ||||
| def test_rgb_oserror(tmp_path): | ||||
|     # Arrange | ||||
|     mode = "RGB" | ||||
| 
 | ||||
|     # Act / Assert | ||||
| @pytest.mark.parametrize("mode", ("L", "RGB")) | ||||
| def test_oserror(tmp_path, mode): | ||||
|     with pytest.raises(OSError): | ||||
|         helper_save_as_palm(tmp_path, mode) | ||||
|  |  | |||
|  | @ -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") | ||||
|  | @ -34,14 +39,14 @@ def test_invalid_file(): | |||
|         PcxImagePlugin.PcxImageFile(invalid_file) | ||||
| 
 | ||||
| 
 | ||||
| def test_odd(tmp_path): | ||||
| @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) | ||||
| def test_odd(tmp_path, mode): | ||||
|     # See issue #523, odd sized images should have a stride that's even. | ||||
|     # Not that ImageMagick or GIMP write PCX that way. | ||||
|     # We were not handling properly. | ||||
|     for mode in ("1", "L", "P", "RGB"): | ||||
|         # larger, odd sized images are better here to ensure that | ||||
|         # we handle interrupted scan lines properly. | ||||
|         _roundtrip(tmp_path, hopper(mode).resize((511, 511))) | ||||
|     # larger, odd sized images are better here to ensure that | ||||
|     # we handle interrupted scan lines properly. | ||||
|     _roundtrip(tmp_path, hopper(mode).resize((511, 511))) | ||||
| 
 | ||||
| 
 | ||||
| def test_odd_read(): | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ import time | |||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, PdfParser | ||||
| from PIL import Image, PdfParser, features | ||||
| 
 | ||||
| from .helper import hopper, mark_if_feature_version | ||||
| 
 | ||||
|  | @ -37,45 +37,19 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs): | |||
|     return outfile | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) | ||||
| def test_save(tmp_path, mode): | ||||
|     helper_save_as_pdf(tmp_path, mode) | ||||
| 
 | ||||
| 
 | ||||
| @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 | ||||
| 
 | ||||
| 
 | ||||
| def test_greyscale(tmp_path): | ||||
|     # Arrange | ||||
|     mode = "L" | ||||
| 
 | ||||
|     # Act / Assert | ||||
|     helper_save_as_pdf(tmp_path, mode) | ||||
| 
 | ||||
| 
 | ||||
| def test_rgb(tmp_path): | ||||
|     # Arrange | ||||
|     mode = "RGB" | ||||
| 
 | ||||
|     # Act / Assert | ||||
|     helper_save_as_pdf(tmp_path, mode) | ||||
| 
 | ||||
| 
 | ||||
| def test_p_mode(tmp_path): | ||||
|     # Arrange | ||||
|     mode = "P" | ||||
| 
 | ||||
|     # Act / Assert | ||||
|     helper_save_as_pdf(tmp_path, mode) | ||||
| 
 | ||||
| 
 | ||||
| def test_cmyk_mode(tmp_path): | ||||
|     # Arrange | ||||
|     mode = "CMYK" | ||||
| 
 | ||||
|     # Act / Assert | ||||
|     helper_save_as_pdf(tmp_path, mode) | ||||
|     assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000) | ||||
| 
 | ||||
| 
 | ||||
| def test_unsupported_mode(tmp_path): | ||||
|  |  | |||
|  | @ -643,7 +643,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: | ||||
|  |  | |||
|  | @ -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): | ||||
|  | @ -123,6 +120,18 @@ def test_save(tmp_path): | |||
|         assert test_im.size == (100, 100) | ||||
| 
 | ||||
| 
 | ||||
| def test_small_palette(tmp_path): | ||||
|     im = Image.new("P", (1, 1)) | ||||
|     colors = [0, 0, 0] | ||||
|     im.putpalette(colors) | ||||
| 
 | ||||
|     out = str(tmp_path / "temp.tga") | ||||
|     im.save(out) | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert reloaded.getpalette() == colors | ||||
| 
 | ||||
| 
 | ||||
| def test_save_wrong_mode(tmp_path): | ||||
|     im = hopper("PA") | ||||
|     out = str(tmp_path / "temp.tga") | ||||
|  |  | |||
|  | @ -84,6 +84,24 @@ class TestFileTiff: | |||
|             with Image.open("Tests/images/multipage.tiff") as im: | ||||
|                 im.load() | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         "path, sizes", | ||||
|         ( | ||||
|             ("Tests/images/hopper.tif", ()), | ||||
|             ("Tests/images/child_ifd.tiff", (16, 8)), | ||||
|             ("Tests/images/child_ifd_jpeg.tiff", (20,)), | ||||
|         ), | ||||
|     ) | ||||
|     def test_get_child_images(self, path, sizes): | ||||
|         with Image.open(path) as im: | ||||
|             ims = im.get_child_images() | ||||
| 
 | ||||
|         assert len(ims) == len(sizes) | ||||
|         for i, im in enumerate(ims): | ||||
|             w = sizes[i] | ||||
|             expected = Image.new("RGB", (w, w), "#f00") | ||||
|             assert_image_similar(im, expected, 1) | ||||
| 
 | ||||
|     def test_mac_tiff(self): | ||||
|         # Read RGBa images from macOS [@PIL136] | ||||
| 
 | ||||
|  | @ -293,14 +311,17 @@ class TestFileTiff: | |||
|             with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"): | ||||
|                 pass | ||||
| 
 | ||||
|     def test_n_frames(self): | ||||
|         for path, n_frames in [ | ||||
|             ["Tests/images/multipage-lastframe.tif", 1], | ||||
|             ["Tests/images/multipage.tiff", 3], | ||||
|         ]: | ||||
|             with Image.open(path) as im: | ||||
|                 assert im.n_frames == n_frames | ||||
|                 assert im.is_animated == (n_frames != 1) | ||||
|     @pytest.mark.parametrize( | ||||
|         "path, n_frames", | ||||
|         ( | ||||
|             ("Tests/images/multipage-lastframe.tif", 1), | ||||
|             ("Tests/images/multipage.tiff", 3), | ||||
|         ), | ||||
|     ) | ||||
|     def test_n_frames(self, path, n_frames): | ||||
|         with Image.open(path) as im: | ||||
|             assert im.n_frames == n_frames | ||||
|             assert im.is_animated == (n_frames != 1) | ||||
| 
 | ||||
|     def test_eoferror(self): | ||||
|         with Image.open("Tests/images/multipage-lastframe.tif") as im: | ||||
|  | @ -416,12 +437,12 @@ class TestFileTiff: | |||
|             len_after = len(dict(im.ifd)) | ||||
|             assert len_before == len_after + 1 | ||||
| 
 | ||||
|     def test_load_byte(self): | ||||
|         for legacy_api in [False, True]: | ||||
|             ifd = TiffImagePlugin.ImageFileDirectory_v2() | ||||
|             data = b"abc" | ||||
|             ret = ifd.load_byte(data, legacy_api) | ||||
|             assert ret == b"abc" | ||||
|     @pytest.mark.parametrize("legacy_api", (False, True)) | ||||
|     def test_load_byte(self, legacy_api): | ||||
|         ifd = TiffImagePlugin.ImageFileDirectory_v2() | ||||
|         data = b"abc" | ||||
|         ret = ifd.load_byte(data, legacy_api) | ||||
|         assert ret == b"abc" | ||||
| 
 | ||||
|     def test_load_string(self): | ||||
|         ifd = TiffImagePlugin.ImageFileDirectory_v2() | ||||
|  | @ -667,18 +688,15 @@ class TestFileTiff: | |||
|             with Image.open(outfile) as reloaded: | ||||
|                 assert_image_equal_tofile(reloaded, infile) | ||||
| 
 | ||||
|     def test_palette(self, tmp_path): | ||||
|         def roundtrip(mode): | ||||
|             outfile = str(tmp_path / "temp.tif") | ||||
|     @pytest.mark.parametrize("mode", ("P", "PA")) | ||||
|     def test_palette(self, mode, tmp_path): | ||||
|         outfile = str(tmp_path / "temp.tif") | ||||
| 
 | ||||
|             im = hopper(mode) | ||||
|             im.save(outfile) | ||||
|         im = hopper(mode) | ||||
|         im.save(outfile) | ||||
| 
 | ||||
|             with Image.open(outfile) as reloaded: | ||||
|                 assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) | ||||
| 
 | ||||
|         for mode in ["P", "PA"]: | ||||
|             roundtrip(mode) | ||||
|         with Image.open(outfile) as reloaded: | ||||
|             assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) | ||||
| 
 | ||||
|     def test_tiff_save_all(self): | ||||
|         mp = BytesIO() | ||||
|  |  | |||
|  | @ -185,6 +185,22 @@ def test_iptc(tmp_path): | |||
|         im.save(out) | ||||
| 
 | ||||
| 
 | ||||
| def test_writing_bytes_to_ascii(tmp_path): | ||||
|     im = hopper() | ||||
|     info = TiffImagePlugin.ImageFileDirectory_v2() | ||||
| 
 | ||||
|     tag = TiffTags.TAGS_V2[271] | ||||
|     assert tag.type == TiffTags.ASCII | ||||
| 
 | ||||
|     info[271] = b"test" | ||||
| 
 | ||||
|     out = str(tmp_path / "temp.tiff") | ||||
|     im.save(out, tiffinfo=info) | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert reloaded.tag_v2[271] == "test" | ||||
| 
 | ||||
| 
 | ||||
| def test_undefined_zero(tmp_path): | ||||
|     # Check that the tag has not been changed since this test was created | ||||
|     tag = TiffTags.TAGS_V2[45059] | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| import os | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile | ||||
| 
 | ||||
| from .helper import ( | ||||
|  | @ -59,23 +61,13 @@ def save_font(request, tmp_path, encoding): | |||
|     return tempname | ||||
| 
 | ||||
| 
 | ||||
| def _test_sanity(request, tmp_path, encoding): | ||||
| @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) | ||||
| def test_sanity(request, tmp_path, encoding): | ||||
|     save_font(request, tmp_path, encoding) | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity_iso8859_1(request, tmp_path): | ||||
|     _test_sanity(request, tmp_path, "iso8859-1") | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity_iso8859_2(request, tmp_path): | ||||
|     _test_sanity(request, tmp_path, "iso8859-2") | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity_cp1250(request, tmp_path): | ||||
|     _test_sanity(request, tmp_path, "cp1250") | ||||
| 
 | ||||
| 
 | ||||
| def _test_draw(request, tmp_path, encoding): | ||||
| @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) | ||||
| def test_draw(request, tmp_path, encoding): | ||||
|     tempname = save_font(request, tmp_path, encoding) | ||||
|     font = ImageFont.load(tempname) | ||||
|     im = Image.new("L", (150, 30), "white") | ||||
|  | @ -85,38 +77,19 @@ def _test_draw(request, tmp_path, encoding): | |||
|     assert_image_similar_tofile(im, charsets[encoding]["image1"], 0) | ||||
| 
 | ||||
| 
 | ||||
| def test_draw_iso8859_1(request, tmp_path): | ||||
|     _test_draw(request, tmp_path, "iso8859-1") | ||||
| 
 | ||||
| 
 | ||||
| def test_draw_iso8859_2(request, tmp_path): | ||||
|     _test_draw(request, tmp_path, "iso8859-2") | ||||
| 
 | ||||
| 
 | ||||
| def test_draw_cp1250(request, tmp_path): | ||||
|     _test_draw(request, tmp_path, "cp1250") | ||||
| 
 | ||||
| 
 | ||||
| def _test_textsize(request, tmp_path, encoding): | ||||
| @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) | ||||
| 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) | ||||
| 
 | ||||
| 
 | ||||
| def test_textsize_iso8859_1(request, tmp_path): | ||||
|     _test_textsize(request, tmp_path, "iso8859-1") | ||||
| 
 | ||||
| 
 | ||||
| def test_textsize_iso8859_2(request, tmp_path): | ||||
|     _test_textsize(request, tmp_path, "iso8859-2") | ||||
| 
 | ||||
| 
 | ||||
| def test_textsize_cp1250(request, tmp_path): | ||||
|     _test_textsize(request, tmp_path, "cp1250") | ||||
|         assert font.getlength(msg) == len(msg) * 10 | ||||
|         assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) | ||||
|  |  | |||
|  | @ -29,8 +29,9 @@ from .helper import ( | |||
| 
 | ||||
| 
 | ||||
| class TestImage: | ||||
|     def test_image_modes_success(self): | ||||
|         for mode in [ | ||||
|     @pytest.mark.parametrize( | ||||
|         "mode", | ||||
|         ( | ||||
|             "1", | ||||
|             "P", | ||||
|             "PA", | ||||
|  | @ -51,22 +52,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) | ||||
|  | @ -605,23 +602,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 | ||||
|  | @ -631,23 +627,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" | ||||
|  | @ -691,6 +686,7 @@ class TestImage: | |||
| 
 | ||||
|         im_remapped = im.remap_palette([1, 0]) | ||||
|         assert im_remapped.info["transparency"] == 1 | ||||
|         assert len(im_remapped.getpalette()) == 6 | ||||
| 
 | ||||
|         # Test unused transparency | ||||
|         im.info["transparency"] = 2 | ||||
|  |  | |||
|  | @ -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,28 @@ 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("mode", ("P", "PA")) | ||||
|     @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) | ||||
|     def test_p_putpixel_rgb_rgba(self, mode, color): | ||||
|         im = Image.new(mode, (1, 1)) | ||||
|         im.putpixel((0, 0), color) | ||||
| 
 | ||||
|         alpha = color[3] if len(color) == 4 and mode == "PA" else 255 | ||||
|         assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.skipif(cffi is None, reason="No CFFI") | ||||
|  | @ -337,12 +343,15 @@ class TestCffi(AccessTest): | |||
|                 # pixels can contain garbage if image is released | ||||
|                 assert px[i, 0] == 0 | ||||
| 
 | ||||
|     def test_p_putpixel_rgb_rgba(self): | ||||
|         for color in [(255, 0, 0), (255, 0, 0, 255)]: | ||||
|             im = Image.new("P", (1, 1), 0) | ||||
|     @pytest.mark.parametrize("mode", ("P", "PA")) | ||||
|     def test_p_putpixel_rgb_rgba(self, mode): | ||||
|         for color in [(255, 0, 0), (255, 0, 0, 127)]: | ||||
|             im = Image.new(mode, (1, 1)) | ||||
|             access = PyAccess.new(im, False) | ||||
|             access.putpixel((0, 0), color) | ||||
|             assert im.convert("RGB").getpixel((0, 0)) == (255, 0, 0) | ||||
| 
 | ||||
|             alpha = color[3] if len(color) == 4 and mode == "PA" else 255 | ||||
|             assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) | ||||
| 
 | ||||
| 
 | ||||
| class TestImagePutPixelError(AccessTest): | ||||
|  |  | |||
|  | @ -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(): | ||||
|  |  | |||
|  | @ -236,6 +236,12 @@ def test_p2pa_alpha(): | |||
|             assert im_a.getpixel((x, y)) == alpha | ||||
| 
 | ||||
| 
 | ||||
| def test_p2pa_palette(): | ||||
|     with Image.open("Tests/images/tiny.png") as im: | ||||
|         im_pa = im.convert("PA") | ||||
|     assert im_pa.getpalette() == im.getpalette() | ||||
| 
 | ||||
| 
 | ||||
| def test_matrix_illegal_conversion(): | ||||
|     # Arrange | ||||
|     im = hopper("CMYK") | ||||
|  | @ -268,36 +274,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 | ||||
|  |  | |||
|  | @ -5,90 +5,109 @@ from PIL import Image, ImageFilter | |||
| from .helper import assert_image_equal, hopper | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity(): | ||||
|     def apply_filter(filter_to_apply): | ||||
|         for mode in ["L", "RGB", "CMYK"]: | ||||
|             im = hopper(mode) | ||||
|             out = im.filter(filter_to_apply) | ||||
|             assert out.mode == im.mode | ||||
|             assert out.size == im.size | ||||
| @pytest.mark.parametrize( | ||||
|     "filter_to_apply", | ||||
|     ( | ||||
|         ImageFilter.BLUR, | ||||
|         ImageFilter.CONTOUR, | ||||
|         ImageFilter.DETAIL, | ||||
|         ImageFilter.EDGE_ENHANCE, | ||||
|         ImageFilter.EDGE_ENHANCE_MORE, | ||||
|         ImageFilter.EMBOSS, | ||||
|         ImageFilter.FIND_EDGES, | ||||
|         ImageFilter.SMOOTH, | ||||
|         ImageFilter.SMOOTH_MORE, | ||||
|         ImageFilter.SHARPEN, | ||||
|         ImageFilter.MaxFilter, | ||||
|         ImageFilter.MedianFilter, | ||||
|         ImageFilter.MinFilter, | ||||
|         ImageFilter.ModeFilter, | ||||
|         ImageFilter.GaussianBlur, | ||||
|         ImageFilter.GaussianBlur(5), | ||||
|         ImageFilter.BoxBlur(5), | ||||
|         ImageFilter.UnsharpMask, | ||||
|         ImageFilter.UnsharpMask(10), | ||||
|     ), | ||||
| ) | ||||
| @pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) | ||||
| def test_sanity(filter_to_apply, mode): | ||||
|     im = hopper(mode) | ||||
|     out = im.filter(filter_to_apply) | ||||
|     assert out.mode == im.mode | ||||
|     assert out.size == im.size | ||||
| 
 | ||||
|     apply_filter(ImageFilter.BLUR) | ||||
|     apply_filter(ImageFilter.CONTOUR) | ||||
|     apply_filter(ImageFilter.DETAIL) | ||||
|     apply_filter(ImageFilter.EDGE_ENHANCE) | ||||
|     apply_filter(ImageFilter.EDGE_ENHANCE_MORE) | ||||
|     apply_filter(ImageFilter.EMBOSS) | ||||
|     apply_filter(ImageFilter.FIND_EDGES) | ||||
|     apply_filter(ImageFilter.SMOOTH) | ||||
|     apply_filter(ImageFilter.SMOOTH_MORE) | ||||
|     apply_filter(ImageFilter.SHARPEN) | ||||
|     apply_filter(ImageFilter.MaxFilter) | ||||
|     apply_filter(ImageFilter.MedianFilter) | ||||
|     apply_filter(ImageFilter.MinFilter) | ||||
|     apply_filter(ImageFilter.ModeFilter) | ||||
|     apply_filter(ImageFilter.GaussianBlur) | ||||
|     apply_filter(ImageFilter.GaussianBlur(5)) | ||||
|     apply_filter(ImageFilter.BoxBlur(5)) | ||||
|     apply_filter(ImageFilter.UnsharpMask) | ||||
|     apply_filter(ImageFilter.UnsharpMask(10)) | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("L", "RGB", "CMYK")) | ||||
| def test_sanity_error(mode): | ||||
|     with pytest.raises(TypeError): | ||||
|         apply_filter("hello") | ||||
|         im = hopper(mode) | ||||
|         im.filter("hello") | ||||
| 
 | ||||
| 
 | ||||
| def test_crash(): | ||||
| 
 | ||||
|     # crashes on small images | ||||
|     im = Image.new("RGB", (1, 1)) | ||||
|     im.filter(ImageFilter.SMOOTH) | ||||
| 
 | ||||
|     im = Image.new("RGB", (2, 2)) | ||||
|     im.filter(ImageFilter.SMOOTH) | ||||
| 
 | ||||
|     im = Image.new("RGB", (3, 3)) | ||||
| # crashes on small images | ||||
| @pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) | ||||
| def test_crash(size): | ||||
|     im = Image.new("RGB", size) | ||||
|     im.filter(ImageFilter.SMOOTH) | ||||
| 
 | ||||
| 
 | ||||
| def test_modefilter(): | ||||
|     def modefilter(mode): | ||||
|         im = Image.new(mode, (3, 3), None) | ||||
|         im.putdata(list(range(9))) | ||||
|         # image is: | ||||
|         #   0 1 2 | ||||
|         #   3 4 5 | ||||
|         #   6 7 8 | ||||
|         mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) | ||||
|         im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0])  # mode=0 | ||||
|         mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) | ||||
|         return mod, mod2 | ||||
| 
 | ||||
|     assert modefilter("1") == (4, 0) | ||||
|     assert modefilter("L") == (4, 0) | ||||
|     assert modefilter("P") == (4, 0) | ||||
|     assert modefilter("RGB") == ((4, 0, 0), (0, 0, 0)) | ||||
| @pytest.mark.parametrize( | ||||
|     "mode, expected", | ||||
|     ( | ||||
|         ("1", (4, 0)), | ||||
|         ("L", (4, 0)), | ||||
|         ("P", (4, 0)), | ||||
|         ("RGB", ((4, 0, 0), (0, 0, 0))), | ||||
|     ), | ||||
| ) | ||||
| def test_modefilter(mode, expected): | ||||
|     im = Image.new(mode, (3, 3), None) | ||||
|     im.putdata(list(range(9))) | ||||
|     # image is: | ||||
|     #   0 1 2 | ||||
|     #   3 4 5 | ||||
|     #   6 7 8 | ||||
|     mod = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) | ||||
|     im.putdata([0, 0, 1, 2, 5, 1, 5, 2, 0])  # mode=0 | ||||
|     mod2 = im.filter(ImageFilter.ModeFilter).getpixel((1, 1)) | ||||
|     assert (mod, mod2) == expected | ||||
| 
 | ||||
| 
 | ||||
| def test_rankfilter(): | ||||
|     def rankfilter(mode): | ||||
|         im = Image.new(mode, (3, 3), None) | ||||
|         im.putdata(list(range(9))) | ||||
|         # image is: | ||||
|         #   0 1 2 | ||||
|         #   3 4 5 | ||||
|         #   6 7 8 | ||||
|         minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) | ||||
|         med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) | ||||
|         maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) | ||||
|         return minimum, med, maximum | ||||
| @pytest.mark.parametrize( | ||||
|     "mode, expected", | ||||
|     ( | ||||
|         ("1", (0, 4, 8)), | ||||
|         ("L", (0, 4, 8)), | ||||
|         ("RGB", ((0, 0, 0), (4, 0, 0), (8, 0, 0))), | ||||
|         ("I", (0, 4, 8)), | ||||
|         ("F", (0.0, 4.0, 8.0)), | ||||
|     ), | ||||
| ) | ||||
| def test_rankfilter(mode, expected): | ||||
|     im = Image.new(mode, (3, 3), None) | ||||
|     im.putdata(list(range(9))) | ||||
|     # image is: | ||||
|     #   0 1 2 | ||||
|     #   3 4 5 | ||||
|     #   6 7 8 | ||||
|     minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) | ||||
|     med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) | ||||
|     maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) | ||||
|     assert (minimum, med, maximum) == expected | ||||
| 
 | ||||
|     assert rankfilter("1") == (0, 4, 8) | ||||
|     assert rankfilter("L") == (0, 4, 8) | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) | ||||
| ) | ||||
| def test_rankfilter_error(filter): | ||||
|     with pytest.raises(ValueError): | ||||
|         rankfilter("P") | ||||
|     assert rankfilter("RGB") == ((0, 0, 0), (4, 0, 0), (8, 0, 0)) | ||||
|     assert rankfilter("I") == (0, 4, 8) | ||||
|     assert rankfilter("F") == (0.0, 4.0, 8.0) | ||||
|         im = Image.new("P", (3, 3), None) | ||||
|         im.putdata(list(range(9))) | ||||
|         # image is: | ||||
|         #   0 1 2 | ||||
|         #   3 4 5 | ||||
|         #   6 7 8 | ||||
|         im.filter(filter).getpixel((1, 1)) | ||||
| 
 | ||||
| 
 | ||||
| def test_rankfilter_properties(): | ||||
|  | @ -110,7 +129,8 @@ def test_kernel_not_enough_coefficients(): | |||
|         ImageFilter.Kernel((3, 3), (0, 0)) | ||||
| 
 | ||||
| 
 | ||||
| def test_consistency_3x3(): | ||||
| @pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) | ||||
| def test_consistency_3x3(mode): | ||||
|     with Image.open("Tests/images/hopper.bmp") as source: | ||||
|         with Image.open("Tests/images/hopper_emboss.bmp") as reference: | ||||
|             kernel = ImageFilter.Kernel( | ||||
|  | @ -125,14 +145,14 @@ def test_consistency_3x3(): | |||
|             source = source.split() * 2 | ||||
|             reference = reference.split() * 2 | ||||
| 
 | ||||
|             for mode in ["L", "LA", "RGB", "CMYK"]: | ||||
|                 assert_image_equal( | ||||
|                     Image.merge(mode, source[: len(mode)]).filter(kernel), | ||||
|                     Image.merge(mode, reference[: len(mode)]), | ||||
|                 ) | ||||
|             assert_image_equal( | ||||
|                 Image.merge(mode, source[: len(mode)]).filter(kernel), | ||||
|                 Image.merge(mode, reference[: len(mode)]), | ||||
|             ) | ||||
| 
 | ||||
| 
 | ||||
| def test_consistency_5x5(): | ||||
| @pytest.mark.parametrize("mode", ("L", "LA", "RGB", "CMYK")) | ||||
| def test_consistency_5x5(mode): | ||||
|     with Image.open("Tests/images/hopper.bmp") as source: | ||||
|         with Image.open("Tests/images/hopper_emboss_more.bmp") as reference: | ||||
|             kernel = ImageFilter.Kernel( | ||||
|  | @ -149,8 +169,7 @@ def test_consistency_5x5(): | |||
|             source = source.split() * 2 | ||||
|             reference = reference.split() * 2 | ||||
| 
 | ||||
|             for mode in ["L", "LA", "RGB", "CMYK"]: | ||||
|                 assert_image_equal( | ||||
|                     Image.merge(mode, source[: len(mode)]).filter(kernel), | ||||
|                     Image.merge(mode, reference[: len(mode)]), | ||||
|                 ) | ||||
|             assert_image_equal( | ||||
|                 Image.merge(mode, source[: len(mode)]).filter(kernel), | ||||
|                 Image.merge(mode, reference[: len(mode)]), | ||||
|             ) | ||||
|  |  | |||
|  | @ -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)) | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -38,58 +38,64 @@ gradients_image = Image.open("Tests/images/radial_gradients.png") | |||
| gradients_image.load() | ||||
| 
 | ||||
| 
 | ||||
| def test_args_factor(): | ||||
| @pytest.mark.parametrize( | ||||
|     "size, expected", | ||||
|     ( | ||||
|         (3, (4, 4)), | ||||
|         ((3, 1), (4, 10)), | ||||
|         ((1, 3), (10, 4)), | ||||
|     ), | ||||
| ) | ||||
| def test_args_factor(size, expected): | ||||
|     im = Image.new("L", (10, 10)) | ||||
| 
 | ||||
|     assert (4, 4) == im.reduce(3).size | ||||
|     assert (4, 10) == im.reduce((3, 1)).size | ||||
|     assert (10, 4) == im.reduce((1, 3)).size | ||||
| 
 | ||||
|     with pytest.raises(ValueError): | ||||
|         im.reduce(0) | ||||
|     with pytest.raises(TypeError): | ||||
|         im.reduce(2.0) | ||||
|     with pytest.raises(ValueError): | ||||
|         im.reduce((0, 10)) | ||||
|     assert expected == im.reduce(size).size | ||||
| 
 | ||||
| 
 | ||||
| def test_args_box(): | ||||
| @pytest.mark.parametrize( | ||||
|     "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) | ||||
| ) | ||||
| def test_args_factor_error(size, expected_error): | ||||
|     im = Image.new("L", (10, 10)) | ||||
| 
 | ||||
|     assert (5, 5) == im.reduce(2, (0, 0, 10, 10)).size | ||||
|     assert (1, 1) == im.reduce(2, (5, 5, 6, 6)).size | ||||
| 
 | ||||
|     with pytest.raises(TypeError): | ||||
|         im.reduce(2, "stri") | ||||
|     with pytest.raises(TypeError): | ||||
|         im.reduce(2, 2) | ||||
|     with pytest.raises(ValueError): | ||||
|         im.reduce(2, (0, 0, 11, 10)) | ||||
|     with pytest.raises(ValueError): | ||||
|         im.reduce(2, (0, 0, 10, 11)) | ||||
|     with pytest.raises(ValueError): | ||||
|         im.reduce(2, (-1, 0, 10, 10)) | ||||
|     with pytest.raises(ValueError): | ||||
|         im.reduce(2, (0, -1, 10, 10)) | ||||
|     with pytest.raises(ValueError): | ||||
|         im.reduce(2, (0, 5, 10, 5)) | ||||
|     with pytest.raises(ValueError): | ||||
|         im.reduce(2, (5, 0, 5, 10)) | ||||
|     with pytest.raises(expected_error): | ||||
|         im.reduce(size) | ||||
| 
 | ||||
| 
 | ||||
| def test_unsupported_modes(): | ||||
| @pytest.mark.parametrize( | ||||
|     "size, expected", | ||||
|     ( | ||||
|         ((0, 0, 10, 10), (5, 5)), | ||||
|         ((5, 5, 6, 6), (1, 1)), | ||||
|     ), | ||||
| ) | ||||
| def test_args_box(size, expected): | ||||
|     im = Image.new("L", (10, 10)) | ||||
|     assert expected == im.reduce(2, size).size | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "size, expected_error", | ||||
|     ( | ||||
|         ("stri", TypeError), | ||||
|         ((0, 0, 11, 10), ValueError), | ||||
|         ((0, 0, 10, 11), ValueError), | ||||
|         ((-1, 0, 10, 10), ValueError), | ||||
|         ((0, -1, 10, 10), ValueError), | ||||
|         ((0, 5, 10, 5), ValueError), | ||||
|         ((5, 0, 5, 10), ValueError), | ||||
|     ), | ||||
| ) | ||||
| def test_args_box_error(size, expected_error): | ||||
|     im = Image.new("L", (10, 10)) | ||||
|     with pytest.raises(expected_error): | ||||
|         im.reduce(2, size).size | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("P", "1", "I;16")) | ||||
| def test_unsupported_modes(mode): | ||||
|     im = Image.new("P", (10, 10)) | ||||
|     with pytest.raises(ValueError): | ||||
|         im.reduce(3) | ||||
| 
 | ||||
|     im = Image.new("1", (10, 10)) | ||||
|     with pytest.raises(ValueError): | ||||
|         im.reduce(3) | ||||
| 
 | ||||
|     im = Image.new("I;16", (10, 10)) | ||||
|     with pytest.raises(ValueError): | ||||
|         im.reduce(3) | ||||
| 
 | ||||
| 
 | ||||
| def get_image(mode): | ||||
|     mode_info = ImageMode.getmode(mode) | ||||
|  | @ -190,70 +196,76 @@ def assert_compare_images(a, b, max_average_diff, max_diff=255): | |||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| def test_mode_L(): | ||||
| @pytest.mark.parametrize("factor", remarkable_factors) | ||||
| def test_mode_L(factor): | ||||
|     im = get_image("L") | ||||
|     for factor in remarkable_factors: | ||||
|         compare_reduce_with_reference(im, factor) | ||||
|         compare_reduce_with_box(im, factor) | ||||
|     compare_reduce_with_reference(im, factor) | ||||
|     compare_reduce_with_box(im, factor) | ||||
| 
 | ||||
| 
 | ||||
| def test_mode_LA(): | ||||
| @pytest.mark.parametrize("factor", remarkable_factors) | ||||
| def test_mode_LA(factor): | ||||
|     im = get_image("LA") | ||||
|     for factor in remarkable_factors: | ||||
|         compare_reduce_with_reference(im, factor, 0.8, 5) | ||||
|     compare_reduce_with_reference(im, factor, 0.8, 5) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("factor", remarkable_factors) | ||||
| def test_mode_LA_opaque(factor): | ||||
|     im = get_image("LA") | ||||
|     # With opaque alpha, an error should be way smaller. | ||||
|     im.putalpha(Image.new("L", im.size, 255)) | ||||
|     for factor in remarkable_factors: | ||||
|         compare_reduce_with_reference(im, factor) | ||||
|         compare_reduce_with_box(im, factor) | ||||
|     compare_reduce_with_reference(im, factor) | ||||
|     compare_reduce_with_box(im, factor) | ||||
| 
 | ||||
| 
 | ||||
| def test_mode_La(): | ||||
| @pytest.mark.parametrize("factor", remarkable_factors) | ||||
| def test_mode_La(factor): | ||||
|     im = get_image("La") | ||||
|     for factor in remarkable_factors: | ||||
|         compare_reduce_with_reference(im, factor) | ||||
|         compare_reduce_with_box(im, factor) | ||||
|     compare_reduce_with_reference(im, factor) | ||||
|     compare_reduce_with_box(im, factor) | ||||
| 
 | ||||
| 
 | ||||
| def test_mode_RGB(): | ||||
| @pytest.mark.parametrize("factor", remarkable_factors) | ||||
| def test_mode_RGB(factor): | ||||
|     im = get_image("RGB") | ||||
|     for factor in remarkable_factors: | ||||
|         compare_reduce_with_reference(im, factor) | ||||
|         compare_reduce_with_box(im, factor) | ||||
|     compare_reduce_with_reference(im, factor) | ||||
|     compare_reduce_with_box(im, factor) | ||||
| 
 | ||||
| 
 | ||||
| def test_mode_RGBA(): | ||||
| @pytest.mark.parametrize("factor", remarkable_factors) | ||||
| def test_mode_RGBA(factor): | ||||
|     im = get_image("RGBA") | ||||
|     for factor in remarkable_factors: | ||||
|         compare_reduce_with_reference(im, factor, 0.8, 5) | ||||
|     compare_reduce_with_reference(im, factor, 0.8, 5) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("factor", remarkable_factors) | ||||
| def test_mode_RGBA_opaque(factor): | ||||
|     im = get_image("RGBA") | ||||
|     # With opaque alpha, an error should be way smaller. | ||||
|     im.putalpha(Image.new("L", im.size, 255)) | ||||
|     for factor in remarkable_factors: | ||||
|         compare_reduce_with_reference(im, factor) | ||||
|         compare_reduce_with_box(im, factor) | ||||
|     compare_reduce_with_reference(im, factor) | ||||
|     compare_reduce_with_box(im, factor) | ||||
| 
 | ||||
| 
 | ||||
| def test_mode_RGBa(): | ||||
| @pytest.mark.parametrize("factor", remarkable_factors) | ||||
| def test_mode_RGBa(factor): | ||||
|     im = get_image("RGBa") | ||||
|     for factor in remarkable_factors: | ||||
|         compare_reduce_with_reference(im, factor) | ||||
|         compare_reduce_with_box(im, factor) | ||||
|     compare_reduce_with_reference(im, factor) | ||||
|     compare_reduce_with_box(im, factor) | ||||
| 
 | ||||
| 
 | ||||
| def test_mode_I(): | ||||
| @pytest.mark.parametrize("factor", remarkable_factors) | ||||
| def test_mode_I(factor): | ||||
|     im = get_image("I") | ||||
|     for factor in remarkable_factors: | ||||
|         compare_reduce_with_reference(im, factor) | ||||
|         compare_reduce_with_box(im, factor) | ||||
|     compare_reduce_with_reference(im, factor) | ||||
|     compare_reduce_with_box(im, factor) | ||||
| 
 | ||||
| 
 | ||||
| def test_mode_F(): | ||||
| @pytest.mark.parametrize("factor", remarkable_factors) | ||||
| def test_mode_F(factor): | ||||
|     im = get_image("F") | ||||
|     for factor in remarkable_factors: | ||||
|         compare_reduce_with_reference(im, factor, 0, 0) | ||||
|         compare_reduce_with_box(im, factor) | ||||
|     compare_reduce_with_reference(im, factor, 0, 0) | ||||
|     compare_reduce_with_box(im, factor) | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("jpg_2000") | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -548,44 +554,48 @@ class TestCoreResampleBox: | |||
|                 # check that the difference at least that much | ||||
|                 assert_image_similar(res, im.crop(box), 20, f">>> {size} {box}") | ||||
| 
 | ||||
|     def test_skip_horizontal(self): | ||||
|     @pytest.mark.parametrize( | ||||
|         "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) | ||||
|     ) | ||||
|     def test_skip_horizontal(self, flt): | ||||
|         # Can skip resize for one dimension | ||||
|         im = hopper() | ||||
| 
 | ||||
|         for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]: | ||||
|             for size, box in [ | ||||
|                 ((40, 50), (0, 0, 40, 90)), | ||||
|                 ((40, 50), (0, 20, 40, 90)), | ||||
|                 ((40, 50), (10, 0, 50, 90)), | ||||
|                 ((40, 50), (10, 20, 50, 90)), | ||||
|             ]: | ||||
|                 res = im.resize(size, flt, box) | ||||
|                 assert res.size == size | ||||
|                 # Borders should be slightly different | ||||
|                 assert_image_similar( | ||||
|                     res, | ||||
|                     im.crop(box).resize(size, flt), | ||||
|                     0.4, | ||||
|                     f">>> {size} {box} {flt}", | ||||
|                 ) | ||||
|         for size, box in [ | ||||
|             ((40, 50), (0, 0, 40, 90)), | ||||
|             ((40, 50), (0, 20, 40, 90)), | ||||
|             ((40, 50), (10, 0, 50, 90)), | ||||
|             ((40, 50), (10, 20, 50, 90)), | ||||
|         ]: | ||||
|             res = im.resize(size, flt, box) | ||||
|             assert res.size == size | ||||
|             # Borders should be slightly different | ||||
|             assert_image_similar( | ||||
|                 res, | ||||
|                 im.crop(box).resize(size, flt), | ||||
|                 0.4, | ||||
|                 f">>> {size} {box} {flt}", | ||||
|             ) | ||||
| 
 | ||||
|     def test_skip_vertical(self): | ||||
|     @pytest.mark.parametrize( | ||||
|         "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) | ||||
|     ) | ||||
|     def test_skip_vertical(self, flt): | ||||
|         # Can skip resize for one dimension | ||||
|         im = hopper() | ||||
| 
 | ||||
|         for flt in [Image.Resampling.NEAREST, Image.Resampling.BICUBIC]: | ||||
|             for size, box in [ | ||||
|                 ((40, 50), (0, 0, 90, 50)), | ||||
|                 ((40, 50), (20, 0, 90, 50)), | ||||
|                 ((40, 50), (0, 10, 90, 60)), | ||||
|                 ((40, 50), (20, 10, 90, 60)), | ||||
|             ]: | ||||
|                 res = im.resize(size, flt, box) | ||||
|                 assert res.size == size | ||||
|                 # Borders should be slightly different | ||||
|                 assert_image_similar( | ||||
|                     res, | ||||
|                     im.crop(box).resize(size, flt), | ||||
|                     0.4, | ||||
|                     f">>> {size} {box} {flt}", | ||||
|                 ) | ||||
|         for size, box in [ | ||||
|             ((40, 50), (0, 0, 90, 50)), | ||||
|             ((40, 50), (20, 0, 90, 50)), | ||||
|             ((40, 50), (0, 10, 90, 60)), | ||||
|             ((40, 50), (20, 10, 90, 60)), | ||||
|         ]: | ||||
|             res = im.resize(size, flt, box) | ||||
|             assert res.size == size | ||||
|             # Borders should be slightly different | ||||
|             assert_image_similar( | ||||
|                 res, | ||||
|                 im.crop(box).resize(size, flt), | ||||
|                 0.4, | ||||
|                 f">>> {size} {box} {flt}", | ||||
|             ) | ||||
|  |  | |||
|  | @ -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 import Image, features | ||||
| 
 | ||||
| from .helper import assert_image_equal, hopper | ||||
|  | @ -29,19 +31,12 @@ def test_split(): | |||
|     assert split("YCbCr") == [("L", 128, 128), ("L", 128, 128), ("L", 128, 128)] | ||||
| 
 | ||||
| 
 | ||||
| def test_split_merge(): | ||||
|     def split_merge(mode): | ||||
|         return Image.merge(mode, hopper(mode).split()) | ||||
| 
 | ||||
|     assert_image_equal(hopper("1"), split_merge("1")) | ||||
|     assert_image_equal(hopper("L"), split_merge("L")) | ||||
|     assert_image_equal(hopper("I"), split_merge("I")) | ||||
|     assert_image_equal(hopper("F"), split_merge("F")) | ||||
|     assert_image_equal(hopper("P"), split_merge("P")) | ||||
|     assert_image_equal(hopper("RGB"), split_merge("RGB")) | ||||
|     assert_image_equal(hopper("RGBA"), split_merge("RGBA")) | ||||
|     assert_image_equal(hopper("CMYK"), split_merge("CMYK")) | ||||
|     assert_image_equal(hopper("YCbCr"), split_merge("YCbCr")) | ||||
| @pytest.mark.parametrize( | ||||
|     "mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr") | ||||
| ) | ||||
| def test_split_merge(mode): | ||||
|     expected = Image.merge(mode, hopper(mode).split()) | ||||
|     assert_image_equal(hopper(mode), expected) | ||||
| 
 | ||||
| 
 | ||||
| def test_split_open(tmp_path): | ||||
|  |  | |||
|  | @ -97,6 +97,28 @@ def test_load_first(): | |||
|         im.thumbnail((64, 64)) | ||||
|         assert im.size == (64, 10) | ||||
| 
 | ||||
|     # Test thumbnail(), without draft(), | ||||
|     # on an image that is large enough once load() has changed the size | ||||
|     with Image.open("Tests/images/g4_orientation_5.tif") as im: | ||||
|         im.thumbnail((590, 88), reducing_gap=None) | ||||
|         assert im.size == (590, 88) | ||||
| 
 | ||||
| 
 | ||||
| def test_load_first_unless_jpeg(): | ||||
|     # Test that thumbnail() still uses draft() for JPEG | ||||
|     with Image.open("Tests/images/hopper.jpg") as im: | ||||
|         draft = im.draft | ||||
| 
 | ||||
|         def im_draft(mode, size): | ||||
|             result = draft(mode, size) | ||||
|             assert result is not None | ||||
| 
 | ||||
|             return result | ||||
| 
 | ||||
|         im.draft = im_draft | ||||
| 
 | ||||
|         im.thumbnail((64, 64)) | ||||
| 
 | ||||
| 
 | ||||
| # valgrind test is failing with memory allocated in libjpeg | ||||
| @pytest.mark.valgrind_known_error(reason="Known Failing") | ||||
|  |  | |||
|  | @ -75,23 +75,25 @@ class TestImageTransform: | |||
| 
 | ||||
|         assert_image_equal(transformed, scaled) | ||||
| 
 | ||||
|     def test_fill(self): | ||||
|         for mode, pixel in [ | ||||
|             ["RGB", (255, 0, 0)], | ||||
|             ["RGBA", (255, 0, 0, 255)], | ||||
|             ["LA", (76, 0)], | ||||
|         ]: | ||||
|             im = hopper(mode) | ||||
|             (w, h) = im.size | ||||
|             transformed = im.transform( | ||||
|                 im.size, | ||||
|                 Image.Transform.EXTENT, | ||||
|                 (0, 0, w * 2, h * 2), | ||||
|                 Image.Resampling.BILINEAR, | ||||
|                 fillcolor="red", | ||||
|             ) | ||||
| 
 | ||||
|             assert transformed.getpixel((w - 1, h - 1)) == pixel | ||||
|     @pytest.mark.parametrize( | ||||
|         "mode, expected_pixel", | ||||
|         ( | ||||
|             ("RGB", (255, 0, 0)), | ||||
|             ("RGBA", (255, 0, 0, 255)), | ||||
|             ("LA", (76, 0)), | ||||
|         ), | ||||
|     ) | ||||
|     def test_fill(self, mode, expected_pixel): | ||||
|         im = hopper(mode) | ||||
|         (w, h) = im.size | ||||
|         transformed = im.transform( | ||||
|             im.size, | ||||
|             Image.Transform.EXTENT, | ||||
|             (0, 0, w * 2, h * 2), | ||||
|             Image.Resampling.BILINEAR, | ||||
|             fillcolor="red", | ||||
|         ) | ||||
|         assert transformed.getpixel((w - 1, h - 1)) == expected_pixel | ||||
| 
 | ||||
|     def test_mesh(self): | ||||
|         # this should be a checkerboard of halfsized hoppers in ul, lr | ||||
|  | @ -222,14 +224,12 @@ class TestImageTransform: | |||
|             with pytest.raises(ValueError): | ||||
|                 im.transform((100, 100), None) | ||||
| 
 | ||||
|     def test_unknown_resampling_filter(self): | ||||
|     @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) | ||||
|     def test_unknown_resampling_filter(self, resample): | ||||
|         with hopper() as im: | ||||
|             (w, h) = im.size | ||||
|             for resample in (Image.Resampling.BOX, "unknown"): | ||||
|                 with pytest.raises(ValueError): | ||||
|                     im.transform( | ||||
|                         (100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample | ||||
|                     ) | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) | ||||
| 
 | ||||
| 
 | ||||
| class TestImageTransformAffine: | ||||
|  | @ -239,7 +239,16 @@ class TestImageTransformAffine: | |||
|         im = hopper("RGB") | ||||
|         return im.crop((10, 20, im.width - 10, im.height - 20)) | ||||
| 
 | ||||
|     def _test_rotate(self, deg, transpose): | ||||
|     @pytest.mark.parametrize( | ||||
|         "deg, transpose", | ||||
|         ( | ||||
|             (0, None), | ||||
|             (90, Image.Transpose.ROTATE_90), | ||||
|             (180, Image.Transpose.ROTATE_180), | ||||
|             (270, Image.Transpose.ROTATE_270), | ||||
|         ), | ||||
|     ) | ||||
|     def test_rotate(self, deg, transpose): | ||||
|         im = self._test_image() | ||||
| 
 | ||||
|         angle = -math.radians(deg) | ||||
|  | @ -271,77 +280,65 @@ class TestImageTransformAffine: | |||
|             ) | ||||
|             assert_image_equal(transposed, transformed) | ||||
| 
 | ||||
|     def test_rotate_0_deg(self): | ||||
|         self._test_rotate(0, None) | ||||
| 
 | ||||
|     def test_rotate_90_deg(self): | ||||
|         self._test_rotate(90, Image.Transpose.ROTATE_90) | ||||
| 
 | ||||
|     def test_rotate_180_deg(self): | ||||
|         self._test_rotate(180, Image.Transpose.ROTATE_180) | ||||
| 
 | ||||
|     def test_rotate_270_deg(self): | ||||
|         self._test_rotate(270, Image.Transpose.ROTATE_270) | ||||
| 
 | ||||
|     def _test_resize(self, scale, epsilonscale): | ||||
|     @pytest.mark.parametrize( | ||||
|         "scale, epsilon_scale", | ||||
|         ( | ||||
|             (1.1, 6.9), | ||||
|             (1.5, 5.5), | ||||
|             (2.0, 5.5), | ||||
|             (2.3, 3.7), | ||||
|             (2.5, 3.7), | ||||
|         ), | ||||
|     ) | ||||
|     @pytest.mark.parametrize( | ||||
|         "resample,epsilon", | ||||
|         ( | ||||
|             (Image.Resampling.NEAREST, 0), | ||||
|             (Image.Resampling.BILINEAR, 2), | ||||
|             (Image.Resampling.BICUBIC, 1), | ||||
|         ), | ||||
|     ) | ||||
|     def test_resize(self, scale, epsilon_scale, resample, epsilon): | ||||
|         im = self._test_image() | ||||
| 
 | ||||
|         size_up = int(round(im.width * scale)), int(round(im.height * scale)) | ||||
|         matrix_up = [1 / scale, 0, 0, 0, 1 / scale, 0, 0, 0] | ||||
|         matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0] | ||||
| 
 | ||||
|         for resample, epsilon in [ | ||||
|         transformed = im.transform(size_up, self.transform, matrix_up, resample) | ||||
|         transformed = transformed.transform( | ||||
|             im.size, self.transform, matrix_down, resample | ||||
|         ) | ||||
|         assert_image_similar(transformed, im, epsilon * epsilon_scale) | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         "x, y, epsilon_scale", | ||||
|         ( | ||||
|             (0.1, 0, 3.7), | ||||
|             (0.6, 0, 9.1), | ||||
|             (50, 50, 0), | ||||
|         ), | ||||
|     ) | ||||
|     @pytest.mark.parametrize( | ||||
|         "resample, epsilon", | ||||
|         ( | ||||
|             (Image.Resampling.NEAREST, 0), | ||||
|             (Image.Resampling.BILINEAR, 2), | ||||
|             (Image.Resampling.BILINEAR, 1.5), | ||||
|             (Image.Resampling.BICUBIC, 1), | ||||
|         ]: | ||||
|             transformed = im.transform(size_up, self.transform, matrix_up, resample) | ||||
|             transformed = transformed.transform( | ||||
|                 im.size, self.transform, matrix_down, resample | ||||
|             ) | ||||
|             assert_image_similar(transformed, im, epsilon * epsilonscale) | ||||
| 
 | ||||
|     def test_resize_1_1x(self): | ||||
|         self._test_resize(1.1, 6.9) | ||||
| 
 | ||||
|     def test_resize_1_5x(self): | ||||
|         self._test_resize(1.5, 5.5) | ||||
| 
 | ||||
|     def test_resize_2_0x(self): | ||||
|         self._test_resize(2.0, 5.5) | ||||
| 
 | ||||
|     def test_resize_2_3x(self): | ||||
|         self._test_resize(2.3, 3.7) | ||||
| 
 | ||||
|     def test_resize_2_5x(self): | ||||
|         self._test_resize(2.5, 3.7) | ||||
| 
 | ||||
|     def _test_translate(self, x, y, epsilonscale): | ||||
|         ), | ||||
|     ) | ||||
|     def test_translate(self, x, y, epsilon_scale, resample, epsilon): | ||||
|         im = self._test_image() | ||||
| 
 | ||||
|         size_up = int(round(im.width + x)), int(round(im.height + y)) | ||||
|         matrix_up = [1, 0, -x, 0, 1, -y, 0, 0] | ||||
|         matrix_down = [1, 0, x, 0, 1, y, 0, 0] | ||||
| 
 | ||||
|         for resample, epsilon in [ | ||||
|             (Image.Resampling.NEAREST, 0), | ||||
|             (Image.Resampling.BILINEAR, 1.5), | ||||
|             (Image.Resampling.BICUBIC, 1), | ||||
|         ]: | ||||
|             transformed = im.transform(size_up, self.transform, matrix_up, resample) | ||||
|             transformed = transformed.transform( | ||||
|                 im.size, self.transform, matrix_down, resample | ||||
|             ) | ||||
|             assert_image_similar(transformed, im, epsilon * epsilonscale) | ||||
| 
 | ||||
|     def test_translate_0_1(self): | ||||
|         self._test_translate(0.1, 0, 3.7) | ||||
| 
 | ||||
|     def test_translate_0_6(self): | ||||
|         self._test_translate(0.6, 0, 9.1) | ||||
| 
 | ||||
|     def test_translate_50(self): | ||||
|         self._test_translate(50, 50, 0) | ||||
|         transformed = im.transform(size_up, self.transform, matrix_up, resample) | ||||
|         transformed = transformed.transform( | ||||
|             im.size, self.transform, matrix_down, resample | ||||
|         ) | ||||
|         assert_image_similar(transformed, im, epsilon * epsilon_scale) | ||||
| 
 | ||||
| 
 | ||||
| class TestImageTransformPerspective(TestImageTransformAffine): | ||||
|  |  | |||
|  | @ -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() | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -64,7 +64,9 @@ def test_mode_mismatch(): | |||
|         ImageDraw.ImageDraw(im, mode="L") | ||||
| 
 | ||||
| 
 | ||||
| def helper_arc(bbox, start, end): | ||||
| @pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) | ||||
| @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) | ||||
| def test_arc(bbox, start, end): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|  | @ -76,16 +78,6 @@ def helper_arc(bbox, start, end): | |||
|     assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_arc1(): | ||||
|     helper_arc(BBOX1, 0, 180) | ||||
|     helper_arc(BBOX1, 0.5, 180.4) | ||||
| 
 | ||||
| 
 | ||||
| def test_arc2(): | ||||
|     helper_arc(BBOX2, 0, 180) | ||||
|     helper_arc(BBOX2, 0.5, 180.4) | ||||
| 
 | ||||
| 
 | ||||
| def test_arc_end_le_start(): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|  | @ -192,29 +184,21 @@ def test_bitmap(): | |||
|     assert_image_equal_tofile(im, "Tests/images/imagedraw_bitmap.png") | ||||
| 
 | ||||
| 
 | ||||
| def helper_chord(mode, bbox, start, end): | ||||
| @pytest.mark.parametrize("mode", ("RGB", "L")) | ||||
| @pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) | ||||
| def test_chord(mode, bbox): | ||||
|     # Arrange | ||||
|     im = Image.new(mode, (W, H)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     expected = f"Tests/images/imagedraw_chord_{mode}.png" | ||||
| 
 | ||||
|     # Act | ||||
|     draw.chord(bbox, start, end, fill="red", outline="yellow") | ||||
|     draw.chord(bbox, 0, 180, fill="red", outline="yellow") | ||||
| 
 | ||||
|     # Assert | ||||
|     assert_image_similar_tofile(im, expected, 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_chord1(): | ||||
|     for mode in ["RGB", "L"]: | ||||
|         helper_chord(mode, BBOX1, 0, 180) | ||||
| 
 | ||||
| 
 | ||||
| def test_chord2(): | ||||
|     for mode in ["RGB", "L"]: | ||||
|         helper_chord(mode, BBOX2, 0, 180) | ||||
| 
 | ||||
| 
 | ||||
| def test_chord_width(): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|  | @ -263,7 +247,9 @@ def test_chord_too_fat(): | |||
|     assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_too_fat.png") | ||||
| 
 | ||||
| 
 | ||||
| def helper_ellipse(mode, bbox): | ||||
| @pytest.mark.parametrize("mode", ("RGB", "L")) | ||||
| @pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) | ||||
| def test_ellipse(mode, bbox): | ||||
|     # Arrange | ||||
|     im = Image.new(mode, (W, H)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|  | @ -276,16 +262,6 @@ def helper_ellipse(mode, bbox): | |||
|     assert_image_similar_tofile(im, expected, 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_ellipse1(): | ||||
|     for mode in ["RGB", "L"]: | ||||
|         helper_ellipse(mode, BBOX1) | ||||
| 
 | ||||
| 
 | ||||
| def test_ellipse2(): | ||||
|     for mode in ["RGB", "L"]: | ||||
|         helper_ellipse(mode, BBOX2) | ||||
| 
 | ||||
| 
 | ||||
| def test_ellipse_translucent(): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|  | @ -405,7 +381,8 @@ def test_ellipse_various_sizes_filled(): | |||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def helper_line(points): | ||||
| @pytest.mark.parametrize("points", (POINTS1, POINTS2)) | ||||
| def test_line(points): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|  | @ -417,14 +394,6 @@ def helper_line(points): | |||
|     assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") | ||||
| 
 | ||||
| 
 | ||||
| def test_line1(): | ||||
|     helper_line(POINTS1) | ||||
| 
 | ||||
| 
 | ||||
| def test_line2(): | ||||
|     helper_line(POINTS2) | ||||
| 
 | ||||
| 
 | ||||
| def test_shape1(): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (100, 100), "white") | ||||
|  | @ -484,7 +453,9 @@ def test_transform(): | |||
|     assert_image_equal(im, expected) | ||||
| 
 | ||||
| 
 | ||||
| def helper_pieslice(bbox, start, end): | ||||
| @pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) | ||||
| @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) | ||||
| def test_pieslice(bbox, start, end): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|  | @ -496,16 +467,6 @@ def helper_pieslice(bbox, start, end): | |||
|     assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_pieslice1(): | ||||
|     helper_pieslice(BBOX1, -92, 46) | ||||
|     helper_pieslice(BBOX1, -92.2, 46.2) | ||||
| 
 | ||||
| 
 | ||||
| def test_pieslice2(): | ||||
|     helper_pieslice(BBOX2, -92, 46) | ||||
|     helper_pieslice(BBOX2, -92.2, 46.2) | ||||
| 
 | ||||
| 
 | ||||
| def test_pieslice_width(): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|  | @ -585,7 +546,8 @@ def test_pieslice_no_spikes(): | |||
|     assert_image_equal(im, im_pre_erase) | ||||
| 
 | ||||
| 
 | ||||
| def helper_point(points): | ||||
| @pytest.mark.parametrize("points", (POINTS1, POINTS2)) | ||||
| def test_point(points): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|  | @ -597,15 +559,8 @@ def helper_point(points): | |||
|     assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") | ||||
| 
 | ||||
| 
 | ||||
| def test_point1(): | ||||
|     helper_point(POINTS1) | ||||
| 
 | ||||
| 
 | ||||
| def test_point2(): | ||||
|     helper_point(POINTS2) | ||||
| 
 | ||||
| 
 | ||||
| def helper_polygon(points): | ||||
| @pytest.mark.parametrize("points", (POINTS1, POINTS2)) | ||||
| def test_polygon(points): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|  | @ -617,28 +572,20 @@ def helper_polygon(points): | |||
|     assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") | ||||
| 
 | ||||
| 
 | ||||
| def test_polygon1(): | ||||
|     helper_polygon(POINTS1) | ||||
| 
 | ||||
| 
 | ||||
| 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(): | ||||
|  | @ -682,7 +629,8 @@ def test_polygon_translucent(): | |||
|     assert_image_equal_tofile(im, expected) | ||||
| 
 | ||||
| 
 | ||||
| def helper_rectangle(bbox): | ||||
| @pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) | ||||
| def test_rectangle(bbox): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|  | @ -694,14 +642,6 @@ def helper_rectangle(bbox): | |||
|     assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") | ||||
| 
 | ||||
| 
 | ||||
| def test_rectangle1(): | ||||
|     helper_rectangle(BBOX1) | ||||
| 
 | ||||
| 
 | ||||
| def test_rectangle2(): | ||||
|     helper_rectangle(BBOX2) | ||||
| 
 | ||||
| 
 | ||||
| def test_big_rectangle(): | ||||
|     # Test drawing a rectangle bigger than the image | ||||
|     # Arrange | ||||
|  | @ -1232,21 +1172,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") | ||||
|  | @ -1296,6 +1254,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 | ||||
|  | @ -1468,7 +1443,7 @@ def test_discontiguous_corners_polygon(): | |||
|     assert_image_similar_tofile(img, expected, 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_polygon(): | ||||
| def test_polygon2(): | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| import os.path | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, ImageDraw, ImageDraw2 | ||||
| 
 | ||||
| from .helper import ( | ||||
|  | @ -50,27 +52,19 @@ def test_sanity(): | |||
|     draw.line(list(range(10)), pen) | ||||
| 
 | ||||
| 
 | ||||
| def helper_ellipse(mode, bbox): | ||||
| @pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) | ||||
| def test_ellipse(bbox): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw2.Draw(im) | ||||
|     pen = ImageDraw2.Pen("blue", width=2) | ||||
|     brush = ImageDraw2.Brush("green") | ||||
|     expected = f"Tests/images/imagedraw_ellipse_{mode}.png" | ||||
| 
 | ||||
|     # Act | ||||
|     draw.ellipse(bbox, pen, brush) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert_image_similar_tofile(im, expected, 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_ellipse1(): | ||||
|     helper_ellipse("RGB", BBOX1) | ||||
| 
 | ||||
| 
 | ||||
| def test_ellipse2(): | ||||
|     helper_ellipse("RGB", BBOX2) | ||||
|     assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1) | ||||
| 
 | ||||
| 
 | ||||
| def test_ellipse_edge(): | ||||
|  | @ -86,7 +80,8 @@ def test_ellipse_edge(): | |||
|     assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) | ||||
| 
 | ||||
| 
 | ||||
| def helper_line(points): | ||||
| @pytest.mark.parametrize("points", (POINTS1, POINTS2)) | ||||
| def test_line(points): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw2.Draw(im) | ||||
|  | @ -99,14 +94,6 @@ def helper_line(points): | |||
|     assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") | ||||
| 
 | ||||
| 
 | ||||
| def test_line1_pen(): | ||||
|     helper_line(POINTS1) | ||||
| 
 | ||||
| 
 | ||||
| def test_line2_pen(): | ||||
|     helper_line(POINTS2) | ||||
| 
 | ||||
| 
 | ||||
| def test_line_pen_as_brush(): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|  | @ -122,7 +109,8 @@ def test_line_pen_as_brush(): | |||
|     assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") | ||||
| 
 | ||||
| 
 | ||||
| def helper_polygon(points): | ||||
| @pytest.mark.parametrize("points", (POINTS1, POINTS2)) | ||||
| def test_polygon(points): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw2.Draw(im) | ||||
|  | @ -136,15 +124,8 @@ def helper_polygon(points): | |||
|     assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") | ||||
| 
 | ||||
| 
 | ||||
| def test_polygon1(): | ||||
|     helper_polygon(POINTS1) | ||||
| 
 | ||||
| 
 | ||||
| def test_polygon2(): | ||||
|     helper_polygon(POINTS2) | ||||
| 
 | ||||
| 
 | ||||
| def helper_rectangle(bbox): | ||||
| @pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) | ||||
| def test_rectangle(bbox): | ||||
|     # Arrange | ||||
|     im = Image.new("RGB", (W, H)) | ||||
|     draw = ImageDraw2.Draw(im) | ||||
|  | @ -158,14 +139,6 @@ def helper_rectangle(bbox): | |||
|     assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") | ||||
| 
 | ||||
| 
 | ||||
| def test_rectangle1(): | ||||
|     helper_rectangle(BBOX1) | ||||
| 
 | ||||
| 
 | ||||
| def test_rectangle2(): | ||||
|     helper_rectangle(BBOX2) | ||||
| 
 | ||||
| 
 | ||||
| def test_big_rectangle(): | ||||
|     # Test drawing a rectangle bigger than the image | ||||
|     # Arrange | ||||
|  | @ -205,7 +178,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 +196,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") | ||||
|  |  | |||
|  | @ -1,3 +1,5 @@ | |||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, ImageEnhance | ||||
| 
 | ||||
| from .helper import assert_image_equal, hopper | ||||
|  | @ -39,17 +41,17 @@ def _check_alpha(im, original, op, amount): | |||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def test_alpha(): | ||||
| @pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) | ||||
| def test_alpha(op): | ||||
|     # Issue https://github.com/python-pillow/Pillow/issues/899 | ||||
|     # Is alpha preserved through image enhancement? | ||||
| 
 | ||||
|     original = _half_transparent_image() | ||||
| 
 | ||||
|     for op in ["Color", "Brightness", "Contrast", "Sharpness"]: | ||||
|         for amount in [0, 0.5, 1.0]: | ||||
|             _check_alpha( | ||||
|                 getattr(ImageEnhance, op)(original).enhance(amount), | ||||
|                 original, | ||||
|                 op, | ||||
|                 amount, | ||||
|             ) | ||||
|     for amount in [0, 0.5, 1.0]: | ||||
|         _check_alpha( | ||||
|             getattr(ImageEnhance, op)(original).enhance(amount), | ||||
|             original, | ||||
|             op, | ||||
|             amount, | ||||
|         ) | ||||
|  |  | |||
|  | @ -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(): | ||||
|  |  | |||
|  | @ -65,14 +65,16 @@ def create_lut(): | |||
| 
 | ||||
| 
 | ||||
| # create_lut() | ||||
| def test_lut(): | ||||
|     for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): | ||||
|         lb = ImageMorph.LutBuilder(op_name=op) | ||||
|         assert lb.get_lut() is None | ||||
| @pytest.mark.parametrize( | ||||
|     "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge") | ||||
| ) | ||||
| def test_lut(op): | ||||
|     lb = ImageMorph.LutBuilder(op_name=op) | ||||
|     assert lb.get_lut() is None | ||||
| 
 | ||||
|         lut = lb.build_lut() | ||||
|         with open(f"Tests/images/{op}.lut", "rb") as f: | ||||
|             assert lut == bytearray(f.read()) | ||||
|     lut = lb.build_lut() | ||||
|     with open(f"Tests/images/{op}.lut", "rb") as f: | ||||
|         assert lut == bytearray(f.read()) | ||||
| 
 | ||||
| 
 | ||||
| def test_no_operator_loaded(): | ||||
|  |  | |||
|  | @ -110,6 +110,16 @@ def test_contain(new_size): | |||
|     assert new_im.size == (256, 256) | ||||
| 
 | ||||
| 
 | ||||
| def test_contain_round(): | ||||
|     im = Image.new("1", (43, 63), 1) | ||||
|     new_im = ImageOps.contain(im, (5, 7)) | ||||
|     assert new_im.width == 5 | ||||
| 
 | ||||
|     im = Image.new("1", (63, 43), 1) | ||||
|     new_im = ImageOps.contain(im, (7, 5)) | ||||
|     assert new_im.height == 5 | ||||
| 
 | ||||
| 
 | ||||
| def test_pad(): | ||||
|     # Same ratio | ||||
|     im = hopper() | ||||
|  | @ -130,6 +140,30 @@ def test_pad(): | |||
|             ) | ||||
| 
 | ||||
| 
 | ||||
| def test_pad_round(): | ||||
|     im = Image.new("1", (1, 1), 1) | ||||
|     new_im = ImageOps.pad(im, (4, 1)) | ||||
|     assert new_im.load()[2, 0] == 1 | ||||
| 
 | ||||
|     new_im = ImageOps.pad(im, (1, 4)) | ||||
|     assert new_im.load()[0, 2] == 1 | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", ("P", "PA")) | ||||
| def test_palette(mode): | ||||
|     im = hopper(mode) | ||||
| 
 | ||||
|     # Expand | ||||
|     expanded_im = ImageOps.expand(im) | ||||
|     assert_image_equal(im.convert("RGB"), expanded_im.convert("RGB")) | ||||
| 
 | ||||
|     # Pad | ||||
|     padded_im = ImageOps.pad(im, (256, 128), centering=(0, 0)) | ||||
|     assert_image_equal( | ||||
|         im.convert("RGB"), padded_im.convert("RGB").crop((0, 0, 128, 128)) | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def test_pil163(): | ||||
|     # Division by zero in equalize if < 255 pixels in image (@PIL163) | ||||
| 
 | ||||
|  | @ -345,11 +379,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 | ||||
|  |  | |||
|  | @ -45,10 +45,10 @@ def test_viewer_show(order): | |||
|     not on_ci() or is_win32(), | ||||
|     reason="Only run on CIs; hangs on Windows CIs", | ||||
| ) | ||||
| def test_show(): | ||||
|     for mode in ("1", "I;16", "LA", "RGB", "RGBA"): | ||||
|         im = hopper(mode) | ||||
|         assert ImageShow.show(im) | ||||
| @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) | ||||
| def test_show(mode): | ||||
|     im = hopper(mode) | ||||
|     assert ImageShow.show(im) | ||||
| 
 | ||||
| 
 | ||||
| def test_show_without_viewers(): | ||||
|  | @ -70,12 +70,12 @@ def test_viewer(): | |||
|         viewer.get_command(None) | ||||
| 
 | ||||
| 
 | ||||
| def test_viewers(): | ||||
|     for viewer in ImageShow._viewers: | ||||
|         try: | ||||
|             viewer.get_command("test.jpg") | ||||
|         except NotImplementedError: | ||||
|             pass | ||||
| @pytest.mark.parametrize("viewer", ImageShow._viewers) | ||||
| def test_viewers(viewer): | ||||
|     try: | ||||
|         viewer.get_command("test.jpg") | ||||
|     except NotImplementedError: | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| def test_ipythonviewer(): | ||||
|  | @ -95,14 +95,14 @@ def test_ipythonviewer(): | |||
|     not on_ci() or is_win32(), | ||||
|     reason="Only run on CIs; hangs on Windows CIs", | ||||
| ) | ||||
| def test_file_deprecated(tmp_path): | ||||
| @pytest.mark.parametrize("viewer", ImageShow._viewers) | ||||
| def test_file_deprecated(tmp_path, viewer): | ||||
|     f = str(tmp_path / "temp.jpg") | ||||
|     for viewer in ImageShow._viewers: | ||||
|         hopper().save(f) | ||||
|         with pytest.warns(DeprecationWarning): | ||||
|             try: | ||||
|                 viewer.show_file(file=f) | ||||
|             except NotImplementedError: | ||||
|                 pass | ||||
|         with pytest.raises(TypeError): | ||||
|             viewer.show_file() | ||||
|     hopper().save(f) | ||||
|     with pytest.warns(DeprecationWarning): | ||||
|         try: | ||||
|             viewer.show_file(file=f) | ||||
|         except NotImplementedError: | ||||
|             pass | ||||
|     with pytest.raises(TypeError): | ||||
|         viewer.show_file() | ||||
|  |  | |||
|  | @ -54,32 +54,39 @@ def test_kw(): | |||
|     assert im is None | ||||
| 
 | ||||
| 
 | ||||
| def test_photoimage(): | ||||
|     for mode in TK_MODES: | ||||
|         # test as image: | ||||
|         im = hopper(mode) | ||||
| @pytest.mark.parametrize("mode", TK_MODES) | ||||
| def test_photoimage(mode): | ||||
|     # test as image: | ||||
|     im = hopper(mode) | ||||
| 
 | ||||
|         # this should not crash | ||||
|     # this should not crash | ||||
|     im_tk = ImageTk.PhotoImage(im) | ||||
| 
 | ||||
|     assert im_tk.width() == im.width | ||||
|     assert im_tk.height() == im.height | ||||
| 
 | ||||
|     reloaded = ImageTk.getimage(im_tk) | ||||
|     assert_image_equal(reloaded, im.convert("RGBA")) | ||||
| 
 | ||||
| 
 | ||||
| def test_photoimage_apply_transparency(): | ||||
|     with Image.open("Tests/images/pil123p.png") as im: | ||||
|         im_tk = ImageTk.PhotoImage(im) | ||||
| 
 | ||||
|         assert im_tk.width() == im.width | ||||
|         assert im_tk.height() == im.height | ||||
| 
 | ||||
|         reloaded = ImageTk.getimage(im_tk) | ||||
|         assert_image_equal(reloaded, im.convert("RGBA")) | ||||
| 
 | ||||
| 
 | ||||
| def test_photoimage_blank(): | ||||
| @pytest.mark.parametrize("mode", TK_MODES) | ||||
| def test_photoimage_blank(mode): | ||||
|     # test a image using mode/size: | ||||
|     for mode in TK_MODES: | ||||
|         im_tk = ImageTk.PhotoImage(mode, (100, 100)) | ||||
|     im_tk = ImageTk.PhotoImage(mode, (100, 100)) | ||||
| 
 | ||||
|         assert im_tk.width() == 100 | ||||
|         assert im_tk.height() == 100 | ||||
|     assert im_tk.width() == 100 | ||||
|     assert im_tk.height() == 100 | ||||
| 
 | ||||
|         im = Image.new(mode, (100, 100)) | ||||
|         reloaded = ImageTk.getimage(im_tk) | ||||
|         assert_image_equal(reloaded.convert(mode), im) | ||||
|     im = Image.new(mode, (100, 100)) | ||||
|     reloaded = ImageTk.getimage(im_tk) | ||||
|     assert_image_equal(reloaded.convert(mode), im) | ||||
| 
 | ||||
| 
 | ||||
| def test_box_deprecation(): | ||||
|  |  | |||
|  | @ -1,3 +1,5 @@ | |||
| import pytest | ||||
| 
 | ||||
| from PIL import Image | ||||
| 
 | ||||
| from .helper import hopper | ||||
|  | @ -20,65 +22,56 @@ def verify(im1): | |||
|             ), f"got {repr(p1)} from mode {im1.mode} at {xy}, expected {repr(p2)}" | ||||
| 
 | ||||
| 
 | ||||
| def test_basic(tmp_path): | ||||
| @pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I")) | ||||
| def test_basic(tmp_path, mode): | ||||
|     # PIL 1.1 has limited support for 16-bit image data.  Check that | ||||
|     # create/copy/transform and save works as expected. | ||||
| 
 | ||||
|     def basic(mode): | ||||
|     im_in = original.convert(mode) | ||||
|     verify(im_in) | ||||
| 
 | ||||
|         im_in = original.convert(mode) | ||||
|         verify(im_in) | ||||
|     w, h = im_in.size | ||||
| 
 | ||||
|         w, h = im_in.size | ||||
|     im_out = im_in.copy() | ||||
|     verify(im_out)  # copy | ||||
| 
 | ||||
|         im_out = im_in.copy() | ||||
|         verify(im_out)  # copy | ||||
|     im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) | ||||
|     verify(im_out)  # transform | ||||
| 
 | ||||
|         im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) | ||||
|         verify(im_out)  # transform | ||||
|     filename = str(tmp_path / "temp.im") | ||||
|     im_in.save(filename) | ||||
| 
 | ||||
|         filename = str(tmp_path / "temp.im") | ||||
|         im_in.save(filename) | ||||
| 
 | ||||
|         with Image.open(filename) as im_out: | ||||
| 
 | ||||
|             verify(im_in) | ||||
|             verify(im_out) | ||||
| 
 | ||||
|         im_out = im_in.crop((0, 0, w, h)) | ||||
|         verify(im_out) | ||||
| 
 | ||||
|         im_out = Image.new(mode, (w, h), None) | ||||
|         im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0)) | ||||
|         im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0)) | ||||
|     with Image.open(filename) as im_out: | ||||
| 
 | ||||
|         verify(im_in) | ||||
|         verify(im_out) | ||||
| 
 | ||||
|         im_in = Image.new(mode, (1, 1), 1) | ||||
|         assert im_in.getpixel((0, 0)) == 1 | ||||
|     im_out = im_in.crop((0, 0, w, h)) | ||||
|     verify(im_out) | ||||
| 
 | ||||
|         im_in.putpixel((0, 0), 2) | ||||
|         assert im_in.getpixel((0, 0)) == 2 | ||||
|     im_out = Image.new(mode, (w, h), None) | ||||
|     im_out.paste(im_in.crop((0, 0, w // 2, h)), (0, 0)) | ||||
|     im_out.paste(im_in.crop((w // 2, 0, w, h)), (w // 2, 0)) | ||||
| 
 | ||||
|         if mode == "L": | ||||
|             maximum = 255 | ||||
|         else: | ||||
|             maximum = 32767 | ||||
|     verify(im_in) | ||||
|     verify(im_out) | ||||
| 
 | ||||
|         im_in = Image.new(mode, (1, 1), 256) | ||||
|         assert im_in.getpixel((0, 0)) == min(256, maximum) | ||||
|     im_in = Image.new(mode, (1, 1), 1) | ||||
|     assert im_in.getpixel((0, 0)) == 1 | ||||
| 
 | ||||
|         im_in.putpixel((0, 0), 512) | ||||
|         assert im_in.getpixel((0, 0)) == min(512, maximum) | ||||
|     im_in.putpixel((0, 0), 2) | ||||
|     assert im_in.getpixel((0, 0)) == 2 | ||||
| 
 | ||||
|     basic("L") | ||||
|     if mode == "L": | ||||
|         maximum = 255 | ||||
|     else: | ||||
|         maximum = 32767 | ||||
| 
 | ||||
|     basic("I;16") | ||||
|     basic("I;16B") | ||||
|     basic("I;16L") | ||||
|     im_in = Image.new(mode, (1, 1), 256) | ||||
|     assert im_in.getpixel((0, 0)) == min(256, maximum) | ||||
| 
 | ||||
|     basic("I") | ||||
|     im_in.putpixel((0, 0), 512) | ||||
|     assert im_in.getpixel((0, 0)) == min(512, maximum) | ||||
| 
 | ||||
| 
 | ||||
| def test_tobytes(): | ||||
|  |  | |||
|  | @ -137,19 +137,9 @@ def test_save_tiff_uint16(): | |||
|     assert img_px[0, 0] == pixel_value | ||||
| 
 | ||||
| 
 | ||||
| def test_to_array(): | ||||
|     def _to_array(mode, dtype): | ||||
|         img = hopper(mode) | ||||
| 
 | ||||
|         # Resize to non-square | ||||
|         img = img.crop((3, 0, 124, 127)) | ||||
|         assert img.size == (121, 127) | ||||
| 
 | ||||
|         np_img = numpy.array(img) | ||||
|         _test_img_equals_nparray(img, np_img) | ||||
|         assert np_img.dtype == dtype | ||||
| 
 | ||||
|     modes = [ | ||||
| @pytest.mark.parametrize( | ||||
|     "mode, dtype", | ||||
|     ( | ||||
|         ("L", numpy.uint8), | ||||
|         ("I", numpy.int32), | ||||
|         ("F", numpy.float32), | ||||
|  | @ -163,10 +153,18 @@ def test_to_array(): | |||
|         ("I;16B", ">u2"), | ||||
|         ("I;16L", "<u2"), | ||||
|         ("HSV", numpy.uint8), | ||||
|     ] | ||||
|     ), | ||||
| ) | ||||
| def test_to_array(mode, dtype): | ||||
|     img = hopper(mode) | ||||
| 
 | ||||
|     for mode in modes: | ||||
|         _to_array(*mode) | ||||
|     # Resize to non-square | ||||
|     img = img.crop((3, 0, 124, 127)) | ||||
|     assert img.size == (121, 127) | ||||
| 
 | ||||
|     np_img = numpy.array(img) | ||||
|     _test_img_equals_nparray(img, np_img) | ||||
|     assert np_img.dtype == dtype | ||||
| 
 | ||||
| 
 | ||||
| def test_point_lut(): | ||||
|  |  | |||