diff --git a/.appveyor.yml b/.appveyor.yml index 0cf1d5a9e..1cca224ab 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -10,29 +10,29 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:/Python39 + - PYTHON: C:/Python310 ARCHITECTURE: x86 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - - PYTHON: C:/Python36-x64 + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 + - PYTHON: C:/Python37-x64 ARCHITECTURE: x64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 install: -- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip +- '%PYTHON%\%EXECUTABLE% --version' +- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip - 7z x pillow-depends.zip -oc:\ -- mv c:\pillow-depends-master c:\pillow-depends +- 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\gs9540w32.exe /S -- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.54.0\bin;%PATH% +- ..\pillow-depends\gs9561w32.exe /S +- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.56.1\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ c:\pillow\winbuild\build\build_dep_all.cmd $host.SetShouldExit(0) - path C:\pillow\winbuild\build\bin;%PATH% -- '%PYTHON%\%EXECUTABLE% -m pip install -U setuptools' build_script: - ps: | @@ -43,7 +43,7 @@ build_script: test_script: - cd c:\pillow -- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov' +- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout' - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% - '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' - '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' @@ -84,7 +84,7 @@ deploy: artifact: /.*egg|wheel/ on: APPVEYOR_REPO_NAME: python-pillow/Pillow - branch: master + branch: main deploy: YES diff --git a/.ci/after_success.sh b/.ci/after_success.sh index ff91b481e..53832c573 100755 --- a/.ci/after_success.sh +++ b/.ci/after_success.sh @@ -1,7 +1,7 @@ #!/bin/bash # gather the coverage data -pip3 install codecov +python3 -m pip install codecov if [[ $MATRIX_DOCKER ]]; then coverage xml --ignore-errors else diff --git a/.ci/install.sh b/.ci/install.sh index 0f3a36bc4..efc57a641 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -19,7 +19,7 @@ set -e sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ - cmake imagemagick libharfbuzz-dev libfribidi-dev + cmake meson imagemagick libharfbuzz-dev libfribidi-dev python3 -m pip install --upgrade pip python3 -m pip install --upgrade wheel @@ -32,8 +32,7 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma python3 -m pip install test-image-results -# TODO Remove condition when numpy supports 3.10 -if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi +python3 -m pip install numpy # PyQt5 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 35bd47be8..bc9587744 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -4,13 +4,13 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v ## Bug fixes, feature additions, etc. -Please send a pull request to the master branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil +Please send a pull request to the `main` branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil - Fork the Pillow repository. -- Create a branch from master. +- Create a branch from `main`. - Develop bug fixes, features, tests, etc. - Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests. -- Create a pull request to pull the changes from your branch to the Pillow master. +- Create a pull request to pull the changes from your branch to the Pillow `main`. ### Guidelines @@ -18,7 +18,7 @@ Please send a pull request to the master branch. Please include [documentation]( - Provide tests for any newly added code. - Follow PEP 8. - When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. -- Include [release notes](https://github.com/python-pillow/Pillow/tree/master/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. +- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. ## Reporting Issues @@ -35,4 +35,4 @@ The best reproductions are self-contained scripts with minimal dependencies. If ## Security vulnerabilities -Please see our [security policy](https://github.com/python-pillow/Pillow/blob/master/.github/SECURITY.md). +Please see our [security policy](https://github.com/python-pillow/Pillow/blob/main/.github/SECURITY.md). diff --git a/.github/mergify.yml b/.github/mergify.yml index 4b8b113d3..8b289bda6 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -7,6 +7,7 @@ pull_request_rules: - status-success=Test Successful - status-success=Docker Test Successful - status-success=Windows Test Successful + - status-success=MinGW Test Successful - status-success=continuous-integration/appveyor/pr actions: merge: diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 9fe8f774f..0e0abaf95 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -1,4 +1,5 @@ name: CIFuzz + on: push: paths: @@ -8,6 +9,7 @@ on: paths: - "**.c" - "**.h" + workflow_dispatch: jobs: Fuzzing: @@ -29,13 +31,13 @@ jobs: language: python dry-run: false - name: Upload New Crash - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() && steps.build.outcome == 'success' with: name: artifacts path: ./out/artifacts - name: Upload Legacy Crash - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: steps.run.outcome == 'success' with: name: crash diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bddeb6150..4540fb5af 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,6 @@ name: Lint -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: build: @@ -10,15 +10,7 @@ jobs: name: Lint steps: - - uses: actions/checkout@v2 - - - name: pip cache - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: lint-pip-${{ hashFiles('**/setup.py') }} - restore-keys: | - lint-pip- + - uses: actions/checkout@v3 - name: pre-commit cache uses: actions/cache@v2 @@ -29,9 +21,11 @@ jobs: lint-pre-commit- - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: - python-version: 3.8 + python-version: "3.10" + cache: pip + cache-dependency-path: "setup.py" - name: Build system information run: python3 .github/workflows/system-info.py @@ -45,4 +39,3 @@ jobs: run: tox -e lint env: PRE_COMMIT_COLOR: always - diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 52456597b..ad66117b1 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -4,14 +4,15 @@ on: push: # branches to consider in the event; optional, defaults to all branches: - - master + - main + workflow_dispatch: jobs: update_release_draft: if: github.repository == 'python-pillow/Pillow' runs-on: ubuntu-latest steps: - # Drafts your next release notes as pull requests are merged into "master" + # Drafts your next release notes as pull requests are merged into "main" - uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..cc5e0d488 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,27 @@ +name: Close stale issues + +on: + schedule: + - cron: "10 0 * * *" + workflow_dispatch: + +permissions: + issues: write + +jobs: + stale: + if: github.repository_owner == 'python-pillow' + + runs-on: ubuntu-latest + + steps: + - name: "Check issues" + uses: actions/stale@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + only-labels: "Awaiting OP Action" + close-issue-message: "Closing this issue as no feedback has been received." + days-before-stale: 7 + days-before-issue-close: 0 + days-before-pr-close: -1 + labels-to-remove-when-unstale: "Awaiting OP Action" diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 8274549d4..fc4667387 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -1,6 +1,6 @@ name: Test Docker -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: build: @@ -19,14 +19,16 @@ jobs: amazon-2-amd64, arch, centos-7-amd64, - centos-8-amd64, + centos-stream-8-amd64, + centos-stream-9-amd64, debian-10-buster-x86, - fedora-33-amd64, - fedora-34-amd64, + debian-11-bullseye-x86, + fedora-35-amd64, + gentoo, ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, ] - dockerTag: [master] + dockerTag: [main] include: - docker: "ubuntu-20.04-focal-arm64v8" qemu-arch: "aarch64" @@ -38,7 +40,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build system information run: python3 .github/workflows/system-info.py diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml new file mode 100644 index 000000000..7b5cc8a97 --- /dev/null +++ b/.github/workflows/test-mingw.yml @@ -0,0 +1,85 @@ +name: Test MinGW + +on: [push, pull_request, workflow_dispatch] + +jobs: + build: + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + mingw: ["MINGW32", "MINGW64"] + include: + - mingw: "MINGW32" + name: "MSYS2 MinGW 32-bit" + package: "mingw-w64-i686" + - mingw: "MINGW64" + name: "MSYS2 MinGW 64-bit" + package: "mingw-w64-x86_64" + + defaults: + run: + shell: bash.exe --login -eo pipefail "{0}" + env: + MSYSTEM: ${{ matrix.mingw }} + CHERE_INVOKING: 1 + + timeout-minutes: 30 + name: ${{ matrix.name }} + + steps: + - name: Checkout Pillow + uses: actions/checkout@v3 + + - name: Set up shell + run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH + shell: pwsh + + - name: Install dependencies + run: | + pacman -S --noconfirm \ + ${{ matrix.package }}-python3-cffi \ + ${{ matrix.package }}-python3-numpy \ + ${{ matrix.package }}-python3-olefile \ + ${{ matrix.package }}-python3-pip \ + ${{ matrix.package }}-python-pyqt6 \ + ${{ matrix.package }}-python3-setuptools \ + ${{ matrix.package }}-freetype \ + ${{ matrix.package }}-gcc \ + ${{ matrix.package }}-ghostscript \ + ${{ matrix.package }}-lcms2 \ + ${{ matrix.package }}-libimagequant \ + ${{ matrix.package }}-libjpeg-turbo \ + ${{ matrix.package }}-libraqm \ + ${{ matrix.package }}-libtiff \ + ${{ matrix.package }}-libwebp \ + ${{ matrix.package }}-openjpeg2 \ + subversion + + python3 -m pip install pyroma pytest pytest-cov pytest-timeout + + pushd depends && ./install_extra_test_images.sh && popd + + - name: Build Pillow + run: CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . + + - name: Test Pillow + run: | + python3 selftest.py --installed + python3 -c "from PIL import Image" + 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 }} + + success: + needs: build + runs-on: ubuntu-latest + name: MinGW Test Successful + steps: + - name: Success + run: echo MinGW Test Successful diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 7b8474d0f..21a2b469e 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -11,6 +11,7 @@ on: paths: - "**.c" - "**.h" + workflow_dispatch: jobs: build: @@ -22,12 +23,12 @@ jobs: docker: [ ubuntu-20.04-focal-amd64-valgrind, ] - dockerTag: [master] + dockerTag: [main] name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build system information run: python3 .github/workflows/system-info.py @@ -42,11 +43,3 @@ jobs: sudo chown -R 1000 $GITHUB_WORKSPACE docker run --name pillow_container -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} sudo chown -R runner $GITHUB_WORKSPACE - - success: - needs: build - runs-on: ubuntu-latest - name: Valgrind Test Successful - steps: - - name: Success - run: echo Valgrind Test Successful diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index ce04ba5ca..6ed8bb0c5 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -1,52 +1,44 @@ name: Test Windows -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: build: - runs-on: windows-2019 + runs-on: windows-latest strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev"] + python-version: ["3.7", "3.8", "3.9", "3.10"] architecture: ["x86", "x64"] include: - # PyPy3.6 only ships 32-bit binaries for Windows - - python-version: "pypy-3.6" - architecture: "x86" # PyPy 7.3.4+ only ships 64-bit binaries for Windows - python-version: "pypy-3.7" architecture: "x64" + - python-version: "pypy-3.8" + architecture: "x64" + timeout-minutes: 30 name: Python ${{ matrix.python-version }} ${{ matrix.architecture }} steps: - name: Checkout Pillow - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Checkout cached dependencies - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: python-pillow/pillow-depends path: winbuild\depends - - name: Cache pip - uses: actions/cache@v2 - with: - path: ~\AppData\Local\pip\Cache - key: - ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.architecture }}-${{ hashFiles('**/.github/workflows/test-windows.yml') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.architecture }}- - ${{ runner.os }}-${{ matrix.python-version }}- - # sets env: pythonLocation - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.architecture }} + cache: pip + cache-dependency-path: ".github/workflows/test-windows.yml" - name: Print build system information run: python .github/workflows/system-info.py @@ -60,8 +52,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\gs9540w32.exe /S - echo "C:\Program Files (x86)\gs\gs9.54.0\bin" >> $env:GITHUB_PATH + winbuild\depends\gs9561w32.exe /S + echo "C:\Program Files (x86)\gs\gs9.56.1\bin" >> $env:GITHUB_PATH xcopy /S /Y winbuild\depends\test_images\* Tests\images\ @@ -140,15 +132,16 @@ jobs: - name: Build Pillow run: | $FLAGS="" - if ('${{ github.event_name }}' -eq 'push') { $FLAGS="--disable-imagequant" } + if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS="--disable-imagequant" } & winbuild\build\build_pillow.cmd $FLAGS install & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh - # failing with PyPy3 + # skip PyPy for speed - name: Enable heap verification if: "!contains(matrix.python-version, 'pypy')" - run: "& 'C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x86\\gflags.exe' /p /enable $env:pythonLocation\\python.exe" + run: | + & reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f - name: Test Pillow run: | @@ -163,7 +156,7 @@ jobs: shell: bash - name: Upload errors - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() with: name: errors @@ -183,92 +176,20 @@ jobs: - name: Build wheel id: wheel - if: "github.event_name == 'push'" + if: "github.event_name != 'pull_request'" run: | for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo ::set-output name=dist::dist-%%a winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel shell: cmd - - uses: actions/upload-artifact@v2 - if: "github.event_name == 'push'" + - uses: actions/upload-artifact@v3 + if: "github.event_name != 'pull_request'" with: name: ${{ steps.wheel.outputs.dist }} path: dist\*.whl - msys: - runs-on: windows-2019 - - strategy: - fail-fast: false - matrix: - mingw: ["MINGW32", "MINGW64"] - include: - - mingw: "MINGW32" - name: "MSYS2 MinGW 32-bit" - package: "mingw-w64-i686" - - mingw: "MINGW64" - name: "MSYS2 MinGW 64-bit" - package: "mingw-w64-x86_64" - - defaults: - run: - shell: bash.exe --login -eo pipefail "{0}" - env: - MSYSTEM: ${{ matrix.mingw }} - CHERE_INVOKING: 1 - - timeout-minutes: 30 - name: ${{ matrix.name }} - - steps: - - uses: actions/checkout@v2 - - - name: Set up shell - run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH - shell: pwsh - - - name: Install Dependencies - run: | - pacman -S --noconfirm \ - ${{ matrix.package }}-python3-cffi \ - ${{ matrix.package }}-python3-numpy \ - ${{ matrix.package }}-python3-olefile \ - ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-pyqt5 \ - ${{ matrix.package }}-python3-setuptools \ - ${{ matrix.package }}-freetype \ - ${{ matrix.package }}-ghostscript \ - ${{ matrix.package }}-lcms2 \ - ${{ matrix.package }}-libimagequant \ - ${{ matrix.package }}-libjpeg-turbo \ - ${{ matrix.package }}-libraqm \ - ${{ matrix.package }}-libtiff \ - ${{ matrix.package }}-libwebp \ - ${{ matrix.package }}-openjpeg2 \ - subversion - - python3 -m pip install pyroma pytest pytest-cov - - pushd depends && ./install_extra_test_images.sh && popd - - - name: Build Pillow - run: CFLAGS="-coverage" python3 setup.py build_ext install - - - name: Test Pillow - run: | - python3 selftest.py --installed - python3 -c "from PIL import Image" - 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 }} - success: - needs: [build, msys] + needs: build runs-on: ubuntu-latest name: Windows Test Successful steps: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cd85bc537..7b13addfd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ name: Test -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: build: @@ -9,54 +9,41 @@ jobs: fail-fast: false matrix: os: [ + "macos-latest", "ubuntu-latest", - "macOS-latest", ] python-version: [ + "pypy-3.8", "pypy-3.7", - "pypy-3.6", - "3.10-dev", + "3.10", "3.9", "3.8", "3.7", - "3.6", ] include: - - python-version: "3.6" + - python-version: "3.7" PYTHONOPTIMIZE: 1 REVERSE: "--reverse" - - python-version: "3.7" + - python-version: "3.8" PYTHONOPTIMIZE: 2 # Include new variables for Codecov - os: ubuntu-latest codecov-flag: GHA_Ubuntu - - os: macOS-latest + - os: macos-latest codecov-flag: GHA_macOS runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - - - name: Get pip cache dir - id: pip-cache - run: | - echo "::set-output name=dir::$(python3 -m pip cache dir)" - - - name: pip cache - uses: actions/cache@v2 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: - ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/.ci/*.sh') }} - restore-keys: | - ${{ matrix.os }}-${{ matrix.python-version }}- + cache: pip + cache-dependency-path: ".ci/*.sh" - name: Build system information run: python3 .github/workflows/system-info.py @@ -97,16 +84,16 @@ jobs: mkdir -p Tests/errors - name: Upload errors - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() with: name: errors path: Tests/errors - name: Docs - if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.9 + if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10 run: | - python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph + python3 -m pip install furo sphinx-copybutton sphinx-issues sphinx-removed-in sphinxext-opengraph make doccheck - name: After success diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml new file mode 100644 index 000000000..2e8c9b730 --- /dev/null +++ b/.github/workflows/tidelift.yml @@ -0,0 +1,26 @@ +name: Tidelift Align +on: + schedule: + - cron: "30 2 * * *" # daily at 02:30 UTC + push: + paths: + - ".github/workflows/tidelift.yml" + pull_request: + paths: + - ".github/workflows/tidelift.yml" + workflow_dispatch: + +jobs: + build: + if: github.repository_owner == 'python-pillow' + name: Run Tidelift to ensure approved open source packages are in use + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Scan + uses: tidelift/alignment-action@main + env: + TIDELIFT_API_KEY: ${{ secrets.TIDELIFT_API_KEY }} + TIDELIFT_ORGANIZATION: team/aclark4life + TIDELIFT_PROJECT: pillow diff --git a/.gitignore b/.gitignore index 5500ec037..790404535 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,7 @@ docs/_build/ Tests/images/README.md Tests/images/crash_1.tif Tests/images/crash_2.tif +Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif Tests/images/string_dimension.tiff Tests/images/jpeg2000 Tests/images/msp diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55fe9c4a7..353dd0c19 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,43 +1,43 @@ repos: - repo: https://github.com/psf/black - rev: e3000ace2fd1fcb1c181bb7a8285f1f976bcbdc7 # frozen: 21.7b0 + rev: 22.3.0 hooks: - id: black - args: ["--target-version", "py36"] + args: ["--target-version", "py37"] # Only .py files, until https://github.com/psf/black/issues/402 resolved files: \.py$ types: [] - repo: https://github.com/PyCQA/isort - rev: fd5ba70665a37ec301a1f714ed09336048b3be63 # frozen: 5.9.3 + rev: 5.10.1 hooks: - id: isort - repo: https://github.com/asottile/yesqa - rev: 644ede78511c02fc6f8e03e014cc1ddcfbf1e1f5 # frozen: v1.2.3 + rev: v1.3.0 hooks: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10 + rev: v1.1.13 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) - - repo: https://gitlab.com/pycqa/flake8 - rev: dcd740bc0ebaf2b3d43e59a0060d157c97de13f3 # frozen: 3.9.2 + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: [flake8-2020, flake8-implicit-str-concat] - repo: https://github.com/pre-commit/pygrep-hooks - rev: 6f51a66bba59954917140ec2eeeaa4d5e630e6ce # frozen: v1.9.0 + rev: v1.9.0 hooks: - id: python-check-blanket-noqa - id: rst-backticks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: 38b88246ccc552bffaaf54259d064beeee434539 # frozen: v4.0.1 + rev: v4.1.0 hooks: - id: check-merge-conflict - id: check-yaml diff --git a/.readthedocs.yml b/.readthedocs.yml index 73e1f8213..0f581ebba 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,2 +1,8 @@ +version: 2 + python: - pip_install: true + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/CHANGES.rst b/CHANGES.rst index 4e4e9648b..dc69e4587 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,300 @@ Changelog (Pillow) ================== -8.4.0 (unreleased) +9.2.0 (unreleased) ------------------ +- Round lut values where necessary #6188 + [radarhere] + +- Load before getting size in resize() #6190 + [radarhere] + +- Load image before performing size calculations in thumbnail() #6186 + [radarhere] + +- Deprecated PhotoImage.paste() box parameter #6178 + [radarhere] + +9.1.0 (2022-04-01) +------------------ + +- Add support for multiple component transformation to JPEG2000 #5500 + [scaramallion, radarhere, hugovk] + +- Fix loading FriBiDi on Alpine #6165 + [nulano] + +- Added setting for converting GIF P frames to RGB #6150 + [radarhere] + +- Allow 1 mode images to be inverted #6034 + [radarhere] + +- Raise ValueError when trying to save empty JPEG #6159 + [radarhere] + +- Always save TIFF with contiguous planar configuration #5973 + [radarhere] + +- Connected discontiguous polygon corners #5980 + [radarhere] + +- Ensure Tkinter hook is activated for getimage() #6032 + [radarhere] + +- Use screencapture arguments to crop on macOS #6152 + [radarhere] + +- Do not mark L mode JPEG as 1 bit in PDF #6151 + [radarhere] + +- Added support for reading I;16R TIFF images #6132 + [radarhere] + +- If an error occurs after creating a file, remove the file #6134 + [radarhere] + +- Fixed calling DisplayViewer or XVViewer without a title #6136 + [radarhere] + +- Retain RGBA transparency when saving multiple GIF frames #6128 + [radarhere] + +- Save additional ICO frames with other bit depths if supplied #6122 + [radarhere] + +- Handle EXIF data truncated to just the header #6124 + [radarhere] + +- Added support for reading BMP images with RLE8 compression #6102 + [radarhere] + +- Support Python distributions where _tkinter is compiled in #6006 + [lukegb] + +- Added support for PPM arbitrary maxval #6119 + [radarhere] + +- Added BigTIFF reading #6097 + [radarhere] + +- When converting, clip I;16 to be unsigned, not signed #6112 + [radarhere] + +- Fixed loading L mode GIF with transparency #6086 + [radarhere] + +- Improved handling of PPM header #5121 + [Piolie, radarhere] + +- Reset size when seeking away from "Large Thumbnail" MPO frame #6101 + [radarhere] + +- Replace requirements.txt with extras #6072 + [hugovk, radarhere] + +- Added PyEncoder and support BLP saving #6069 + [radarhere] + +- Handle TGA images with packets that cross scan lines #6087 + [radarhere] + +- Added FITS reading #6056 + [radarhere, hugovk] + +- Added rawmode argument to Image.getpalette() #6061 + [radarhere] + +- Fixed BUFR, GRIB and HDF5 stub saving #6071 + [radarhere] + +- Do not automatically remove temporary ImageShow files on Unix #6045 + [radarhere] + +- Correctly read JPEG compressed BLP images #4685 + [Meithal, radarhere] + +- Merged _MODE_CONV typ into ImageMode as typestr #6057 + [radarhere] + +- Consider palette size when converting and in getpalette() #6060 + [radarhere] + +- Added enums #5954 + [radarhere] + +- Ensure image is opaque after converting P to PA with RGB palette #6052 + [radarhere] + +- Attach RGBA palettes from putpalette() when suitable #6054 + [radarhere] + +- Added get_photoshop_blocks() to parse Photoshop TIFF tag #6030 + [radarhere] + +- Drop excess values in BITSPERSAMPLE #6041 + [mikhail-iurkov] + +- Added unpacker from RGBA;15 to RGB #6031 + [radarhere] + +- Enable arm64 for MSVC on Windows #5811 + [gaborkertesz-linaro, gaborkertesz] + +- Keep IPython/Jupyter text/plain output stable #5891 + [shamrin, radarhere] + +- Raise an error when performing a negative crop #5972 + [radarhere, hugovk] + +- Deprecated show_file "file" argument in favour of "path" #5959 + [radarhere] + +- Fixed SPIDER images for use with Bio-formats library #5956 + [radarhere] + +- Ensure duplicated file pointer is closed #5946 + [radarhere] + +- Added specific error if path coordinate type is incorrect #5942 + [radarhere] + +- Return an empty bytestring from tobytes() for an empty image #5938 + [radarhere] + +- Remove readonly from Image.__eq__ #5930 + [hugovk] + +9.0.1 (2022-02-03) +------------------ + +- In show_file, use os.remove to remove temporary images. CVE-2022-24303 #6010 + [radarhere, hugovk] + +- Restrict builtins within lambdas for ImageMath.eval. CVE-2022-22817 #6009 + [radarhere] + +9.0.0 (2022-01-02) +------------------ + +- Restrict builtins for ImageMath.eval(). CVE-2022-22817 #5923 + [radarhere] + +- Ensure JpegImagePlugin stops at the end of a truncated file #5921 + [radarhere] + +- Fixed ImagePath.Path array handling. CVE-2022-22815, CVE-2022-22816 #5920 + [radarhere] + +- Remove consecutive duplicate tiles that only differ by their offset #5919 + [radarhere] + +- Improved I;16 operations on big endian #5901 + [radarhere] + +- Limit quantized palette to number of colors #5879 + [radarhere] + +- Fixed palette index for zeroed color in FASTOCTREE quantize #5869 + [radarhere] + +- When saving RGBA to GIF, make use of first transparent palette entry #5859 + [radarhere] + +- Pass SAMPLEFORMAT to libtiff #5848 + [radarhere] + +- Added rounding when converting P and PA #5824 + [radarhere] + +- Improved putdata() documentation and data handling #5910 + [radarhere] + +- Exclude carriage return in PDF regex to help prevent ReDoS #5912 + [hugovk] + +- Fixed freeing pointer in ImageDraw.Outline.transform #5909 + [radarhere] + +- Added ImageShow support for xdg-open #5897 + [m-shinder, radarhere] + +- Support 16-bit grayscale ImageQt conversion #5856 + [cmbruns, radarhere] + +- Convert subsequent GIF frames to RGB or RGBA #5857 + [radarhere] + +- Do not prematurely return in ImageFile when saving to stdout #5665 + [infmagic2047, radarhere] + +- Added support for top right and bottom right TGA orientations #5829 + [radarhere] + +- Corrected ICNS file length in header #5845 + [radarhere] + +- Block tile TIFF tags when saving #5839 + [radarhere] + +- Added line width argument to polygon #5694 + [radarhere] + +- Do not redeclare class each time when converting to NumPy #5844 + [radarhere] + +- Only prevent repeated polygon pixels when drawing with transparency #5835 + [radarhere] + +- Add support for pickling TrueType fonts #5826 + [hugovk, radarhere] + +- Only prefer command line tools SDK on macOS over default MacOSX SDK #5828 + [radarhere] + +- Drop support for soon-EOL Python 3.6 #5768 + [hugovk, nulano, radarhere] + +- Fix compilation on 64-bit Termux #5793 + [landfillbaby] + +- Use title for display in ImageShow #5788 + [radarhere] + +- Remove support for FreeType 2.7 and older #5777 + [hugovk, radarhere] + +- Fix for PyQt6 #5775 + [hugovk, radarhere] + +- Removed deprecated PILLOW_VERSION, Image.show command parameter, Image._showxv and ImageFile.raise_ioerror #5776 + [radarhere] + +8.4.0 (2021-10-15) +------------------ + +- Prefer global transparency in GIF when replacing with background color #5756 + [radarhere] + +- Added "exif" keyword argument to TIFF saving #5575 + [radarhere] + +- Copy Python palette to new image in quantize() #5696 + [radarhere] + +- Read ICO AND mask from end #5667 + [radarhere] + +- Actually check the framesize in FliDecode.c #5659 + [wiredfool] + +- Determine JPEG2000 mode purely from ihdr header box #5654 + [radarhere] + +- Fixed using info dictionary when writing multiple APNG frames #5611 + [radarhere] + - Allow saving 1 and L mode TIFF with PhotometricInterpretation 0 #5655 [radarhere] @@ -59,12 +350,30 @@ Changelog (Pillow) - Fixed ImageOps expand with tuple border on P image #5615 [radarhere] -- Ensure TIFF RowsPerStrip is multiple of 8 for JPEG compression #5588 - [kmilos, radarhere] - - Fixed error saving APNG with duplicate frames and different duration times #5609 [thak1411, radarhere] +8.3.2 (2021-09-02) +------------------ + +- CVE-2021-23437 Raise ValueError if color specifier is too long + [hugovk, radarhere] + +- Fix 6-byte OOB read in FliDecode + [wiredfool] + +- Add support for Python 3.10 #5569, #5570 + [hugovk, radarhere] + +- Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression #5588 + [kmilos, radarhere] + +- Updates for ``ImagePalette`` channel order #5599 + [radarhere] + +- Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library #5651 + [nulano] + 8.3.1 (2021-07-06) ------------------ @@ -338,7 +647,7 @@ Changelog (Pillow) - Changed Image.open formats parameter to be case-insensitive #5250 [Piolie, radarhere] -- Deprecate Tk/Tcl 8.4, to be removed in Pillow 10 (2023-01-02) #5216 +- Deprecate Tk/Tcl 8.4, to be removed in Pillow 10 (2023-07-01) #5216 [radarhere] - Added tk version to pilinfo #5226 diff --git a/LICENSE b/LICENSE index 1197291bc..40aabc323 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2021 by Alex Clark and contributors + Copyright © 2010-2022 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source HPND License: diff --git a/MANIFEST.in b/MANIFEST.in index e9aaa8318..26f9401f2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include *.c include *.h include *.in +include *.lock include *.md include *.py include *.rst @@ -9,6 +10,7 @@ include *.txt include *.yaml include LICENSE include Makefile +include Pipfile include tox.ini graft Tests graft src diff --git a/Makefile b/Makefile index af3059f34..437050ed4 100644 --- a/Makefile +++ b/Makefile @@ -9,9 +9,11 @@ clean: .PHONY: coverage coverage: - pytest -qq + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq rm -r htmlcov || true - coverage report + python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage + python3 -m coverage report .PHONY: doc doc: @@ -33,33 +35,29 @@ help: @echo "Welcome to Pillow development. Please use \`make \` where is one of" @echo " clean remove build products" @echo " coverage run coverage test (in progress)" - @echo " doc make html docs" - @echo " docserve run an http server on the docs directory" + @echo " doc make HTML docs" + @echo " docserve run an HTTP server on the docs directory" @echo " html to make standalone HTML files" @echo " inplace make inplace extension" @echo " install make and install" @echo " install-coverage make and install with C coverage" - @echo " install-req install documentation and test dependencies" - @echo " install-venv (deprecated) install in virtualenv" @echo " lint run the lint checks" - @echo " lint-fix run black and isort to (mostly) fix lint issues." + @echo " lint-fix run Black and isort to (mostly) fix lint issues" @echo " release-test run code and package tests before release" - @echo " test run tests on installed pillow" - @echo " upload build and upload sdists to PyPI" - @echo " upload-test build and upload sdists to test.pythonpackages.com" + @echo " test run tests on installed Pillow" .PHONY: inplace inplace: clean - python3 setup.py develop build_ext --inplace + python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" . .PHONY: install install: - python3 setup.py install + python3 -m pip install . python3 selftest.py .PHONY: install-coverage install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 setup.py build_ext install + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip install --global-option="build_ext" . python3 selftest.py .PHONY: debug @@ -68,58 +66,52 @@ debug: # for our stuff, kills optimization, and redirects to dev null so we # see any build failures. make clean > /dev/null - CFLAGS='-g -O0' python3 setup.py build_ext install > /dev/null - -.PHONY: install-req -install-req: - python3 -m pip install -r requirements.txt - -.PHONY: install-venv -install-venv: - echo "'install-venv' is deprecated and will be removed in a future Pillow release" - virtualenv . - bin/pip install -r requirements.txt + CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null .PHONY: release-test release-test: - $(MAKE) install-req - python3 setup.py develop + python3 -m pip install -e .[tests] python3 selftest.py python3 -m pytest Tests - python3 setup.py install + python3 -m pip install . -rm dist/*.egg -rmdir dist python3 -m pytest -qq - check-manifest - pyroma . + python3 -m check_manifest + python3 -m pyroma . $(MAKE) readme .PHONY: sdist sdist: - python3 setup.py sdist --format=gztar + python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build + python3 -m build --sdist .PHONY: test test: - pytest -qq + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq .PHONY: valgrind valgrind: - python3 -c "import pytest_valgrind" || pip3 install pytest-valgrind + python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ --log-file=/tmp/valgrind-output \ python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output .PHONY: readme readme: - python3 setup.py --long-description | markdown2 > .long-description.html && open .long-description.html + python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 + python3 -m markdown2 README.md > .long-description.html && open .long-description.html .PHONY: lint lint: - tox --help > /dev/null || python3 -m pip install tox - tox -e lint + python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox + python3 -m tox -e lint .PHONY: lint-fix lint-fix: - black --target-version py36 . - isort . + python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black + python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort + python3 -m black --target-version py37 . + python3 -m isort . diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000..1e611a63c --- /dev/null +++ b/Pipfile @@ -0,0 +1,22 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +black = "*" +check-manifest = "*" +coverage = "*" +defusedxml = "*" +packaging = "*" +markdown2 = "*" +olefile = "*" +pyroma = "*" +pytest = "*" +pytest-cov = "*" +pytest-timeout = "*" + +[dev-packages] + +[requires] +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 000000000..600b19050 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,324 @@ +{ + "_meta": { + "hash": { + "sha256": "e5cad23bf4187647d53b613a64dc4792b7064bf86b08dfb5737580e32943f54d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "attrs": { + "hashes": [ + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" + }, + "black": { + "hashes": [ + "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3", + "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f" + ], + "index": "pypi", + "version": "==21.12b0" + }, + "build": { + "hashes": [ + "sha256:1aaadcd69338252ade4f7ec1265e1a19184bf916d84c9b7df095f423948cb89f", + "sha256:21b7ebbd1b22499c4dac536abc7606696ea4d909fd755e00f09f3c0f2c05e3c8" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "certifi": { + "hashes": [ + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + ], + "version": "==2021.10.8" + }, + "charset-normalizer": { + "hashes": [ + "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", + "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" + ], + "markers": "python_version >= '3'", + "version": "==2.0.9" + }, + "check-manifest": { + "hashes": [ + "sha256:365c94d65de4c927d9d8b505371d08ee19f9f369c86b9ac3db97c2754c827c95", + "sha256:56dadd260a9c7d550b159796d2894b6d0bcc176a94cbc426d9bb93e5e48d12ce" + ], + "index": "pypi", + "version": "==0.47" + }, + "click": { + "hashes": [ + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + ], + "markers": "python_version >= '3.6'", + "version": "==8.0.3" + }, + "coverage": { + "hashes": [ + "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0", + "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd", + "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884", + "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48", + "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76", + "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0", + "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64", + "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685", + "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47", + "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d", + "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840", + "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f", + "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971", + "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c", + "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a", + "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de", + "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17", + "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4", + "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521", + "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57", + "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b", + "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282", + "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644", + "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475", + "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d", + "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da", + "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953", + "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2", + "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e", + "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c", + "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc", + "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64", + "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74", + "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617", + "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3", + "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d", + "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa", + "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739", + "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8", + "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8", + "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781", + "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58", + "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9", + "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c", + "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd", + "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e", + "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49" + ], + "index": "pypi", + "version": "==6.2" + }, + "defusedxml": { + "hashes": [ + "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", + "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" + ], + "index": "pypi", + "version": "==0.7.1" + }, + "docutils": { + "hashes": [ + "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", + "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.18.1" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "markers": "python_version >= '3'", + "version": "==3.3" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "markdown2": { + "hashes": [ + "sha256:8f4ac8d9a124ab408c67361090ed512deda746c04362c36c2ec16190c720c2b0", + "sha256:91113caf23aa662570fe21984f08fe74f814695c0a0ea8e863a8b4c4f63f9f6e" + ], + "index": "pypi", + "version": "==2.4.2" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "olefile": { + "hashes": [ + "sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964" + ], + "index": "pypi", + "version": "==0.46" + }, + "packaging": { + "hashes": [ + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + ], + "index": "pypi", + "version": "==21.3" + }, + "pathspec": { + "hashes": [ + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + ], + "version": "==0.9.0" + }, + "pep517": { + "hashes": [ + "sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0", + "sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161" + ], + "version": "==0.12.0" + }, + "platformdirs": { + "hashes": [ + "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2", + "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d" + ], + "markers": "python_version >= '3.6'", + "version": "==2.4.0" + }, + "pluggy": { + "hashes": [ + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + ], + "markers": "python_version >= '3.6'", + "version": "==1.0.0" + }, + "py": { + "hashes": [ + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.11.0" + }, + "pygments": { + "hashes": [ + "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", + "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" + ], + "markers": "python_version >= '3.5'", + "version": "==2.10.0" + }, + "pyparsing": { + "hashes": [ + "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", + "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.6" + }, + "pyroma": { + "hashes": [ + "sha256:0fba67322913026091590e68e0d9e0d4fbd6420fcf34d315b2ad6985ab104d65", + "sha256:f8c181e0d5d292f11791afc18f7d0218a83c85cf64d6f8fb1571ce9d29a24e4a" + ], + "index": "pypi", + "version": "==3.2" + }, + "pytest": { + "hashes": [ + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" + ], + "index": "pypi", + "version": "==6.2.5" + }, + "pytest-cov": { + "hashes": [ + "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", + "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" + ], + "index": "pypi", + "version": "==3.0.0" + }, + "pytest-timeout": { + "hashes": [ + "sha256:e6f98b54dafde8d70e4088467ff621260b641eb64895c4195b6e5c8f45638112", + "sha256:fe9c3d5006c053bb9e062d60f641e6a76d6707aedb645350af9593e376fcc717" + ], + "index": "pypi", + "version": "==2.0.2" + }, + "requests": { + "hashes": [ + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==2.26.0" + }, + "setuptools": { + "hashes": [ + "sha256:5ec2bbb534ed160b261acbbdd1b463eb3cf52a8d223d96a8ab9981f63798e85c", + "sha256:75fd345a47ce3d79595b27bf57e6f49c2ca7904f3c7ce75f8a87012046c86b0b" + ], + "markers": "python_version >= '3.7'", + "version": "==60.0.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" + }, + "tomli": { + "hashes": [ + "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f", + "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.1" + }, + "urllib3": { + "hashes": [ + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.7" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index 29b5b8a6a..7bff737a2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Pillow logo + Pillow logo

# Pillow @@ -24,30 +24,36 @@ As of 2019, Pillow development is tests - GitHub Actions build status (Lint) - GitHub Actions build status (Test Linux and macOS) - GitHub Actions build status (Test Windows) - GitHub Actions build status (Test MinGW) + GitHub Actions build status (Test Docker) AppVeyor CI build status (Windows) + src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"> GitHub Actions wheels build status (Wheels) - Travis CI wheels build status (aarch64) - + Code coverage + src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"> + Tidelift Align @@ -93,12 +99,12 @@ The core image library is designed for fast access to data stored in a few basic - [Documentation](https://pillow.readthedocs.io/) - [Installation](https://pillow.readthedocs.io/en/latest/installation.html) - [Handbook](https://pillow.readthedocs.io/en/latest/handbook/index.html) -- [Contribute](https://github.com/python-pillow/Pillow/blob/master/.github/CONTRIBUTING.md) +- [Contribute](https://github.com/python-pillow/Pillow/blob/main/.github/CONTRIBUTING.md) - [Issues](https://github.com/python-pillow/Pillow/issues) - [Pull requests](https://github.com/python-pillow/Pillow/pulls) - [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html) -- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst) - - [Pre-fork](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst#pre-fork) +- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) + - [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork) ## Report a Vulnerability diff --git a/RELEASING.md b/RELEASING.md index 6045f84ac..a6049b685 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -8,8 +8,8 @@ information about how the version numbers line up with releases. Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 -* [ ] Develop and prepare release in `master` branch. -* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `master` branch. +* [ ] Develop and prepare release in `main` branch. +* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. * [ ] Check that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI and GitHub Actions. * [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Update `CHANGES.rst`. @@ -24,13 +24,13 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Create and check source distribution: ```bash make sdist - twine check dist/* + python3 -m twine check --strict dist/* ``` -* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/master/RELEASING.md#binary-distributions) +* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Check and upload all binaries and source distributions e.g.: ```bash - twine check dist/* - twine upload dist/Pillow-5.2.0* + python3 -m twine check --strict dist/* + python3 -m twine upload dist/Pillow-5.2.0* ``` * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` @@ -39,13 +39,13 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. Released as needed for security, installation or critical bug fixes. -* [ ] Make necessary changes in `master` branch. +* [ ] Make necessary changes in `main` branch. * [ ] Update `CHANGES.rst`. * [ ] Check out release branch e.g.: ```bash git checkout -t remotes/origin/5.2.x ``` -* [ ] Cherry pick individual commits from `master` branch to release branch e.g. `5.2.x`, then `git push`. +* [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`. @@ -61,13 +61,13 @@ Released as needed for security, installation or critical bug fixes. * [ ] Create and check source distribution: ```bash make sdist - twine check dist/* + python3 -m twine check --strict dist/* ``` -* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/master/RELEASING.md#binary-distributions) +* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Check and upload all binaries and source distributions e.g.: ```bash - twine check dist/* - twine upload dist/Pillow-5.2.1* + python3 -m twine check --strict dist/* + python3 -m twine upload dist/Pillow-5.2.1* ``` * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) @@ -76,7 +76,7 @@ Released as needed for security, installation or critical bug fixes. Released as needed privately to individual vendors for critical security-related bug fixes. * [ ] Prepare patch for all versions that will get a fix. Test against local installations. -* [ ] Commit against master, cherry pick to affected release branches. +* [ ] Commit against `main`, cherry pick to affected release branches. * [ ] Run local test matrix on each release & Python version. * [ ] Privately send to distros. * [ ] Run pre-release check via `make release-test` @@ -91,9 +91,9 @@ Released as needed privately to individual vendors for critical security-related * [ ] Create and check source distribution: ```bash make sdist - twine check dist/* + python3 -m twine check --strict dist/* ``` -* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/master/RELEASING.md#binary-distributions) +* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) ## Binary Distributions diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py index e19cdf7a9..2ff7f908f 100755 --- a/Tests/32bit_segfault_check.py +++ b/Tests/32bit_segfault_check.py @@ -4,5 +4,5 @@ import sys from PIL import Image -if sys.maxsize < 2 ** 32: +if sys.maxsize < 2**32: im = Image.new("L", (999999, 999999), 0) diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py index 6b63a6826..7b3d4d7ee 100644 --- a/Tests/check_fli_oob.py +++ b/Tests/check_fli_oob.py @@ -61,8 +61,8 @@ repro_copy = ( for path in repro_ss2 + repro_lc + repro_advance + repro_brun + repro_copy: - im = Image.open(path) - try: - im.load() - except Exception as msg: - print(msg) + with Image.open(path) as im: + try: + im.load() + except Exception as msg: + print(msg) diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py index f81a360ce..0210505f5 100755 --- a/Tests/check_jp2_overflow.py +++ b/Tests/check_jp2_overflow.py @@ -19,8 +19,8 @@ from PIL import Image repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2") for path in repro: - im = Image.open(path) - try: - im.load() - except Exception as msg: - print(msg) + with Image.open(path) as im: + try: + im.load() + except Exception as msg: + print(msg) diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index c191ffc1e..d98f4a694 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -23,7 +23,7 @@ YDIM = 32769 XDIM = 48000 -pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") def _write_png(tmp_path, xdim, ydim): diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 70ae6d230..24cb1f722 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -19,7 +19,7 @@ YDIM = 32769 XDIM = 48000 -pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") def _write_png(tmp_path, xdim, ydim): diff --git a/Tests/helper.py b/Tests/helper.py index 8504993fb..13c6955e4 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -30,7 +30,6 @@ if os.environ.get("SHOW_ERRORS", None): a.show() b.show() - elif "GITHUB_ACTIONS" in os.environ: HAS_UPLOADER = True @@ -44,7 +43,6 @@ elif "GITHUB_ACTIONS" in os.environ: b.save(os.path.join(tmpdir, "b.png")) return tmpdir - else: try: import test_image_results @@ -326,7 +324,7 @@ def is_mingw(): return sysconfig.get_platform() == "mingw" -class cached_property: +class CachedProperty: def __init__(self, func): self.func = func diff --git a/Tests/images/16bit.r.tif b/Tests/images/16bit.r.tif new file mode 100644 index 000000000..0f3996e95 Binary files /dev/null and b/Tests/images/16bit.r.tif differ diff --git a/Tests/images/balloon_eciRGBv2_aware.jp2 b/Tests/images/balloon_eciRGBv2_aware.jp2 new file mode 100644 index 000000000..18fd1e172 Binary files /dev/null and b/Tests/images/balloon_eciRGBv2_aware.jp2 differ diff --git a/Tests/images/bitmap_font_stroke_basic.png b/Tests/images/bitmap_font_stroke_basic.png new file mode 100644 index 000000000..86b2d09f6 Binary files /dev/null and b/Tests/images/bitmap_font_stroke_basic.png differ diff --git a/Tests/images/bitmap_font_stroke_raqm.png b/Tests/images/bitmap_font_stroke_raqm.png new file mode 100644 index 000000000..08029ce34 Binary files /dev/null and b/Tests/images/bitmap_font_stroke_raqm.png differ diff --git a/Tests/images/blp/blp1_jpeg.png b/Tests/images/blp/blp1_jpeg.png new file mode 100644 index 000000000..be151f205 Binary files /dev/null and b/Tests/images/blp/blp1_jpeg.png differ diff --git a/Tests/images/crash-5762152299364352.fli b/Tests/images/crash-5762152299364352.fli new file mode 100644 index 000000000..944fe0b56 Binary files /dev/null and b/Tests/images/crash-5762152299364352.fli differ diff --git a/Tests/images/cross_scan_line.png b/Tests/images/cross_scan_line.png new file mode 100644 index 000000000..64b68ed33 Binary files /dev/null and b/Tests/images/cross_scan_line.png differ diff --git a/Tests/images/cross_scan_line.tga b/Tests/images/cross_scan_line.tga new file mode 100644 index 000000000..5ef8c8154 Binary files /dev/null and b/Tests/images/cross_scan_line.tga differ diff --git a/Tests/images/different_transparency_merged.gif b/Tests/images/different_transparency_merged.gif deleted file mode 100644 index 94d0f53e0..000000000 Binary files a/Tests/images/different_transparency_merged.gif and /dev/null differ diff --git a/Tests/images/different_transparency_merged.png b/Tests/images/different_transparency_merged.png new file mode 100644 index 000000000..3438f62a6 Binary files /dev/null and b/Tests/images/different_transparency_merged.png differ diff --git a/Tests/images/dispose_bgnd_rgba.gif b/Tests/images/dispose_bgnd_rgba.gif new file mode 100644 index 000000000..c18a0ba71 Binary files /dev/null and b/Tests/images/dispose_bgnd_rgba.gif differ diff --git a/Tests/images/dispose_bgnd_transparency.gif b/Tests/images/dispose_bgnd_transparency.gif new file mode 100644 index 000000000..7c626fe72 Binary files /dev/null and b/Tests/images/dispose_bgnd_transparency.gif differ diff --git a/Tests/images/dispose_none_load_end_second.gif b/Tests/images/dispose_none_load_end_second.gif deleted file mode 100644 index 5d8462ceb..000000000 Binary files a/Tests/images/dispose_none_load_end_second.gif and /dev/null differ diff --git a/Tests/images/dispose_none_load_end_second.png b/Tests/images/dispose_none_load_end_second.png new file mode 100644 index 000000000..dc01ccbdd Binary files /dev/null and b/Tests/images/dispose_none_load_end_second.png differ diff --git a/Tests/images/dispose_prev_first_frame_seeked.gif b/Tests/images/dispose_prev_first_frame_seeked.gif deleted file mode 100644 index bc3eb1393..000000000 Binary files a/Tests/images/dispose_prev_first_frame_seeked.gif and /dev/null differ diff --git a/Tests/images/dispose_prev_first_frame_seeked.png b/Tests/images/dispose_prev_first_frame_seeked.png new file mode 100644 index 000000000..85a3753e1 Binary files /dev/null and b/Tests/images/dispose_prev_first_frame_seeked.png differ diff --git a/Tests/images/hopper.fits b/Tests/images/hopper.fits index 85afa4ac1..7f28f75e5 100644 Binary files a/Tests/images/hopper.fits and b/Tests/images/hopper.fits differ diff --git a/Tests/images/hopper_bigtiff.tif b/Tests/images/hopper_bigtiff.tif new file mode 100644 index 000000000..9588a37d8 Binary files /dev/null and b/Tests/images/hopper_bigtiff.tif differ diff --git a/Tests/images/hopper_mask.ico b/Tests/images/hopper_mask.ico new file mode 100644 index 000000000..e8d66c689 Binary files /dev/null and b/Tests/images/hopper_mask.ico differ diff --git a/Tests/images/hopper_mask.png b/Tests/images/hopper_mask.png new file mode 100644 index 000000000..c7bd2f708 Binary files /dev/null and b/Tests/images/hopper_mask.png differ diff --git a/Tests/images/hopper_rle8.bmp b/Tests/images/hopper_rle8.bmp new file mode 100644 index 000000000..0fff4a0d4 Binary files /dev/null and b/Tests/images/hopper_rle8.bmp differ diff --git a/Tests/images/hopper_rle8_row_overflow.bmp b/Tests/images/hopper_rle8_row_overflow.bmp new file mode 100644 index 000000000..d606dc3e4 Binary files /dev/null and b/Tests/images/hopper_rle8_row_overflow.bmp differ diff --git a/Tests/images/imagedraw/discontiguous_corners_polygon.png b/Tests/images/imagedraw/discontiguous_corners_polygon.png new file mode 100644 index 000000000..509c42b26 Binary files /dev/null and b/Tests/images/imagedraw/discontiguous_corners_polygon.png differ diff --git a/Tests/images/imagedraw/triangle_right_width.png b/Tests/images/imagedraw/triangle_right_width.png new file mode 100644 index 000000000..57b73553a Binary files /dev/null and b/Tests/images/imagedraw/triangle_right_width.png differ diff --git a/Tests/images/imagedraw/triangle_right_width_no_fill.png b/Tests/images/imagedraw/triangle_right_width_no_fill.png new file mode 100644 index 000000000..dd65be6be Binary files /dev/null and b/Tests/images/imagedraw/triangle_right_width_no_fill.png differ diff --git a/Tests/images/imagedraw_polygon_translucent.png b/Tests/images/imagedraw_polygon_translucent.png new file mode 100644 index 000000000..da8d790a3 Binary files /dev/null and b/Tests/images/imagedraw_polygon_translucent.png differ diff --git a/Tests/images/missing_background_first_frame.gif b/Tests/images/missing_background_first_frame.gif deleted file mode 100644 index be2c95b99..000000000 Binary files a/Tests/images/missing_background_first_frame.gif and /dev/null differ diff --git a/Tests/images/missing_background_first_frame.png b/Tests/images/missing_background_first_frame.png new file mode 100644 index 000000000..25237ba5d Binary files /dev/null and b/Tests/images/missing_background_first_frame.png differ diff --git a/Tests/images/multiline_text.png b/Tests/images/multiline_text.png index ff1308c5e..e39c6586c 100644 Binary files a/Tests/images/multiline_text.png and b/Tests/images/multiline_text.png differ diff --git a/Tests/images/multiline_text_center.png b/Tests/images/multiline_text_center.png index f44d0783a..837c6382a 100644 Binary files a/Tests/images/multiline_text_center.png and b/Tests/images/multiline_text_center.png differ diff --git a/Tests/images/multiline_text_right.png b/Tests/images/multiline_text_right.png index 1b32d9167..58b3bdddd 100644 Binary files a/Tests/images/multiline_text_right.png and b/Tests/images/multiline_text_right.png differ diff --git a/Tests/images/multiline_text_spacing.png b/Tests/images/multiline_text_spacing.png index 3c3bc0f26..3b367c7dd 100644 Binary files a/Tests/images/multiline_text_spacing.png and b/Tests/images/multiline_text_spacing.png differ diff --git a/Tests/images/no_palette.gif b/Tests/images/no_palette.gif new file mode 100644 index 000000000..0432ebcb6 Binary files /dev/null and b/Tests/images/no_palette.gif differ diff --git a/Tests/images/no_palette_with_background.gif b/Tests/images/no_palette_with_background.gif new file mode 100644 index 000000000..e49e5d461 Binary files /dev/null and b/Tests/images/no_palette_with_background.gif differ diff --git a/Tests/images/no_palette_with_transparency.gif b/Tests/images/no_palette_with_transparency.gif new file mode 100644 index 000000000..031bdcfce Binary files /dev/null and b/Tests/images/no_palette_with_transparency.gif differ diff --git a/Tests/images/pal8_offset.bmp b/Tests/images/pal8_offset.bmp new file mode 100644 index 000000000..24be65f22 Binary files /dev/null and b/Tests/images/pal8_offset.bmp differ diff --git a/Tests/images/palette_negative.png b/Tests/images/palette_negative.png new file mode 100644 index 000000000..938a7285f Binary files /dev/null and b/Tests/images/palette_negative.png differ diff --git a/Tests/images/palette_sepia.png b/Tests/images/palette_sepia.png new file mode 100644 index 000000000..f3fc93253 Binary files /dev/null and b/Tests/images/palette_sepia.png differ diff --git a/Tests/images/palette_wedge.png b/Tests/images/palette_wedge.png new file mode 100644 index 000000000..23fb7940d Binary files /dev/null and b/Tests/images/palette_wedge.png differ diff --git a/Tests/images/pillow3.icns b/Tests/images/pillow3.icns index ef9b89178..49b691d90 100644 Binary files a/Tests/images/pillow3.icns and b/Tests/images/pillow3.icns differ diff --git a/Tests/images/rgb32rle_bottom_right.tga b/Tests/images/rgb32rle_bottom_right.tga new file mode 100644 index 000000000..bd4609e9c Binary files /dev/null and b/Tests/images/rgb32rle_bottom_right.tga differ diff --git a/Tests/images/rgb32rle_top_right.tga b/Tests/images/rgb32rle_top_right.tga new file mode 100644 index 000000000..78f9dc5df Binary files /dev/null and b/Tests/images/rgb32rle_top_right.tga differ diff --git a/Tests/images/tiff_wrong_bits_per_sample_2.tiff b/Tests/images/tiff_wrong_bits_per_sample_2.tiff new file mode 100644 index 000000000..d44176ce7 Binary files /dev/null and b/Tests/images/tiff_wrong_bits_per_sample_2.tiff differ diff --git a/Tests/images/timeout-6646305047838720 b/Tests/images/timeout-6646305047838720 new file mode 100644 index 000000000..eae1f333a Binary files /dev/null and b/Tests/images/timeout-6646305047838720 differ diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh index 513136fff..09cc7bc16 100755 --- a/Tests/oss-fuzz/build.sh +++ b/Tests/oss-fuzz/build.sh @@ -22,7 +22,7 @@ for fuzzer in $(find $SRC -name 'fuzz_*.py'); do fuzzer_basename=$(basename -s .py $fuzzer) fuzzer_package=${fuzzer_basename}.pkg pyinstaller \ - --add-binary /usr/local/lib/libjpeg.so.9:. \ + --add-binary /usr/local/lib/libjpeg.so.62.3.0:. \ --add-binary /usr/local/lib/libfreetype.so.6:. \ --add-binary /usr/local/lib/liblcms2.so.2:. \ --add-binary /usr/local/lib/libopenjp2.so.7:. \ diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index e11471011..bc2ba9a7e 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -14,10 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys -import atheris_no_libfuzzer as atheris -import fuzzers +import atheris + +with atheris.instrument_imports(): + import sys + + import fuzzers def TestOneInput(data): @@ -26,13 +29,12 @@ def TestOneInput(data): except Exception: # We're catching all exceptions because Pillow's exceptions are # directly inheriting from Exception. - return - return + pass def main(): fuzzers.enable_decompressionbomb_error() - atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) + atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() fuzzers.disable_decompressionbomb_error() diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index b3c55fe22..545daccb6 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -14,10 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import sys -import atheris_no_libfuzzer as atheris -import fuzzers +import atheris + +with atheris.instrument_imports(): + import sys + + import fuzzers def TestOneInput(data): @@ -26,13 +29,12 @@ def TestOneInput(data): except Exception: # We're catching all exceptions because Pillow's exceptions are # directly inheriting from Exception. - return - return + pass def main(): fuzzers.enable_decompressionbomb_error() - atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) + atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() fuzzers.disable_decompressionbomb_error() diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 99e16391a..b17aad2ea 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,6 +1,5 @@ import os - -import pytest +import warnings from PIL import Image @@ -20,16 +19,14 @@ def test_bad(): either""" for f in get_files("b"): - with pytest.warns(None) as record: + # Assert that there is no unclosed file warning + with warnings.catch_warnings(): try: with Image.open(f) as im: im.load() except Exception: # as msg: pass - # Assert that there is no unclosed file warning - assert not record - def test_questionable(): """These shouldn't crash/dos, but it's not well defined that these @@ -43,6 +40,7 @@ def test_questionable(): "rgb32fakealpha.bmp", "rgb24largepal.bmp", "pal8os2sp.bmp", + "pal8rletrns.bmp", "rgb32bf-xbgr.bmp", ] for f in get_files("q"): diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 94f504e0b..3bdd5177d 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -25,7 +25,7 @@ def box_blur(image, radius=1, n=1): return image._new(image.im.box_blur(radius, n)) -def assertImage(im, data, delta=0): +def assert_image(im, data, delta=0): it = iter(im.getdata()) for data_row in data: im_row = [next(it) for _ in range(im.size[0])] @@ -35,12 +35,12 @@ def assertImage(im, data, delta=0): next(it) -def assertBlur(im, radius, data, passes=1, delta=0): +def assert_blur(im, radius, data, passes=1, delta=0): # check grayscale image - assertImage(box_blur(im, radius, passes), data, delta) + assert_image(box_blur(im, radius, passes), data, delta) rgba = Image.merge("RGBA", (im, im, im, im)) for band in box_blur(rgba, radius, passes).split(): - assertImage(band, data, delta) + assert_image(band, data, delta) def test_color_modes(): @@ -64,7 +64,7 @@ def test_color_modes(): def test_radius_0(): - assertBlur( + assert_blur( sample, 0, [ @@ -80,7 +80,7 @@ def test_radius_0(): def test_radius_0_02(): - assertBlur( + assert_blur( sample, 0.02, [ @@ -97,7 +97,7 @@ def test_radius_0_02(): def test_radius_0_05(): - assertBlur( + assert_blur( sample, 0.05, [ @@ -114,7 +114,7 @@ def test_radius_0_05(): def test_radius_0_1(): - assertBlur( + assert_blur( sample, 0.1, [ @@ -131,7 +131,7 @@ def test_radius_0_1(): def test_radius_0_5(): - assertBlur( + assert_blur( sample, 0.5, [ @@ -148,7 +148,7 @@ def test_radius_0_5(): def test_radius_1(): - assertBlur( + assert_blur( sample, 1, [ @@ -165,7 +165,7 @@ def test_radius_1(): def test_radius_1_5(): - assertBlur( + assert_blur( sample, 1.5, [ @@ -182,7 +182,7 @@ def test_radius_1_5(): def test_radius_bigger_then_half(): - assertBlur( + assert_blur( sample, 3, [ @@ -199,7 +199,7 @@ def test_radius_bigger_then_half(): def test_radius_bigger_then_width(): - assertBlur( + assert_blur( sample, 10, [ @@ -214,7 +214,7 @@ def test_radius_bigger_then_width(): def test_extreme_large_radius(): - assertBlur( + assert_blur( sample, 600, [ @@ -229,7 +229,7 @@ def test_extreme_large_radius(): def test_two_passes(): - assertBlur( + assert_blur( sample, 1, [ @@ -247,7 +247,7 @@ def test_two_passes(): def test_three_passes(): - assertBlur( + assert_blur( sample, 1, [ diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 99776ce58..abe13cf13 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -15,27 +15,27 @@ except ImportError: class TestColorLut3DCoreAPI: def generate_identity_table(self, channels, size): if isinstance(size, tuple): - size1D, size2D, size3D = size + size_1d, size_2d, size_3d = size else: - size1D, size2D, size3D = (size, size, size) + size_1d, size_2d, size_3d = (size, size, size) table = [ [ - r / (size1D - 1) if size1D != 1 else 0, - g / (size2D - 1) if size2D != 1 else 0, - b / (size3D - 1) if size3D != 1 else 0, - r / (size1D - 1) if size1D != 1 else 0, - g / (size2D - 1) if size2D != 1 else 0, + r / (size_1d - 1) if size_1d != 1 else 0, + g / (size_2d - 1) if size_2d != 1 else 0, + b / (size_3d - 1) if size_3d != 1 else 0, + r / (size_1d - 1) if size_1d != 1 else 0, + g / (size_2d - 1) if size_2d != 1 else 0, ][:channels] - for b in range(size3D) - for g in range(size2D) - for r in range(size1D) + for b in range(size_3d) + for g in range(size_2d) + for r in range(size_1d) ] return ( channels, - size1D, - size2D, - size3D, + size_1d, + size_2d, + size_3d, [item for sublist in table for item in sublist], ) @@ -43,107 +43,158 @@ class TestColorLut3DCoreAPI: im = Image.new("RGB", (10, 10), 0) with pytest.raises(ValueError, match="filter"): - im.im.color_lut_3d("RGB", Image.CUBIC, *self.generate_identity_table(3, 3)) + im.im.color_lut_3d( + "RGB", Image.Resampling.BICUBIC, *self.generate_identity_table(3, 3) + ) with pytest.raises(ValueError, match="image mode"): im.im.color_lut_3d( - "wrong", Image.LINEAR, *self.generate_identity_table(3, 3) + "wrong", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) ) with pytest.raises(ValueError, match="table_channels"): - im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(5, 3)) - - with pytest.raises(ValueError, match="table_channels"): - im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(1, 3)) - - with pytest.raises(ValueError, match="table_channels"): - im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(2, 3)) - - with pytest.raises(ValueError, match="Table size"): im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (1, 3, 3)) + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(5, 3) + ) + + with pytest.raises(ValueError, match="table_channels"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(1, 3) + ) + + with pytest.raises(ValueError, match="table_channels"): + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(2, 3) ) with pytest.raises(ValueError, match="Table size"): im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (66, 3, 3)) + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (1, 3, 3)), + ) + + with pytest.raises(ValueError, match="Table size"): + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (66, 3, 3)), ) with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): - im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 7) + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7 + ) with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"): - im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, 0] * 9) + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9 + ) with pytest.raises(TypeError): - im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8) + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8 + ) with pytest.raises(TypeError): - im.im.color_lut_3d("RGB", Image.LINEAR, 3, 2, 2, 2, 16) + im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) def test_correct_args(self): im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(3, 3)) - - im.im.color_lut_3d("CMYK", Image.LINEAR, *self.generate_identity_table(4, 3)) - im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (2, 3, 3)) + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) ) im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (65, 3, 3)) + "CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ) im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (3, 65, 3)) + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (2, 3, 3)), ) im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (3, 3, 65)) + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (65, 3, 3)), + ) + + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (3, 65, 3)), + ) + + im.im.color_lut_3d( + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (3, 3, 65)), ) def test_wrong_mode(self): with pytest.raises(ValueError, match="wrong mode"): im = Image.new("L", (10, 10), 0) - im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(3, 3)) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3)) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("L", (10, 10), 0) - im.im.color_lut_3d("L", Image.LINEAR, *self.generate_identity_table(3, 3)) - - with pytest.raises(ValueError, match="wrong mode"): - im = Image.new("RGB", (10, 10), 0) im.im.color_lut_3d( - "RGBA", Image.LINEAR, *self.generate_identity_table(3, 3) + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) ) with pytest.raises(ValueError, match="wrong mode"): im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d("RGB", Image.LINEAR, *self.generate_identity_table(4, 3)) + im.im.color_lut_3d( + "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("L", (10, 10), 0) + im.im.color_lut_3d( + "L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) + + with pytest.raises(ValueError, match="wrong mode"): + im = Image.new("RGB", (10, 10), 0) + im.im.color_lut_3d( + "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) def test_correct_mode(self): im = Image.new("RGBA", (10, 10), 0) - im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(3, 3)) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) im = Image.new("RGBA", (10, 10), 0) - im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(4, 3)) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d("HSV", Image.LINEAR, *self.generate_identity_table(3, 3)) + im.im.color_lut_3d( + "HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) + ) im = Image.new("RGB", (10, 10), 0) - im.im.color_lut_3d("RGBA", Image.LINEAR, *self.generate_identity_table(4, 3)) + im.im.color_lut_3d( + "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) + ) def test_identities(self): g = Image.linear_gradient("L") im = Image.merge( - "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], ) # Fast test with small cubes @@ -152,7 +203,9 @@ class TestColorLut3DCoreAPI: im, im._new( im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, size) + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, size), ) ), ) @@ -162,7 +215,9 @@ class TestColorLut3DCoreAPI: im, im._new( im.im.color_lut_3d( - "RGB", Image.LINEAR, *self.generate_identity_table(3, (2, 2, 65)) + "RGB", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, (2, 2, 65)), ) ), ) @@ -170,7 +225,12 @@ class TestColorLut3DCoreAPI: def test_identities_4_channels(self): g = Image.linear_gradient("L") im = Image.merge( - "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], ) # Red channel copied to alpha @@ -178,7 +238,9 @@ class TestColorLut3DCoreAPI: Image.merge("RGBA", (im.split() * 2)[:4]), im._new( im.im.color_lut_3d( - "RGBA", Image.LINEAR, *self.generate_identity_table(4, 17) + "RGBA", + Image.Resampling.BILINEAR, + *self.generate_identity_table(4, 17), ) ), ) @@ -189,9 +251,9 @@ class TestColorLut3DCoreAPI: "RGBA", [ g, - g.transpose(Image.ROTATE_90), - g.transpose(Image.ROTATE_180), - g.transpose(Image.ROTATE_270), + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + g.transpose(Image.Transpose.ROTATE_270), ], ) @@ -199,7 +261,9 @@ class TestColorLut3DCoreAPI: im, im._new( im.im.color_lut_3d( - "RGBA", Image.LINEAR, *self.generate_identity_table(3, 17) + "RGBA", + Image.Resampling.BILINEAR, + *self.generate_identity_table(3, 17), ) ), ) @@ -207,14 +271,19 @@ class TestColorLut3DCoreAPI: def test_channels_order(self): g = Image.linear_gradient("L") im = Image.merge( - "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], ) # Reverse channels by splitting and using table # fmt: off assert_image_equal( Image.merge('RGB', im.split()[::-1]), - im._new(im.im.color_lut_3d('RGB', Image.LINEAR, + im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, 3, 2, 2, 2, [ 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, @@ -227,11 +296,16 @@ class TestColorLut3DCoreAPI: def test_overflow(self): g = Image.linear_gradient("L") im = Image.merge( - "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], ) # fmt: off - transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR, + transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, 3, 2, 2, 2, [ -1, -1, -1, 2, -1, -1, @@ -251,7 +325,7 @@ class TestColorLut3DCoreAPI: assert transformed[205, 205] == (255, 255, 0) # fmt: off - transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR, + transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR, 3, 2, 2, 2, [ -3, -3, -3, 5, -3, -3, @@ -354,7 +428,12 @@ class TestColorLut3DFilter: def test_numpy_formats(self): g = Image.linear_gradient("L") im = Image.merge( - "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], ) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), lambda r, g, b: (r, g, b)) @@ -445,7 +524,12 @@ class TestGenerateColorLut3D: g = Image.linear_gradient("L") im = Image.merge( - "RGB", [g, g.transpose(Image.ROTATE_90), g.transpose(Image.ROTATE_180)] + "RGB", + [ + g, + g.transpose(Image.Transpose.ROTATE_90), + g.transpose(Image.Transpose.ROTATE_180), + ], ) assert im == im.filter(lut) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 6c52d25a4..385192a3c 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -110,9 +110,9 @@ class TestCoreMemory: with pytest.raises(ValueError): Image.core.set_blocks_max(-1) - if sys.maxsize < 2 ** 32: + if sys.maxsize < 2**32: with pytest.raises(ValueError): - Image.core.set_blocks_max(2 ** 29) + Image.core.set_blocks_max(2**29) @pytest.mark.skipif(is_pypy(), reason="Images not collected") def test_set_blocks_max_stats(self): diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index d918ef941..b590a84c5 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -78,7 +78,7 @@ class TestDecompressionCrop: def teardown_class(self): Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - def testEnlargeCrop(self): + def test_enlarge_crop(self): # Crops can extend the extents, therefore we should have the # same decompression bomb warnings on them. with hopper() as src: @@ -86,21 +86,12 @@ class TestDecompressionCrop: pytest.warns(Image.DecompressionBombWarning, src.crop, box) def test_crop_decompression_checks(self): - im = Image.new("RGB", (100, 100)) - good_values = ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)) - - warning_values = ((-160, -160, 99, 99), (160, 160, -99, -99)) - - error_values = ((-99909, -99990, 99999, 99999), (99909, 99990, -99999, -99999)) - - for value in good_values: + for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): assert im.crop(value).size == (9, 9) - for value in warning_values: - pytest.warns(Image.DecompressionBombWarning, im.crop, value) + pytest.warns(Image.DecompressionBombWarning, im.crop, (-160, -160, 99, 99)) - for value in error_values: - with pytest.raises(Image.DecompressionBombError): - im.crop(value) + with pytest.raises(Image.DecompressionBombError): + im.crop((-99909, -99990, 99999, 99999)) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py new file mode 100644 index 000000000..30ed4a808 --- /dev/null +++ b/Tests/test_deprecate.py @@ -0,0 +1,91 @@ +import pytest + +from PIL import _deprecate + + +@pytest.mark.parametrize( + "version, expected", + [ + ( + 10, + "Old thing is deprecated and will be removed in Pillow 10 " + r"\(2023-07-01\)\. Use new thing instead\.", + ), + ( + None, + r"Old thing is deprecated and will be removed in a future version\. " + r"Use new thing instead\.", + ), + ], +) +def test_version(version, expected): + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old thing", version, "new thing") + + +def test_unknown_version(): + expected = r"Unknown removal version, update PIL\._deprecate\?" + with pytest.raises(ValueError, match=expected): + _deprecate.deprecate("Old thing", 12345, "new thing") + + +@pytest.mark.parametrize( + "deprecated, plural, expected", + [ + ( + "Old thing", + False, + r"Old thing is deprecated and should be removed\.", + ), + ( + "Old things", + True, + r"Old things are deprecated and should be removed\.", + ), + ], +) +def test_old_version(deprecated, plural, expected): + expected = r"" + with pytest.raises(RuntimeError, match=expected): + _deprecate.deprecate(deprecated, 1, plural=plural) + + +def test_plural(): + expected = ( + r"Old things are deprecated and will be removed in Pillow 10 \(2023-07-01\)\. " + r"Use new thing instead\." + ) + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old things", 10, "new thing", plural=True) + + +def test_replacement_and_action(): + expected = "Use only one of 'replacement' and 'action'" + with pytest.raises(ValueError, match=expected): + _deprecate.deprecate( + "Old thing", 10, replacement="new thing", action="Upgrade to new thing" + ) + + +@pytest.mark.parametrize( + "action", + [ + "Upgrade to new thing", + "Upgrade to new thing.", + ], +) +def test_action(action): + expected = ( + r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)\. " + r"Upgrade to new thing\." + ) + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old thing", 10, action=action) + + +def test_no_replacement_or_action(): + expected = ( + r"Old thing is deprecated and will be removed in Pillow 10 \(2023-07-01\)" + ) + with pytest.warns(DeprecationWarning, match=expected): + _deprecate.deprecate("Old thing", 10) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 15e007ca1..d1d5c85c1 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -120,9 +120,9 @@ def test_apng_dispose_op_previous_frame(): # save_all=True, # append_images=[green, blue], # disposal=[ - # PngImagePlugin.APNG_DISPOSE_OP_NONE, - # PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, - # PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS + # PngImagePlugin.Disposal.OP_NONE, + # PngImagePlugin.Disposal.OP_PREVIOUS, + # PngImagePlugin.Disposal.OP_PREVIOUS # ], # ) with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: @@ -441,6 +441,12 @@ def test_apng_save_duration_loop(tmp_path): assert im.n_frames == 1 assert im.info.get("duration") == 750 + # test info duration + frame.info["duration"] = 750 + frame.save(test_file, save_all=True) + with Image.open(test_file) as im: + assert im.info.get("duration") == 750 + def test_apng_save_disposal(tmp_path): test_file = str(tmp_path / "temp.png") @@ -449,31 +455,31 @@ def test_apng_save_disposal(tmp_path): green = Image.new("RGBA", size, (0, 255, 0, 255)) transparent = Image.new("RGBA", size, (0, 0, 0, 0)) - # test APNG_DISPOSE_OP_NONE + # test OP_NONE red.save( test_file, save_all=True, append_images=[green, transparent], - disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, + disposal=PngImagePlugin.Disposal.OP_NONE, + blend=PngImagePlugin.Blend.OP_OVER, ) with Image.open(test_file) as im: im.seek(2) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) - # test APNG_DISPOSE_OP_BACKGROUND + # test OP_BACKGROUND disposal = [ - PngImagePlugin.APNG_DISPOSE_OP_NONE, - PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, - PngImagePlugin.APNG_DISPOSE_OP_NONE, + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_BACKGROUND, + PngImagePlugin.Disposal.OP_NONE, ] red.save( test_file, save_all=True, append_images=[red, transparent], disposal=disposal, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, + blend=PngImagePlugin.Blend.OP_OVER, ) with Image.open(test_file) as im: im.seek(2) @@ -481,26 +487,26 @@ def test_apng_save_disposal(tmp_path): assert im.getpixel((64, 32)) == (0, 0, 0, 0) disposal = [ - PngImagePlugin.APNG_DISPOSE_OP_NONE, - PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND, + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_BACKGROUND, ] red.save( test_file, save_all=True, append_images=[green], disposal=disposal, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, + blend=PngImagePlugin.Blend.OP_OVER, ) with Image.open(test_file) as im: im.seek(1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) - # test APNG_DISPOSE_OP_PREVIOUS + # test OP_PREVIOUS disposal = [ - PngImagePlugin.APNG_DISPOSE_OP_NONE, - PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, - PngImagePlugin.APNG_DISPOSE_OP_NONE, + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_PREVIOUS, + PngImagePlugin.Disposal.OP_NONE, ] red.save( test_file, @@ -508,7 +514,7 @@ def test_apng_save_disposal(tmp_path): append_images=[green, red, transparent], default_image=True, disposal=disposal, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, + blend=PngImagePlugin.Blend.OP_OVER, ) with Image.open(test_file) as im: im.seek(3) @@ -516,21 +522,32 @@ def test_apng_save_disposal(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) disposal = [ - PngImagePlugin.APNG_DISPOSE_OP_NONE, - PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, + PngImagePlugin.Disposal.OP_NONE, + PngImagePlugin.Disposal.OP_PREVIOUS, ] red.save( test_file, save_all=True, append_images=[green], disposal=disposal, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, + blend=PngImagePlugin.Blend.OP_OVER, ) with Image.open(test_file) as im: im.seek(1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) + # test info disposal + red.info["disposal"] = PngImagePlugin.Disposal.OP_BACKGROUND + red.save( + test_file, + save_all=True, + append_images=[Image.new("RGBA", (10, 10), (0, 255, 0, 255))], + ) + with Image.open(test_file) as im: + im.seek(1) + assert im.getpixel((64, 32)) == (0, 0, 0, 0) + def test_apng_save_disposal_previous(tmp_path): test_file = str(tmp_path / "temp.png") @@ -539,12 +556,12 @@ def test_apng_save_disposal_previous(tmp_path): red = Image.new("RGBA", size, (255, 0, 0, 255)) green = Image.new("RGBA", size, (0, 255, 0, 255)) - # test APNG_DISPOSE_OP_NONE + # test OP_NONE transparent.save( test_file, save_all=True, append_images=[red, green], - disposal=PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS, + disposal=PngImagePlugin.Disposal.OP_PREVIOUS, ) with Image.open(test_file) as im: im.seek(2) @@ -559,17 +576,17 @@ def test_apng_save_blend(tmp_path): green = Image.new("RGBA", size, (0, 255, 0, 255)) transparent = Image.new("RGBA", size, (0, 0, 0, 0)) - # test APNG_BLEND_OP_SOURCE on solid color + # test OP_SOURCE on solid color blend = [ - PngImagePlugin.APNG_BLEND_OP_OVER, - PngImagePlugin.APNG_BLEND_OP_SOURCE, + PngImagePlugin.Blend.OP_OVER, + PngImagePlugin.Blend.OP_SOURCE, ] red.save( test_file, save_all=True, append_images=[red, green], default_image=True, - disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, + disposal=PngImagePlugin.Disposal.OP_NONE, blend=blend, ) with Image.open(test_file) as im: @@ -577,17 +594,17 @@ def test_apng_save_blend(tmp_path): assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) - # test APNG_BLEND_OP_SOURCE on transparent color + # test OP_SOURCE on transparent color blend = [ - PngImagePlugin.APNG_BLEND_OP_OVER, - PngImagePlugin.APNG_BLEND_OP_SOURCE, + PngImagePlugin.Blend.OP_OVER, + PngImagePlugin.Blend.OP_SOURCE, ] red.save( test_file, save_all=True, append_images=[red, transparent], default_image=True, - disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, + disposal=PngImagePlugin.Disposal.OP_NONE, blend=blend, ) with Image.open(test_file) as im: @@ -595,14 +612,14 @@ def test_apng_save_blend(tmp_path): assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) - # test APNG_BLEND_OP_OVER + # test OP_OVER red.save( test_file, save_all=True, append_images=[green, transparent], default_image=True, - disposal=PngImagePlugin.APNG_DISPOSE_OP_NONE, - blend=PngImagePlugin.APNG_BLEND_OP_OVER, + disposal=PngImagePlugin.Disposal.OP_NONE, + blend=PngImagePlugin.Blend.OP_OVER, ) with Image.open(test_file) as im: im.seek(1) @@ -611,3 +628,20 @@ def test_apng_save_blend(tmp_path): im.seek(2) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) + + # test info blend + red.info["blend"] = PngImagePlugin.Blend.OP_OVER + red.save(test_file, save_all=True, append_images=[green, transparent]) + with Image.open(test_file) as im: + im.seek(2) + assert im.getpixel((0, 0)) == (0, 255, 0, 255) + + +def test_constants_deprecation(): + for enum, prefix in { + PngImagePlugin.Disposal: "APNG_DISPOSE_", + PngImagePlugin.Blend: "APNG_BLEND_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(PngImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 15bd7e4f8..c1fae44ca 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,8 +1,18 @@ import pytest -from PIL import Image +from PIL import BlpImagePlugin, Image -from .helper import assert_image_equal_tofile +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, +) + + +def test_load_blp1(): + with Image.open("Tests/images/blp/blp1_jpeg.blp") as im: + assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") def test_load_blp2_raw(): @@ -20,6 +30,28 @@ def test_load_blp2_dxt1a(): assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") +def test_save(tmp_path): + f = str(tmp_path / "temp.blp") + + for version in ("BLP1", "BLP2"): + im = hopper("P") + im.save(f, blp_version=version) + + with Image.open(f) as reloaded: + assert_image_equal(im.convert("RGB"), reloaded) + + with Image.open("Tests/images/transparent.png") as im: + f = str(tmp_path / "temp.blp") + im.convert("P").save(f, blp_version=version) + + with Image.open(f) as reloaded: + assert_image_similar(im, reloaded, 8) + + im = hopper() + with pytest.raises(ValueError): + im.save(f) + + @pytest.mark.parametrize( "test_file", [ @@ -37,3 +69,14 @@ def test_crashes(test_file): with Image.open(f) as im: with pytest.raises(OSError): im.load() + + +def test_constants_deprecation(): + for enum, prefix in { + BlpImagePlugin.Format: "BLP_FORMAT_", + BlpImagePlugin.Encoding: "BLP_ENCODING_", + BlpImagePlugin.AlphaEncoding: "BLP_ALPHA_ENCODING_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(BlpImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 3374fe54e..f214fd6bd 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -4,7 +4,12 @@ import pytest from PIL import BmpImagePlugin, Image -from .helper import assert_image_equal, assert_image_equal_tofile, hopper +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar_tofile, + hopper, +) def test_sanity(tmp_path): @@ -123,3 +128,46 @@ def test_rgba_bitfields(): im = Image.merge("RGB", (r, g, b)) assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") + + +def test_rle8(): + with Image.open("Tests/images/hopper_rle8.bmp") as im: + assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) + + # This test image has been manually hexedited + # to have rows with too much data + with Image.open("Tests/images/hopper_rle8_row_overflow.bmp") as im: + assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) + + # Signal end of bitmap before the image is finished + with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp: + data = fp.read(1063) + b"\x01" + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(ValueError): + im.load() + + +@pytest.mark.parametrize( + "file_name,length", + ( + # EOF immediately after the header + ("Tests/images/hopper_rle8.bmp", 1078), + # EOF during delta + ("Tests/images/bmp/q/pal8rletrns.bmp", 3670), + # EOF when reading data in absolute mode + ("Tests/images/bmp/g/pal8rle.bmp", 1064), + ), +) +def test_rle8_eof(file_name, length): + with open(file_name, "rb") as fp: + data = fp.read(length) + with Image.open(io.BytesIO(data)) as im: + with pytest.raises(ValueError): + im.load() + + +def test_offset(): + # This image has been hexedited + # to exclude the palette size from the pixel data offset + with Image.open("Tests/images/pal8_offset.bmp") as im: + assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 11acc1c88..e330404d6 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -45,3 +45,35 @@ def test_save(tmp_path): # Act / Assert: stub cannot save without an implemented handler with pytest.raises(OSError): im.save(tmpfile) + + +def test_handler(tmp_path): + class TestHandler: + opened = False + loaded = False + saved = False + + def open(self, im): + self.opened = True + + def load(self, im): + self.loaded = True + return Image.new("RGB", (1, 1)) + + def save(self, im, fp, filename): + self.saved = True + + handler = TestHandler() + BufrStubImagePlugin.register_handler(handler) + with Image.open(TEST_FILE) as im: + assert handler.opened + assert not handler.loaded + + im.load() + assert handler.loaded + + temp_file = str(tmp_path / "temp.bufr") + im.save(temp_file) + assert handler.saved + + BufrStubImagePlugin._handler = None diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 58d5cbf1a..0f09c4b99 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import DcxImagePlugin, Image @@ -31,21 +33,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() - assert not record - def test_invalid_file(): with open("Tests/images/flower.jpg", "rb") as fp: diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 2f46ed77e..58447122e 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -196,6 +196,13 @@ def test__accept_false(): assert not output +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + DdsImagePlugin.DdsImageFile(invalid_file) + + def test_short_header(): """Check a short header""" with open(TEST_FILE_DXT5, "rb") as f: diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 4c0b96f73..1790f4f77 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -58,6 +58,15 @@ def test_sanity(): assert image2_scale2.format == "EPS" +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +def test_load(): + with Image.open(FILE1) as im: + assert im.load()[0, 0] == (255, 255, 255) + + # Test again now that it has already been loaded once + assert im.load()[0, 0] == (255, 255, 255) + + def test_invalid_file(): invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py new file mode 100644 index 000000000..447888acd --- /dev/null +++ b/Tests/test_file_fits.py @@ -0,0 +1,80 @@ +from io import BytesIO + +import pytest + +from PIL import FitsImagePlugin, FitsStubImagePlugin, Image + +from .helper import assert_image_equal, hopper + +TEST_FILE = "Tests/images/hopper.fits" + + +def test_open(): + # Act + with Image.open(TEST_FILE) as im: + + # Assert + assert im.format == "FITS" + assert im.size == (128, 128) + assert im.mode == "L" + + assert_image_equal(im, hopper("L")) + + +def test_invalid_file(): + # Arrange + invalid_file = "Tests/images/flower.jpg" + + # Act / Assert + with pytest.raises(SyntaxError): + FitsImagePlugin.FitsImageFile(invalid_file) + + +def test_truncated_fits(): + # No END to headers + image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE" + with pytest.raises(OSError): + FitsImagePlugin.FitsImageFile(BytesIO(image_data)) + + +def test_naxis_zero(): + # This test image has been manually hexedited + # to set the number of data axes to zero + with pytest.raises(ValueError): + with Image.open("Tests/images/hopper_naxis_zero.fits"): + pass + + +def test_stub_deprecated(): + class Handler: + opened = False + loaded = False + + def open(self, im): + self.opened = True + + def load(self, im): + self.loaded = True + return Image.new("RGB", (1, 1)) + + handler = Handler() + with pytest.warns(DeprecationWarning): + FitsStubImagePlugin.register_handler(handler) + + with Image.open(TEST_FILE) as im: + assert im.format == "FITS" + assert im.size == (128, 128) + assert im.mode == "L" + + assert handler.opened + assert not handler.loaded + + im.load() + assert handler.loaded + + FitsStubImagePlugin._handler = None + Image.register_open( + FitsImagePlugin.FitsImageFile.format, + FitsImagePlugin.FitsImageFile, + FitsImagePlugin._accept, + ) diff --git a/Tests/test_file_fitsstub.py b/Tests/test_file_fitsstub.py deleted file mode 100644 index c77457947..000000000 --- a/Tests/test_file_fitsstub.py +++ /dev/null @@ -1,63 +0,0 @@ -from io import BytesIO - -import pytest - -from PIL import FitsStubImagePlugin, Image - -TEST_FILE = "Tests/images/hopper.fits" - - -def test_open(): - # Act - with Image.open(TEST_FILE) as im: - - # Assert - assert im.format == "FITS" - assert im.size == (128, 128) - assert im.mode == "L" - - -def test_invalid_file(): - # Arrange - invalid_file = "Tests/images/flower.jpg" - - # Act / Assert - with pytest.raises(SyntaxError): - FitsStubImagePlugin.FITSStubImageFile(invalid_file) - - -def test_load(): - # Arrange - with Image.open(TEST_FILE) as im: - - # Act / Assert: stub cannot load without an implemented handler - with pytest.raises(OSError): - im.load() - - -def test_truncated_fits(): - # No END to headers - image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE" - with pytest.raises(OSError): - FitsStubImagePlugin.FITSStubImageFile(BytesIO(image_data)) - - -def test_naxis_zero(): - # This test image has been manually hexedited - # to set the number of data axes to zero - with pytest.raises(ValueError): - with Image.open("Tests/images/hopper_naxis_zero.fits"): - pass - - -def test_save(): - # Arrange - with Image.open(TEST_FILE) as im: - dummy_fp = None - dummy_filename = "dummy.filename" - - # Act / Assert: stub cannot save without an implemented handler - with pytest.raises(OSError): - im.save(dummy_filename) - with pytest.raises(OSError): - FitsStubImagePlugin._save(im, dummy_fp, dummy_filename) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 1c1abf2b1..c1ad4a7f0 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import FliImagePlugin, Image @@ -38,21 +40,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(static_test_file) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(static_test_file) as im: im.load() - assert not record - def test_tell(): # Arrange @@ -138,3 +136,16 @@ def test_timeouts(test_file): with Image.open(f) as im: with pytest.raises(OSError): im.load() + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/crash-5762152299364352.fli", + ], +) +def test_crash(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index f76fd895a..cae20fa46 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,4 +1,6 @@ -from PIL import Image +import pytest + +from PIL import FtexImagePlugin, Image from .helper import assert_image_equal_tofile, assert_image_similar @@ -12,3 +14,19 @@ def test_load_dxt1(): with Image.open("Tests/images/ftex_dxt1.ftc") as im: with Image.open("Tests/images/ftex_dxt1.png") as target: assert_image_similar(im, target.convert("RGBA"), 15) + + +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + FtexImagePlugin.FtexImageFile(invalid_file) + + +def test_constants_deprecation(): + for enum, prefix in { + FtexImagePlugin.Format: "FORMAT_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(FtexImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index 8d7fcf147..1ea8af8ee 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -5,20 +5,28 @@ from PIL import GbrImagePlugin, Image from .helper import assert_image_equal_tofile -def test_invalid_file(): - invalid_file = "Tests/images/flower.jpg" - - with pytest.raises(SyntaxError): - GbrImagePlugin.GbrImageFile(invalid_file) - - def test_gbr_file(): with Image.open("Tests/images/gbr.gbr") as im: assert_image_equal_tofile(im, "Tests/images/gbr.png") +def test_load(): + with Image.open("Tests/images/gbr.gbr") as im: + assert im.load()[0, 0] == (0, 0, 0, 0) + + # Test again now that it has already been loaded once + assert im.load()[0, 0] == (0, 0, 0, 0) + + def test_multiple_load_operations(): with Image.open("Tests/images/gbr.gbr") as im: im.load() im.load() assert_image_equal_tofile(im, "Tests/images/gbr.png") + + +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + GbrImagePlugin.GbrImageFile(invalid_file) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 56da68d60..fd30cded0 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,3 +1,4 @@ +import warnings from io import BytesIO import pytest @@ -39,21 +40,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(TEST_GIF) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(TEST_GIF) as im: im.load() - assert not record - def test_invalid_file(): invalid_file = "Tests/images/flower.jpg" @@ -62,6 +59,51 @@ def test_invalid_file(): GifImagePlugin.GifImageFile(invalid_file) +def test_l_mode_transparency(): + with Image.open("Tests/images/no_palette_with_transparency.gif") as im: + assert im.mode == "L" + assert im.load()[0, 0] == 128 + assert im.info["transparency"] == 255 + + im.seek(1) + assert im.mode == "L" + assert im.load()[0, 0] == 128 + + +def test_strategy(): + with Image.open("Tests/images/chi.gif") as im: + expected_zero = im.convert("RGB") + + im.seek(1) + expected_one = im.convert("RGB") + + try: + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS + with Image.open("Tests/images/chi.gif") as im: + assert im.mode == "RGB" + assert_image_equal(im, expected_zero) + + GifImagePlugin.LOADING_STRATEGY = ( + GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY + ) + # Stay in P mode with only a global palette + with Image.open("Tests/images/chi.gif") as im: + assert im.mode == "P" + + im.seek(1) + assert im.mode == "P" + assert_image_equal(im.convert("RGB"), expected_one) + + # Change to RGB mode when a frame has an individual palette + with Image.open("Tests/images/iss634.gif") as im: + assert im.mode == "P" + + im.seek(1) + assert im.mode == "RGB" + finally: + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST + + def test_optimize(): def test_grayscale(optimize): im = Image.new("L", (1, 1), 0) @@ -163,6 +205,32 @@ def test_roundtrip_save_all(tmp_path): assert reread.n_frames == 5 +@pytest.mark.parametrize( + "path, mode", + ( + ("Tests/images/dispose_bgnd.gif", "RGB"), + # Hexeditted copy of dispose_bgnd to add transparency + ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), + ), +) +def test_loading_multiple_palettes(path, mode): + with Image.open(path) as im: + assert im.mode == "P" + first_frame_colors = im.palette.colors.keys() + original_color = im.convert("RGB").load()[0, 0] + + im.seek(1) + assert im.mode == mode + if mode == "RGBA": + im = im.convert("RGB") + + # Check a color only from the old palette + assert im.load()[0, 0] == original_color + + # Check a color from the new palette + assert im.load()[24, 24] not in first_frame_colors + + def test_headers_saving_for_animated_gifs(tmp_path): important_headers = ["background", "version", "duration", "loop"] # Multiframe image @@ -184,8 +252,8 @@ def test_palette_handling(tmp_path): with Image.open(TEST_GIF) as im: im = im.convert("RGB") - im = im.resize((100, 100), Image.LANCZOS) - im2 = im.convert("P", palette=Image.ADAPTIVE, colors=256) + im = im.resize((100, 100), Image.Resampling.LANCZOS) + im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) f = str(tmp_path / "temp.gif") im2.save(f, optimize=True) @@ -285,6 +353,22 @@ def test_n_frames(): assert im.is_animated == (n_frames != 1) +def test_no_change(): + # Test n_frames does not change the image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + im.seek(1) + expected = im.copy() + assert im.n_frames == 5 + assert_image_equal(im, expected) + + # Test is_animated does not change the image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + im.seek(3) + expected = im.copy() + assert im.is_animated + assert_image_equal(im, expected) + + def test_eoferror(): with Image.open(TEST_GIF) as im: n_frames = im.n_frames @@ -324,7 +408,7 @@ def test_dispose_none_load_end(): with Image.open("Tests/images/dispose_none_load_end.gif") as img: img.seek(1) - assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.gif") + assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png") def test_dispose_background(): @@ -337,14 +421,45 @@ def test_dispose_background(): pass -def test_transparent_dispose(): - expected_colors = [(2, 1, 2), (0, 1, 0), (2, 1, 2)] - with Image.open("Tests/images/transparent_dispose.gif") as img: - for frame in range(3): - img.seek(frame) - for x in range(3): - color = img.getpixel((x, 0)) - assert color == expected_colors[frame][x] +def test_dispose_background_transparency(): + with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: + img.seek(2) + px = img.load() + assert px[35, 30][3] == 0 + + +@pytest.mark.parametrize( + "loading_strategy, expected_colors", + ( + ( + GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST, + ( + (2, 1, 2), + ((0, 255, 24, 255), (0, 0, 255, 255), (0, 255, 24, 255)), + ((0, 0, 0, 0), (0, 0, 255, 255), (0, 0, 0, 0)), + ), + ), + ( + GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY, + ( + (2, 1, 2), + (0, 1, 0), + (2, 1, 2), + ), + ), + ), +) +def test_transparent_dispose(loading_strategy, expected_colors): + GifImagePlugin.LOADING_STRATEGY = loading_strategy + try: + with Image.open("Tests/images/transparent_dispose.gif") as img: + for frame in range(3): + img.seek(frame) + for x in range(3): + color = img.getpixel((x, 0)) + assert color == expected_colors[frame][x] + finally: + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST def test_dispose_previous(): @@ -361,7 +476,7 @@ def test_dispose_previous_first_frame(): with Image.open("Tests/images/dispose_prev_first_frame.gif") as im: im.seek(1) assert_image_equal_tofile( - im, "Tests/images/dispose_prev_first_frame_seeked.gif" + im, "Tests/images/dispose_prev_first_frame_seeked.png" ) @@ -501,7 +616,7 @@ def test_dispose2_background(tmp_path): with Image.open(out) as im: im.seek(1) - assert im.getpixel((0, 0)) == 0 + assert im.getpixel((0, 0)) == (255, 0, 0) def test_transparency_in_second_frame(): @@ -510,9 +625,9 @@ def test_transparency_in_second_frame(): # Seek to the second frame im.seek(im.tell() + 1) - assert im.info["transparency"] == 0 + assert "transparency" not in im.info - assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.gif") + assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png") def test_no_transparency_in_second_frame(): @@ -684,31 +799,31 @@ def test_zero_comment_subblocks(): def test_version(tmp_path): out = str(tmp_path / "temp.gif") - def assertVersionAfterSave(im, version): + def assert_version_after_save(im, version): im.save(out) with Image.open(out) as reread: assert reread.info["version"] == version # Test that GIF87a is used by default im = Image.new("L", (100, 100), "#000") - assertVersionAfterSave(im, b"GIF87a") + assert_version_after_save(im, b"GIF87a") # Test setting the version to 89a im = Image.new("L", (100, 100), "#000") im.info["version"] = b"89a" - assertVersionAfterSave(im, b"GIF89a") + assert_version_after_save(im, b"GIF89a") # Test that adding a GIF89a feature changes the version im.info["transparency"] = 1 - assertVersionAfterSave(im, b"GIF89a") + assert_version_after_save(im, b"GIF89a") # Test that a GIF87a image is also saved in that format with Image.open("Tests/images/test.colors.gif") as im: - assertVersionAfterSave(im, b"GIF87a") + assert_version_after_save(im, b"GIF87a") # Test that a GIF89a image is also saved in that format im.info["version"] = b"GIF89a" - assertVersionAfterSave(im, b"GIF87a") + assert_version_after_save(im, b"GIF87a") def test_append_images(tmp_path): @@ -723,10 +838,10 @@ def test_append_images(tmp_path): assert reread.n_frames == 3 # Tests appending using a generator - def imGenerator(ims): + def im_generator(ims): yield from ims - im.save(out, save_all=True, append_images=imGenerator(ims)) + im.save(out, save_all=True, append_images=im_generator(ims)) with Image.open(out) as reread: assert reread.n_frames == 3 @@ -781,6 +896,17 @@ def test_rgb_transparency(tmp_path): assert "transparency" not in reloaded.info +def test_rgba_transparency(tmp_path): + out = str(tmp_path / "temp.gif") + + im = hopper("P") + im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)]) + + with Image.open(out) as reloaded: + reloaded.seek(1) + assert_image_equal(hopper("P").convert("RGB"), reloaded) + + def test_bbox(tmp_path): out = str(tmp_path / "temp.gif") @@ -811,7 +937,7 @@ def test_palette_save_P(tmp_path): # Forcing a non-straight grayscale palette. im = hopper("P") - palette = bytes([255 - i // 3 for i in range(768)]) + palette = bytes(255 - i // 3 for i in range(768)) out = str(tmp_path / "temp.gif") im.save(out, palette=palette) @@ -856,7 +982,7 @@ def test_palette_save_ImagePalette(tmp_path): with Image.open(out) as reloaded: im.putpalette(palette) - assert_image_equal(reloaded, im) + assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) def test_save_I(tmp_path): @@ -874,11 +1000,11 @@ def test_save_I(tmp_path): def test_getdata(): # Test getheader/getdata against legacy values. # Create a 'P' image with holes in the palette. - im = Image._wedge().resize((16, 16), Image.NEAREST) + im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) im.putpalette(ImagePalette.ImagePalette("RGB")) im.info = {"background": 0} - passed_palette = bytes([255 - i // 3 for i in range(768)]) + passed_palette = bytes(255 - i // 3 for i in range(768)) GifImagePlugin._FORCE_OPTIMIZE = True try: @@ -910,6 +1036,11 @@ def test_lzw_bits(): def test_extents(): with Image.open("Tests/images/test_extents.gif") as im: assert im.size == (100, 100) + + # Check that n_frames does not change the size + assert im.n_frames == 2 + assert im.size == (100, 100) + im.seek(1) assert im.size == (150, 150) @@ -919,4 +1050,14 @@ def test_missing_background(): # but the disposal method is "Restore to background color" with Image.open("Tests/images/missing_background.gif") as im: im.seek(1) - assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.gif") + assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png") + + +def test_saving_rgba(tmp_path): + out = str(tmp_path / "temp.gif") + with Image.open("Tests/images/transparent.png") as im: + im.save(out) + + with Image.open(out) as reloaded: + reloaded_rgba = reloaded.convert("RGBA") + assert reloaded_rgba.load()[0, 0][3] == 0 diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index e4930d8dc..fd427746e 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -45,3 +45,35 @@ def test_save(tmp_path): # Act / Assert: stub cannot save without an implemented handler with pytest.raises(OSError): im.save(tmpfile) + + +def test_handler(tmp_path): + class TestHandler: + opened = False + loaded = False + saved = False + + def open(self, im): + self.opened = True + + def load(self, im): + self.loaded = True + return Image.new("RGB", (1, 1)) + + def save(self, im, fp, filename): + self.saved = True + + handler = TestHandler() + GribStubImagePlugin.register_handler(handler) + with Image.open(TEST_FILE) as im: + assert handler.opened + assert not handler.loaded + + im.load() + assert handler.loaded + + temp_file = str(tmp_path / "temp.grib") + im.save(temp_file) + assert handler.saved + + GribStubImagePlugin._handler = None diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index ff3397055..20b4b9619 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -46,3 +46,35 @@ def test_save(): im.save(dummy_filename) with pytest.raises(OSError): Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename) + + +def test_handler(tmp_path): + class TestHandler: + opened = False + loaded = False + saved = False + + def open(self, im): + self.opened = True + + def load(self, im): + self.loaded = True + return Image.new("RGB", (1, 1)) + + def save(self, im, fp, filename): + self.saved = True + + handler = TestHandler() + Hdf5StubImagePlugin.register_handler(handler) + with Image.open(TEST_FILE) as im: + assert handler.opened + assert not handler.loaded + + im.load() + assert handler.loaded + + temp_file = str(tmp_path / "temp.h5") + im.save(temp_file) + assert handler.saved + + Hdf5StubImagePlugin._handler = None diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 47de38d06..7d8f89184 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,8 +1,10 @@ import io +import os +import warnings import pytest -from PIL import IcnsImagePlugin, Image, features +from PIL import IcnsImagePlugin, Image, _binary, features from .helper import assert_image_equal, assert_image_similar_tofile @@ -18,15 +20,22 @@ def test_sanity(): with Image.open(TEST_FILE) as im: # Assert that there is no unclosed file warning - with pytest.warns(None) as record: + with warnings.catch_warnings(): im.load() - assert not record assert im.mode == "RGBA" assert im.size == (1024, 1024) assert im.format == "ICNS" +def test_load(): + with Image.open(TEST_FILE) as im: + assert im.load()[0, 0] == (0, 0, 0, 0) + + # Test again now that it has already been loaded once + assert im.load()[0, 0] == (0, 0, 0, 0) + + def test_save(tmp_path): temp_file = str(tmp_path / "temp.icns") @@ -38,6 +47,11 @@ def test_save(tmp_path): assert reread.size == (1024, 1024) assert reread.format == "ICNS" + file_length = os.path.getsize(temp_file) + with open(temp_file, "rb") as fp: + fp.seek(4) + assert _binary.i32be(fp.read(4)) == file_length + def test_save_append_images(tmp_path): temp_file = str(tmp_path / "temp.icns") @@ -98,12 +112,9 @@ def test_older_icon(): def test_jp2_icon(): - # This icon was made by using Uli Kusterer's oldiconutil to replace - # the PNG images with JPEG 2000 ones. The advantage of doing this is - # that OS X 10.5 supports JPEG 2000 but not PNG; some commercial - # software therefore does just this. - - # (oldiconutil is here: https://github.com/uliwitness/oldiconutil) + # This icon uses JPEG 2000 images instead of the PNG images. + # The advantage of doing this is that OS X 10.5 supports JPEG 2000 + # but not PNG; some commercial software therefore does just this. if not ENABLE_JPEG2K: return diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 8060d1b76..3fcd5c61f 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,4 +1,5 @@ import io +import os import pytest @@ -18,6 +19,16 @@ def test_sanity(): assert im.get_format_mimetype() == "image/x-icon" +def test_load(): + with Image.open(TEST_ICO_FILE) as im: + assert im.load()[0, 0] == (1, 1, 9, 255) + + +def test_mask(): + with Image.open("Tests/images/hopper_mask.ico") as im: + assert_image_equal_tofile(im, "Tests/images/hopper_mask.png") + + def test_black_and_white(): with Image.open("Tests/images/black_and_white.ico") as im: assert im.mode == "RGBA" @@ -43,7 +54,9 @@ def test_save_to_bytes(): assert im.mode == reloaded.mode assert (64, 64) == reloaded.size assert reloaded.format == "ICO" - assert_image_equal(reloaded, hopper().resize((64, 64), Image.LANCZOS)) + assert_image_equal( + reloaded, hopper().resize((64, 64), Image.Resampling.LANCZOS) + ) # The other one output.seek(0) @@ -53,7 +66,56 @@ def test_save_to_bytes(): assert im.mode == reloaded.mode assert (32, 32) == reloaded.size assert reloaded.format == "ICO" - assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) + assert_image_equal( + reloaded, hopper().resize((32, 32), Image.Resampling.LANCZOS) + ) + + +def test_no_duplicates(tmp_path): + temp_file = str(tmp_path / "temp.ico") + temp_file2 = str(tmp_path / "temp2.ico") + + im = hopper() + sizes = [(32, 32), (64, 64)] + im.save(temp_file, "ico", sizes=sizes) + + sizes.append(sizes[-1]) + im.save(temp_file2, "ico", sizes=sizes) + + assert os.path.getsize(temp_file) == os.path.getsize(temp_file2) + + +def test_different_bit_depths(tmp_path): + temp_file = str(tmp_path / "temp.ico") + temp_file2 = str(tmp_path / "temp2.ico") + + im = hopper() + im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)]) + + hopper("1").save( + temp_file2, + "ico", + bitmap_format="bmp", + sizes=[(128, 128)], + append_images=[im], + ) + + assert os.path.getsize(temp_file) != os.path.getsize(temp_file2) + + # Test that only matching sizes of different bit depths are saved + temp_file3 = str(tmp_path / "temp3.ico") + temp_file4 = str(tmp_path / "temp4.ico") + + im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)]) + im.save( + temp_file4, + "ico", + bitmap_format="bmp", + sizes=[(128, 128)], + append_images=[Image.new("P", (64, 64))], + ) + + assert os.path.getsize(temp_file3) == os.path.getsize(temp_file4) @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) @@ -70,7 +132,7 @@ def test_save_to_bytes_bmp(mode): assert "RGBA" == reloaded.mode assert (64, 64) == reloaded.size assert reloaded.format == "ICO" - im = hopper(mode).resize((64, 64), Image.LANCZOS).convert("RGBA") + im = hopper(mode).resize((64, 64), Image.Resampling.LANCZOS).convert("RGBA") assert_image_equal(reloaded, im) # The other one @@ -81,7 +143,7 @@ def test_save_to_bytes_bmp(mode): assert "RGBA" == reloaded.mode assert (32, 32) == reloaded.size assert reloaded.format == "ICO" - im = hopper(mode).resize((32, 32), Image.LANCZOS).convert("RGBA") + im = hopper(mode).resize((32, 32), Image.Resampling.LANCZOS).convert("RGBA") assert_image_equal(reloaded, im) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 9d25a4d1a..675210c30 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,4 +1,5 @@ import filecmp +import warnings import pytest @@ -35,21 +36,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(TEST_IM) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(TEST_IM) as im: im.load() - assert not record - def test_tell(): # Arrange diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 13d99c15d..203065802 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,5 +1,6 @@ import os import re +import warnings from io import BytesIO import pytest @@ -67,6 +68,13 @@ class TestFileJpeg: assert im.format == "JPEG" assert im.get_format_mimetype() == "image/jpeg" + @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) + def test_zero(self, size, tmp_path): + f = str(tmp_path / "temp.jpg") + im = Image.new("RGB", size) + with pytest.raises(ValueError): + im.save(f) + def test_app(self): # Test APP/COM reader (@PIL135) with Image.open(TEST_FILE) as im: @@ -85,26 +93,26 @@ class TestFileJpeg: f = "Tests/images/pil_sample_cmyk.jpg" with Image.open(f) as im: # the source image has red pixels in the upper left corner. - c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] + c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) assert c == 0.0 assert m > 0.8 assert y > 0.8 assert k == 0.0 # the opposite corner is black - c, m, y, k = [ + c, m, y, k = ( x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ] + ) assert k > 0.9 # roundtrip, and check again im = self.roundtrip(im) - c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] + c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) assert c == 0.0 assert m > 0.8 assert y > 0.8 assert k == 0.0 - c, m, y, k = [ + c, m, y, k = ( x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ] + ) assert k > 0.9 @pytest.mark.parametrize( @@ -271,7 +279,7 @@ class TestFileJpeg: del exif[0x8769] # Assert that it needs to be transposed - assert exif[0x0112] == Image.TRANSVERSE + assert exif[0x0112] == Image.Transpose.TRANSVERSE # Assert that the GPS IFD is present and empty assert exif.get_ifd(0x8825) == {} @@ -756,9 +764,8 @@ class TestFileJpeg: assert exif[282] == 180 out = str(tmp_path / "out.jpg") - with pytest.warns(None) as record: + with warnings.catch_warnings(): im.save(out, exif=exif) - assert not record with Image.open(out) as reloaded: assert reloaded.getexif()[282] == 180 @@ -870,6 +877,30 @@ class TestFileJpeg: with Image.open("Tests/images/hopper.jpg") as im: assert im.getxmp() == {} + @pytest.mark.timeout(timeout=1) + def test_eof(self): + # Even though this decoder never says that it is finished + # the image should still end when there is no new data + class InfiniteMockPyDecoder(ImageFile.PyDecoder): + def decode(self, buffer): + return 0, 0 + + decoder = InfiniteMockPyDecoder(None) + + def closure(mode, *args): + decoder.__init__(mode, *args) + return decoder + + Image.register_decoder("INFINITE", closure) + + with Image.open(TEST_FILE) as im: + im.tile = [ + ("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), + ] + ImageFile.LOAD_TRUNCATED_IMAGES = True + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = False + @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 2abb2bcfb..677a14981 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -10,7 +10,6 @@ from .helper import ( assert_image_equal, assert_image_similar, assert_image_similar_tofile, - is_big_endian, skip_unless_feature, ) @@ -31,9 +30,9 @@ def roundtrip(im, **options): im.save(out, "JPEG2000", **options) test_bytes = out.tell() out.seek(0) - im = Image.open(out) - im.bytes = test_bytes # for testing only - im.load() + with Image.open(out) as im: + im.bytes = test_bytes # for testing only + im.load() return im @@ -159,6 +158,16 @@ def test_load_dpi(): assert "dpi" not in im.info +def test_restricted_icc_profile(): + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + # JPEG2000 image with a restricted ICC profile and a known colorspace + with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im: + assert im.mode == "RGB" + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + + def test_header_errors(): for path in ( "Tests/images/invalid_header_length.jp2", @@ -200,6 +209,49 @@ def test_layers(): assert_image_similar(im, test_card, 0.4) +@pytest.mark.parametrize( + "name, args, offset, data", + ( + ("foo.j2k", {}, 0, b"\xff\x4f"), + ("foo.jp2", {}, 4, b"jP"), + (None, {"no_jp2": True}, 0, b"\xff\x4f"), + ("foo.j2k", {"no_jp2": True}, 0, b"\xff\x4f"), + ("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"), + ("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"), + ("foo.jp2", {"no_jp2": False}, 4, b"jP"), + ("foo.jp2", {"no_jp2": False}, 4, b"jP"), + ), +) +def test_no_jp2(name, args, offset, data): + out = BytesIO() + if name: + out.name = name + test_card.save(out, "JPEG2000", **args) + out.seek(offset) + assert out.read(2) == data + + +def test_mct(): + # Three component + for val in (0, 1): + out = BytesIO() + test_card.save(out, "JPEG2000", mct=val, no_jp2=True) + + assert out.getvalue()[59] == val + with Image.open(out) as im: + assert_image_similar(im, test_card, 1.0e-3) + + # Single component should have MCT disabled + for val in (0, 1): + out = BytesIO() + with Image.open("Tests/images/16bit.cropped.jp2") as jp2: + jp2.save(out, "JPEG2000", mct=val, no_jp2=True) + + assert out.getvalue()[53] == 0 + with Image.open(out) as im: + assert_image_similar(im, jp2, 1.0e-3) + + def test_rgba(): # Arrange with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: @@ -224,13 +276,11 @@ def test_16bit_monochrome_has_correct_mode(): assert jp2.mode == "I;16" -@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") def test_16bit_monochrome_jp2_like_tiff(): with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.jp2", 1e-3) -@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") def test_16bit_monochrome_j2k_like_tiff(): with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.j2k", 1e-3) @@ -284,7 +334,7 @@ def test_subsampling_decode(name): # RGB reference images are downscaled epsilon = 3e-3 width, height = width * 2, height * 2 - expected = im2.resize((width, height), Image.NEAREST) + expected = im2.resize((width, height), Image.Resampling.NEAREST) assert_image_similar(im, expected, epsilon) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 1d0c93f06..d83c584b5 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -4,12 +4,11 @@ import itertools import os import re from collections import namedtuple -from ctypes import c_float import pytest from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features -from PIL.TiffImagePlugin import STRIPOFFSETS, SUBIFD +from PIL.TiffImagePlugin import SAMPLEFORMAT, STRIPOFFSETS, SUBIFD from .helper import ( assert_image_equal, @@ -112,7 +111,7 @@ class TestFileLibTiff(LibTiffTestCase): test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: out = str(tmp_path / "temp.tif") - rot = orig.transpose(Image.ROTATE_90) + rot = orig.transpose(Image.Transpose.ROTATE_90) assert rot.size == (500, 500) rot.save(out) @@ -168,14 +167,11 @@ class TestFileLibTiff(LibTiffTestCase): val = original[tag] if tag.endswith("Resolution"): if legacy_api: - assert ( - c_float(val[0][0] / val[0][1]).value - == c_float(value[0][0] / value[0][1]).value + assert val[0][0] / val[0][1] == ( + 4294967295 / 113653537 ), f"{tag} didn't roundtrip" else: - assert ( - c_float(val).value == c_float(value).value - ), f"{tag} didn't roundtrip" + assert val == 37.79000115940079, f"{tag} didn't roundtrip" else: assert val == value, f"{tag} didn't roundtrip" @@ -218,7 +214,7 @@ class TestFileLibTiff(LibTiffTestCase): values = { 2: "test", 3: 1, - 4: 2 ** 20, + 4: 2**20, 5: TiffImagePlugin.IFDRational(100, 1), 12: 1.05, } @@ -825,6 +821,17 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") + def test_sampleformat_write(self, tmp_path): + im = Image.new("F", (1, 1)) + out = str(tmp_path / "temp.tif") + TiffImagePlugin.WRITE_LIBTIFF = True + im.save(out) + TiffImagePlugin.WRITE_LIBTIFF = False + + with Image.open(out) as reloaded: + assert reloaded.mode == "F" + assert reloaded.getexif()[SAMPLEFORMAT] == 3 + def test_lzw(self): with Image.open("Tests/images/hopper_lzw.tif") as im: assert im.mode == "RGB" @@ -920,6 +927,23 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/tiff_strip_planar_16bit_RGBa.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + @pytest.mark.parametrize("compression", (None, "jpeg")) + def test_block_tile_tags(self, compression, tmp_path): + im = hopper() + out = str(tmp_path / "temp.tif") + + tags = { + TiffImagePlugin.TILEWIDTH: 256, + TiffImagePlugin.TILELENGTH: 256, + TiffImagePlugin.TILEOFFSETS: 256, + TiffImagePlugin.TILEBYTECOUNTS: 256, + } + im.save(out, exif=tags, compression=compression) + + with Image.open(out) as reloaded: + for tag in tags.keys(): + assert tag not in reloaded.getexif() + def test_old_style_jpeg(self): with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") @@ -986,3 +1010,24 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as im: # Assert that there are multiple strips assert len(im.tag_v2[STRIPOFFSETS]) > 1 + + def test_save_single_strip(self, tmp_path): + im = hopper("RGB").resize((256, 256)) + out = str(tmp_path / "temp.tif") + + TiffImagePlugin.STRIP_SIZE = 2**18 + try: + + im.save(out, compression="tiff_adobe_deflate") + + with Image.open(out) as im: + assert len(im.tag_v2[STRIPOFFSETS]) == 1 + finally: + TiffImagePlugin.STRIP_SIZE = 65536 + + @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) + def test_save_zero(self, compression, tmp_path): + im = Image.new("RGB", (0, 0)) + out = str(tmp_path / "temp.tif") + with pytest.raises(SystemError): + im.save(out, compression=compression) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 9de096458..ca3ea8419 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,3 +1,4 @@ +import warnings from io import BytesIO import pytest @@ -41,21 +42,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(test_files[0]) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(test_files[0]) as im: im.load() - assert not record - def test_app(): for test_file in test_files: @@ -88,6 +85,9 @@ def test_frame_size(): im.seek(1) assert im.size == (680, 480) + im.seek(0) + assert im.size == (640, 480) + def test_ignore_frame_size(): # Ignore the different size of the second frame @@ -145,10 +145,10 @@ def test_mp_attribute(): for test_file in test_files: with Image.open(test_file) as im: mpinfo = im._getmp() - frameNumber = 0 + frame_number = 0 for mpentry in mpinfo[0xB002]: mpattr = mpentry["Attribute"] - if frameNumber: + if frame_number: assert not mpattr["RepresentativeImageFlag"] else: assert mpattr["RepresentativeImageFlag"] @@ -157,7 +157,7 @@ def test_mp_attribute(): assert mpattr["ImageDataFormat"] == "JPEG" assert mpattr["MPType"] == "Multi-Frame Image: (Disparity)" assert mpattr["Reserved"] == 0 - frameNumber += 1 + frame_number += 1 def test_seek(): diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 40a027cc5..c71d4f5f2 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -131,10 +131,10 @@ def test_save_all(tmp_path): assert os.path.getsize(outfile) > 0 # Test appending using a generator - def imGenerator(ims): + def im_generator(ims): yield from ims - im.save(outfile, save_all=True, append_images=imGenerator(ims)) + im.save(outfile, save_all=True, append_images=im_generator(ims)) assert os.path.isfile(outfile) assert os.path.getsize(outfile) > 0 @@ -253,9 +253,9 @@ def test_pdf_append(tmp_path): check_pdf_pages_consistency(pdf) # append two images - mode_CMYK = hopper("CMYK") - mode_P = hopper("P") - mode_CMYK.save(pdf_filename, append=True, save_all=True, append_images=[mode_P]) + mode_cmyk = hopper("CMYK") + mode_p = hopper("P") + mode_cmyk.save(pdf_filename, append=True, save_all=True, append_images=[mode_p]) # open the PDF again, check pages and info again with PdfParser.PdfParser(pdf_filename) as pdf: @@ -313,8 +313,9 @@ def test_pdf_append_to_bytesio(): @pytest.mark.timeout(1) -def test_redos(): - malicious = b" trailer<<>>" + b"\n" * 3456 +@pytest.mark.parametrize("newline", (b"\r", b"\n")) +def test_redos(newline): + malicious = b" trailer<<>>" + newline * 3456 # This particular exception isn't relevant here. # The important thing is it doesn't timeout, cause a ReDoS (CVE-2021-25292). diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ffacbbbf4..bb2b0d119 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,5 +1,6 @@ import re import sys +import warnings import zlib from io import BytesIO @@ -13,7 +14,6 @@ from .helper import ( assert_image_equal, assert_image_equal_tofile, hopper, - is_big_endian, is_win32, mark_if_feature_version, skip_unless_feature, @@ -77,7 +77,6 @@ class TestFilePng: png.crc(cid, s) return chunks - @pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") def test_sanity(self, tmp_path): # internal version number @@ -333,9 +332,8 @@ class TestFilePng: with Image.open(TEST_PNG_FILE) as im: # Assert that there is no unclosed file warning - with pytest.warns(None) as record: + with warnings.catch_warnings(): im.verify() - assert not record with Image.open(TEST_PNG_FILE) as im: im.load() @@ -757,8 +755,8 @@ class TestFilePng: if buffer: mystdout = mystdout.buffer - reloaded = Image.open(mystdout) - assert_image_equal_tofile(reloaded, TEST_PNG_FILE) + with Image.open(mystdout) as reloaded: + assert_image_equal_tofile(reloaded, TEST_PNG_FILE) @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 0ccfb5e88..2c965318b 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,6 +1,9 @@ +import sys +from io import BytesIO + import pytest -from PIL import Image +from PIL import Image, UnidentifiedImageError from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -10,16 +13,53 @@ TEST_FILE = "Tests/images/hopper.ppm" def test_sanity(): with Image.open(TEST_FILE) as im: - im.load() assert im.mode == "RGB" assert im.size == (128, 128) - assert im.format, "PPM" + assert im.format == "PPM" assert im.get_format_mimetype() == "image/x-portable-pixmap" +@pytest.mark.parametrize( + "data, mode, pixels", + ( + (b"P5 3 1 4 \x00\x02\x04", "L", (0, 128, 255)), + (b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)), + # P6 with maxval < 255 + ( + b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11", + "RGB", + ( + (0, 15, 30), + (120, 135, 150), + (225, 240, 255), + ), + ), + # P6 with maxval > 255 + # Scale down to 255, since there is no RGB mode with more than 8-bit + ( + b"P6 3 1 257 \x00\x00\x00\x01\x00\x02" + b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF", + "RGB", + ( + (0, 1, 2), + (127, 128, 129), + (254, 255, 255), + ), + ), + ), +) +def test_arbitrary_maxval(data, mode, pixels): + fp = BytesIO(data) + with Image.open(fp) as im: + assert im.size == (3, 1) + assert im.mode == mode + + px = im.load() + assert tuple(px[x, 0] for x in range(3)) == pixels + + def test_16bit_pgm(): with Image.open("Tests/images/16_bit_binary.pgm") as im: - im.load() assert im.mode == "I" assert im.size == (20, 100) assert im.get_format_mimetype() == "image/x-portable-graymap" @@ -29,8 +69,6 @@ def test_16bit_pgm(): def test_16bit_pgm_write(tmp_path): with Image.open("Tests/images/16_bit_binary.pgm") as im: - im.load() - f = str(tmp_path / "temp.pgm") im.save(f, "PPM") @@ -47,16 +85,66 @@ def test_pnm(tmp_path): assert_image_equal_tofile(im, f) -def test_truncated_file(tmp_path): - path = str(tmp_path / "temp.pgm") - with open(path, "w") as f: - f.write("P6") +def test_magic(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"PyInvalid") + + with pytest.raises(UnidentifiedImageError): + with Image.open(path): + pass + + +def test_header_with_comments(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") + + with Image.open(path) as im: + assert im.size == (128, 128) + + +def test_non_integer_token(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6\nTEST") with pytest.raises(ValueError): with Image.open(path): pass +def test_token_too_long(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6\n 01234567890") + + with pytest.raises(ValueError) as e: + with Image.open(path): + pass + + assert str(e.value) == "Token too long in file header: b'01234567890'" + + +def test_truncated_file(tmp_path): + # Test EOF in header + path = str(tmp_path / "temp.pgm") + with open(path, "w") as f: + f.write("P6") + + with pytest.raises(ValueError) as e: + with Image.open(path): + pass + + assert str(e.value) == "Reached EOF while reading header" + + # Test EOF for PyDecoder + fp = BytesIO(b"P5 3 1 4") + with Image.open(fp) as im: + with pytest.raises(ValueError): + im.load() + + def test_neg_ppm(): # Storage.c accepted negative values for xsize, ysize. the # internal open_ppm function didn't check for sanity but it @@ -80,3 +168,30 @@ def test_mimetypes(tmp_path): f.write("PyCMYK\n128 128\n255") with Image.open(path) as im: assert im.get_format_mimetype() == "image/x-portable-anymap" + + +@pytest.mark.parametrize("buffer", (True, False)) +def test_save_stdout(buffer): + old_stdout = sys.stdout + + if buffer: + + class MyStdOut: + buffer = BytesIO() + + mystdout = MyStdOut() + else: + mystdout = BytesIO() + + sys.stdout = mystdout + + with Image.open(TEST_FILE) as im: + im.save(sys.stdout, "PPM") + + # Reset stdout + sys.stdout = old_stdout + + if buffer: + mystdout = mystdout.buffer + with Image.open(mystdout) as reloaded: + assert_image_equal_tofile(reloaded, TEST_FILE) diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index f50fe133f..b4b5b7a0c 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import Image, PsdImagePlugin @@ -29,21 +31,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(test_file) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(test_file) as im: im.load() - assert not record - def test_invalid_file(): invalid_file = "Tests/images/flower.jpg" @@ -123,7 +121,7 @@ def test_no_icc_profile(): def test_combined_larger_than_size(): - # The 'combined' sizes of the individual parts is larger than the + # The combined size of the individual parts is larger than the # declared 'size' of the extra data field, resulting in a backwards seek. # If we instead take the 'size' of the extra data field as the source of truth, diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 3c93160f1..0e3b705a2 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,4 +1,5 @@ import tempfile +import warnings from io import BytesIO import pytest @@ -28,21 +29,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() - assert not record - def test_save(tmp_path): # Arrange diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index b38727fb9..5daab47fc 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import Image, TarIO, features @@ -31,16 +33,12 @@ def test_unclosed_file(): def test_close(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar.close() - assert not record - def test_contextmanager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): pass - - assert not record diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 3450c9274..aeea3fb42 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -97,6 +97,11 @@ def test_id_field_rle(): assert im.size == (199, 199) +def test_cross_scan_line(): + with Image.open("Tests/images/cross_scan_line.tga") as im: + assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png") + + def test_save(tmp_path): test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: @@ -171,6 +176,15 @@ def test_save_orientation(tmp_path): assert test_im.info["orientation"] == 1 +def test_horizontal_orientations(): + # These images have been manually hexedited to have the relevant orientations + with Image.open("Tests/images/rgb32rle_top_right.tga") as im: + assert im.load()[90, 90][:3] == (0, 0, 0) + + with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im: + assert im.load()[90, 90][:3] == (0, 255, 0) + + def test_save_rle(tmp_path): test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index d5dda5799..16c43b00f 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,9 +1,10 @@ import os +import warnings from io import BytesIO import pytest -from PIL import Image, TiffImagePlugin +from PIL import Image, ImageFile, TiffImagePlugin from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION from .helper import ( @@ -64,20 +65,16 @@ class TestFileTiff: pytest.warns(ResourceWarning, open) def test_closed_file(self): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open("Tests/images/multipage.tiff") im.load() im.close() - assert not record - def test_context_manager(self): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open("Tests/images/multipage.tiff") as im: im.load() - assert not record - def test_mac_tiff(self): # Read RGBa images from macOS [@PIL136] @@ -90,11 +87,22 @@ class TestFileTiff: assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - def test_wrong_bits_per_sample(self): - with Image.open("Tests/images/tiff_wrong_bits_per_sample.tiff") as im: - assert im.mode == "RGBA" - assert im.size == (52, 53) - assert im.tile == [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))] + def test_bigtiff(self): + with Image.open("Tests/images/hopper_bigtiff.tif") as im: + assert_image_equal_tofile(im, "Tests/images/hopper.tif") + + @pytest.mark.parametrize( + "file_name,mode,size,offset", + [ + ("tiff_wrong_bits_per_sample.tiff", "RGBA", (52, 53), 160), + ("tiff_wrong_bits_per_sample_2.tiff", "RGB", (16, 16), 8), + ], + ) + def test_wrong_bits_per_sample(self, file_name, mode, size, offset): + with Image.open("Tests/images/" + file_name) as im: + assert im.mode == mode + assert im.size == size + assert im.tile == [("raw", (0, 0) + size, offset, (mode, 0, 1))] im.load() def test_set_legacy_api(self): @@ -143,14 +151,14 @@ class TestFileTiff: assert im.info["dpi"] == (71.0, 71.0) @pytest.mark.parametrize( - "resolutionUnit, dpi", + "resolution_unit, dpi", [(None, 72.8), (2, 72.8), (3, 184.912)], ) - def test_load_float_dpi(self, resolutionUnit, dpi): + def test_load_float_dpi(self, resolution_unit, dpi): with Image.open( - "Tests/images/hopper_float_dpi_" + str(resolutionUnit) + ".tif" + "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: - assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit + assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit assert im.info["dpi"] == (dpi, dpi) def test_save_float_dpi(self, tmp_path): @@ -217,6 +225,15 @@ class TestFileTiff: assert b[0] == ord(b"\x01") assert b[1] == ord(b"\xe0") + def test_16bit_r(self): + with Image.open("Tests/images/16bit.r.tif") as im: + assert im.getpixel((0, 0)) == 480 + assert im.mode == "I;16" + + b = im.tobytes() + assert b[0] == ord(b"\xe0") + assert b[1] == ord(b"\x01") + def test_16bit_s(self): with Image.open("Tests/images/16bit.s.tif") as im: im.load() @@ -403,10 +420,8 @@ class TestFileTiff: with Image.open("Tests/images/ifd_tag_type.tiff") as im: assert 0x8825 in im.tag_v2 - def test_exif(self): - with Image.open("Tests/images/ifd_tag_type.tiff") as im: - exif = im.getexif() - + def test_exif(self, tmp_path): + def check_exif(exif): assert sorted(exif.keys()) == [ 256, 257, @@ -439,6 +454,24 @@ class TestFileTiff: assert gps[0] == b"\x03\x02\x00\x00" assert gps[18] == "WGS-84" + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/ifd_tag_type.tiff") as im: + exif = im.getexif() + check_exif(exif) + + im.save(outfile, exif=exif) + + outfile2 = str(tmp_path / "temp2.tif") + with Image.open(outfile) as im: + exif = im.getexif() + check_exif(exif) + + im.save(outfile2, exif=exif.tobytes()) + + with Image.open(outfile2) as im: + exif = im.getexif() + check_exif(exif) + def test_exif_frames(self): # Test that EXIF data can change across frames with Image.open("Tests/images/g4-multi.tiff") as im: @@ -578,6 +611,17 @@ class TestFileTiff: with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") + def test_planar_configuration_save(self, tmp_path): + infile = "Tests/images/tiff_tiled_planar_raw.tif" + with Image.open(infile) as im: + assert im._planar_configuration == 2 + + outfile = str(tmp_path / "temp.tif") + im.save(outfile) + + with Image.open(outfile) as reloaded: + assert_image_equal_tofile(reloaded, infile) + def test_palette(self, tmp_path): def roundtrip(mode): outfile = str(tmp_path / "temp.tif") @@ -611,11 +655,11 @@ class TestFileTiff: assert reread.n_frames == 3 # Test appending using a generator - def imGenerator(ims): + def im_generator(ims): yield from ims mp = BytesIO() - im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims)) + im.save(mp, format="TIFF", save_all=True, append_images=im_generator(ims)) mp.seek(0, os.SEEK_SET) with Image.open(mp) as reread: @@ -669,6 +713,32 @@ class TestFileTiff: assert description[0]["format"] == "image/tiff" assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] + def test_get_photoshop_blocks(self): + with Image.open("Tests/images/lab.tif") as im: + assert list(im.get_photoshop_blocks().keys()) == [ + 1061, + 1002, + 1005, + 1062, + 1037, + 1049, + 1011, + 1034, + 10000, + 1013, + 1016, + 1032, + 1054, + 1050, + 1064, + 1041, + 1044, + 1036, + 1057, + 4000, + 4001, + ] + def test_close_on_load_exclusive(self, tmp_path): # similar to test_fd_leak, but runs on unixlike os tmpfile = str(tmp_path / "temp.tif") @@ -698,6 +768,8 @@ class TestFileTiff: # Ignore this UserWarning which triggers for four tags: # "Possibly corrupt EXIF data. Expecting to read 50404352 bytes but..." @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") + # Ignore this UserWarning: + @pytest.mark.filterwarnings("ignore:Truncated File Read") @pytest.mark.skipif( not os.path.exists("Tests/images/string_dimension.tiff"), reason="Extra image files not installed", @@ -708,6 +780,14 @@ class TestFileTiff: with pytest.raises(OSError): im.load() + @pytest.mark.timeout(6) + @pytest.mark.filterwarnings("ignore:Truncated File Read") + def test_timeout(self): + with Image.open("Tests/images/timeout-6646305047838720") as im: + ImageFile.LOAD_TRUNCATED_IMAGES = True + im.load() + ImageFile.LOAD_TRUNCATED_IMAGES = False + @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestFileTiffW32: diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 2213af5aa..d7a0d9377 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -28,26 +28,26 @@ def test_rt_metadata(tmp_path): # For text items, we still have to decode('ascii','replace') because # the tiff file format can't take 8 bit bytes in that field. - basetextdata = "This is some arbitrary metadata for a text field" - bindata = basetextdata.encode("ascii") + b" \xff" - textdata = basetextdata + " " + chr(255) - reloaded_textdata = basetextdata + " ?" - floatdata = 12.345 - doubledata = 67.89 + base_text_data = "This is some arbitrary metadata for a text field" + bin_data = base_text_data.encode("ascii") + b" \xff" + text_data = base_text_data + " " + chr(255) + reloaded_text_data = base_text_data + " ?" + float_data = 12.345 + double_data = 67.89 info = TiffImagePlugin.ImageFileDirectory() ImageJMetaData = TAG_IDS["ImageJMetaData"] ImageJMetaDataByteCounts = TAG_IDS["ImageJMetaDataByteCounts"] ImageDescription = TAG_IDS["ImageDescription"] - info[ImageJMetaDataByteCounts] = len(bindata) - info[ImageJMetaData] = bindata - info[TAG_IDS["RollAngle"]] = floatdata + info[ImageJMetaDataByteCounts] = len(bin_data) + info[ImageJMetaData] = bin_data + info[TAG_IDS["RollAngle"]] = float_data info.tagtype[TAG_IDS["RollAngle"]] = 11 - info[TAG_IDS["YawAngle"]] = doubledata + info[TAG_IDS["YawAngle"]] = double_data info.tagtype[TAG_IDS["YawAngle"]] = 12 - info[ImageDescription] = textdata + info[ImageDescription] = text_data f = str(tmp_path / "temp.tif") @@ -55,28 +55,28 @@ def test_rt_metadata(tmp_path): with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (len(bindata),) - assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bindata),) + assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) + assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) - assert loaded.tag[ImageJMetaData] == bindata - assert loaded.tag_v2[ImageJMetaData] == bindata + assert loaded.tag[ImageJMetaData] == bin_data + assert loaded.tag_v2[ImageJMetaData] == bin_data - assert loaded.tag[ImageDescription] == (reloaded_textdata,) - assert loaded.tag_v2[ImageDescription] == reloaded_textdata + assert loaded.tag[ImageDescription] == (reloaded_text_data,) + assert loaded.tag_v2[ImageDescription] == reloaded_text_data loaded_float = loaded.tag[TAG_IDS["RollAngle"]][0] - assert round(abs(loaded_float - floatdata), 5) == 0 + assert round(abs(loaded_float - float_data), 5) == 0 loaded_double = loaded.tag[TAG_IDS["YawAngle"]][0] - assert round(abs(loaded_double - doubledata), 7) == 0 + assert round(abs(loaded_double - double_data), 7) == 0 # check with 2 element ImageJMetaDataByteCounts, issue #2006 - info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8) + info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) img.save(f, tiffinfo=info) with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) - assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bindata) - 8) + assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) + assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) def test_read_metadata(): @@ -258,7 +258,7 @@ def test_ifd_unsigned_rational(tmp_path): im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() - max_long = 2 ** 32 - 1 + max_long = 2**32 - 1 # 4 bytes unsigned long numerator = max_long @@ -290,8 +290,8 @@ def test_ifd_signed_rational(tmp_path): info = TiffImagePlugin.ImageFileDirectory_v2() # pair of 4 byte signed longs - numerator = 2 ** 31 - 1 - denominator = -(2 ** 31) + numerator = 2**31 - 1 + denominator = -(2**31) info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) @@ -302,8 +302,8 @@ def test_ifd_signed_rational(tmp_path): assert numerator == reloaded.tag_v2[37380].numerator assert denominator == reloaded.tag_v2[37380].denominator - numerator = -(2 ** 31) - denominator = 2 ** 31 - 1 + numerator = -(2**31) + denominator = 2**31 - 1 info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) @@ -315,7 +315,7 @@ def test_ifd_signed_rational(tmp_path): assert denominator == reloaded.tag_v2[37380].denominator # out of bounds of 4 byte signed long - numerator = -(2 ** 31) - 1 + numerator = -(2**31) - 1 denominator = 1 info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) @@ -324,7 +324,7 @@ def test_ifd_signed_rational(tmp_path): im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: - assert 2 ** 31 - 1 == reloaded.tag_v2[37380].numerator + assert 2**31 - 1 == reloaded.tag_v2[37380].numerator assert -1 == reloaded.tag_v2[37380].denominator @@ -356,7 +356,7 @@ def test_empty_values(): assert 33432 in info -def test_PhotoshopInfo(tmp_path): +def test_photoshop_info(tmp_path): with Image.open("Tests/images/issue_2278.tif") as im: assert len(im.tag_v2[34377]) == 70 assert isinstance(im.tag_v2[34377], bytes) diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index f25b42fe0..4be46e9d6 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -2,15 +2,11 @@ from PIL import WalImageFile from .helper import assert_image_equal_tofile +TEST_FILE = "Tests/images/hopper.wal" + def test_open(): - # Arrange - TEST_FILE = "Tests/images/hopper.wal" - - # Act with WalImageFile.open(TEST_FILE) as im: - - # Assert assert im.format == "WAL" assert im.format_description == "Quake2 Texture" assert im.mode == "P" @@ -19,3 +15,11 @@ def test_open(): assert isinstance(im, WalImageFile.WalImageFile) assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") + + +def test_load(): + with WalImageFile.open(TEST_FILE) as im: + assert im.load()[0, 0] == 122 + + # Test again now that it has already been loaded once + assert im.load()[0, 0] == 122 diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 420594b0c..c69e13a89 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,12 +1,14 @@ import io import re import sys +import warnings import pytest from PIL import Image, WebPImagePlugin, features from .helper import ( + assert_image_equal, assert_image_similar, assert_image_similar_tofile, hopper, @@ -104,6 +106,19 @@ class TestFileWebp: hopper().save(buffer_method, format="WEBP", method=6) assert buffer_no_args.getbuffer() != buffer_method.getbuffer() + @skip_unless_feature("webp_anim") + def test_save_all(self, tmp_path): + temp_file = str(tmp_path / "temp.webp") + im = Image.new("RGB", (1, 1)) + im2 = Image.new("RGB", (1, 1), "#f00") + im.save(temp_file, save_all=True, append_images=[im2]) + + with Image.open(temp_file) as reloaded: + assert_image_equal(im, reloaded) + + reloaded.seek(1) + assert_image_similar(im2, reloaded, 1) + def test_icc_profile(self, tmp_path): self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) if _webp.HAVE_WEBPANIM: @@ -127,7 +142,7 @@ class TestFileWebp: self._roundtrip(tmp_path, "P", 50.0) - @pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_message(self, tmp_path): temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) @@ -161,9 +176,8 @@ class TestFileWebp: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: temp_file = str(tmp_path / "temp.webp") - with pytest.warns(None) as record: + with warnings.catch_warnings(): image.save(temp_file) - assert not record def test_file_pointer_could_be_reused(self): file_path = "Tests/images/hopper.webp" @@ -171,9 +185,14 @@ class TestFileWebp: Image.open(blob).load() Image.open(blob).load() - @skip_unless_feature("webp") @skip_unless_feature("webp_anim") def test_background_from_gif(self, tmp_path): + # Save L mode GIF with background + with Image.open("Tests/images/no_palette_with_background.gif") as im: + out_webp = str(tmp_path / "temp.webp") + im.save(out_webp, save_all=True) + + # Save P mode GIF with background with Image.open("Tests/images/chi.gif") as im: original_value = im.convert("RGB").getpixel((1, 1)) @@ -188,12 +207,9 @@ class TestFileWebp: with Image.open(out_gif) as reread: reread_value = reread.convert("RGB").getpixel((1, 1)) - difference = sum( - [abs(original_value[i] - reread_value[i]) for i in range(0, 3)] - ) + difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) assert difference < 5 - @skip_unless_feature("webp") @skip_unless_feature("webp_anim") def test_duration(self, tmp_path): with Image.open("Tests/images/dispose_bgnd.gif") as im: diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 25ebffe02..c621df0d9 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,6 +1,7 @@ import pytest +from packaging.version import parse as parse_version -from PIL import Image +from PIL import Image, features from .helper import ( assert_image_equal, @@ -27,7 +28,6 @@ def test_n_frames(): assert im.is_animated -@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") def test_write_animation_L(tmp_path): """ Convert an animated GIF to animated WebP, then compare the frame count, and first @@ -46,6 +46,11 @@ def test_write_animation_L(tmp_path): orig.load() im.load() assert_image_similar(im, orig.convert("RGBA"), 32.9) + + if is_big_endian(): + webp = parse_version(features.version_module("webp")) + if webp < parse_version("1.2.2"): + pytest.skip("Fails with libwebp earlier than 1.2.2") orig.seek(orig.n_frames - 1) im.seek(im.n_frames - 1) orig.load() @@ -53,7 +58,6 @@ def test_write_animation_L(tmp_path): assert_image_similar(im, orig.convert("RGBA"), 32.9) -@pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") def test_write_animation_RGB(tmp_path): """ Write an animated WebP from RGB frames, and ensure the frames @@ -69,6 +73,10 @@ def test_write_animation_RGB(tmp_path): assert_image_equal(im, frame1.convert("RGBA")) # Compare second frame to original + if is_big_endian(): + webp = parse_version(features.version_module("webp")) + if webp < parse_version("1.2.2"): + pytest.skip("Fails with libwebp earlier than 1.2.2") im.seek(1) im.load() assert_image_equal(im, frame2.convert("RGBA")) @@ -82,14 +90,14 @@ def test_write_animation_RGB(tmp_path): check(temp_file1) # Tests appending using a generator - def imGenerator(ims): + def im_generator(ims): yield from ims temp_file2 = str(tmp_path / "temp_generator.webp") frame1.copy().save( temp_file2, save_all=True, - append_images=imGenerator([frame2]), + append_images=im_generator([frame2]), lossless=True, ) check(temp_file2) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 3f8bc96cc..d6769a24b 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -24,6 +24,12 @@ def test_load_raw(): assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref.png", 2.0) +def test_load(): + with Image.open("Tests/images/drawing.emf") as im: + if hasattr(Image.core, "drawwmf"): + assert im.load()[0, 0] == (255, 255, 255) + + def test_register_handler(tmp_path): class TestHandler: methodCalled = False diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 487920a92..9c54c6755 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -2,7 +2,7 @@ from io import BytesIO import pytest -from PIL import Image +from PIL import Image, XbmImagePlugin from .helper import hopper @@ -63,6 +63,13 @@ def test_open_filename_with_underscore(): assert im.size == (128, 128) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + XbmImagePlugin.XbmImageFile(invalid_file) + + def test_save_wrong_mode(tmp_path): im = hopper() out = str(tmp_path / "temp.xbm") diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 015210b4d..38f7ddac5 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -4,7 +4,7 @@ from .helper import PillowLeakTestCase, skip_unless_feature class TestTTypeFontLeak(PillowLeakTestCase): - # fails at iteration 3 in master + # fails at iteration 3 in main iterations = 10 mem_limit = 4096 # k @@ -24,7 +24,7 @@ class TestTTypeFontLeak(PillowLeakTestCase): class TestDefaultFontLeak(TestTTypeFontLeak): - # fails at iteration 37 in master + # fails at iteration 37 in main iterations = 100 mem_limit = 1024 # k diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 3b9c8b071..b485e854f 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -77,7 +77,7 @@ def to_rgb_colorsys(im): def test_wedge(): - src = wedge().resize((3 * 32, 32), Image.BILINEAR) + src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR) im = src.convert("HSV") comparable = to_hsv_colorsys(src) diff --git a/Tests/test_image.py b/Tests/test_image.py index 843921f5f..7ea61baa6 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -4,11 +4,11 @@ import pathlib import shutil import sys import tempfile +import warnings import pytest -import PIL -from PIL import Image, ImageDraw, ImagePalette, ImageShow, UnidentifiedImageError +from PIL import Image, ImageDraw, ImagePalette, UnidentifiedImageError from .helper import ( assert_image_equal, @@ -107,6 +107,17 @@ class TestImage: # with pytest.raises(MemoryError): # Image.new("L", (1000000, 1000000)) + def test_repr_pretty(self): + class Pretty: + def text(self, text): + self.pretty_output = text + + im = Image.new("L", (100, 100)) + + p = Pretty() + im._repr_pretty_(p, None) + assert p.pretty_output == "" + def test_open_formats(self): PNGFILE = "Tests/images/hopper.png" JPGFILE = "Tests/images/hopper.jpg" @@ -219,6 +230,10 @@ class TestImage: assert not im.readonly @pytest.mark.skipif(is_win32(), reason="Test requires opening tempfile twice") + @pytest.mark.skipif( + sys.platform == "cygwin", + reason="Test requires opening an mmaped file for writing", + ) def test_readonly_save(self, tmp_path): temp_file = str(tmp_path / "temp.bmp") shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) @@ -651,22 +666,6 @@ class TestImage: expected = Image.new(mode, (100, 100), color) assert_image_equal(im.convert(mode), expected) - def test_showxv_deprecation(self): - class TestViewer(ImageShow.Viewer): - def show_image(self, image, **options): - return True - - viewer = TestViewer() - ImageShow.register(viewer, -1) - - im = Image.new("RGB", (50, 50), "white") - - with pytest.warns(DeprecationWarning): - Image._showxv(im) - - # Restore original state - ImageShow._viewers.pop(0) - def test_no_resource_warning_on_save(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/835 # Arrange @@ -675,9 +674,17 @@ class TestImage: # Act/Assert with Image.open(test_file) as im: - with pytest.warns(None) as record: + with warnings.catch_warnings(): im.save(temp_file) - assert not record + + def test_no_new_file_on_error(self, tmp_path): + temp_file = str(tmp_path / "temp.jpg") + + im = Image.new("RGB", (0, 0)) + with pytest.raises(ValueError): + im.save(temp_file) + + assert not os.path.exists(temp_file) def test_load_on_nonexclusive_multiframe(self): with open("Tests/images/frozenpond.mpo", "rb") as fp: @@ -693,6 +700,19 @@ class TestImage: assert not fp.closed + def test_empty_exif(self): + with Image.open("Tests/images/exif.png") as im: + exif = im.getexif() + assert dict(exif) != {} + + # Test that exif data is cleared after another load + exif.load(None) + assert dict(exif) == {} + + # Test loading just the EXIF header + exif.load(b"Exif\x00\x00") + assert dict(exif) == {} + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @@ -824,9 +844,11 @@ class TestImage: 34665: 196, } - @pytest.mark.skipif( - sys.version_info < (3, 7), reason="Python 3.7 or greater required" - ) + @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) + def test_zero_tobytes(self, size): + im = Image.new("RGB", size) + assert im.tobytes() == b"" + def test_categories_deprecation(self): with pytest.warns(DeprecationWarning): assert hopper().category == 0 @@ -838,34 +860,30 @@ class TestImage: with pytest.warns(DeprecationWarning): assert Image.CONTAINER == 2 - @pytest.mark.parametrize( - "test_module", - [PIL, Image], - ) - def test_pillow_version(self, test_module): + def test_constants_deprecation(self): with pytest.warns(DeprecationWarning): - assert test_module.PILLOW_VERSION == PIL.__version__ + assert Image.NEAREST == 0 + with pytest.warns(DeprecationWarning): + assert Image.NONE == 0 with pytest.warns(DeprecationWarning): - str(test_module.PILLOW_VERSION) - + assert Image.LINEAR == Image.Resampling.BILINEAR with pytest.warns(DeprecationWarning): - assert int(test_module.PILLOW_VERSION[0]) >= 7 - + assert Image.CUBIC == Image.Resampling.BICUBIC with pytest.warns(DeprecationWarning): - assert test_module.PILLOW_VERSION < "9.9.0" + assert Image.ANTIALIAS == Image.Resampling.LANCZOS - with pytest.warns(DeprecationWarning): - assert test_module.PILLOW_VERSION <= "9.9.0" - - with pytest.warns(DeprecationWarning): - assert test_module.PILLOW_VERSION != "7.0.0" - - with pytest.warns(DeprecationWarning): - assert test_module.PILLOW_VERSION >= "7.0.0" - - with pytest.warns(DeprecationWarning): - assert test_module.PILLOW_VERSION > "7.0.0" + for enum in ( + Image.Transpose, + Image.Transform, + Image.Resampling, + Image.Dither, + Image.Palette, + Image.Quantize, + ): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(Image, name) == enum[name] @pytest.mark.parametrize( "path", @@ -902,18 +920,6 @@ class TestImage: except OSError as e: assert str(e) == "buffer overrun when reading image file" - def test_show_deprecation(self, monkeypatch): - monkeypatch.setattr(Image, "_show", lambda *args, **kwargs: None) - - im = Image.new("RGB", (50, 50), "white") - - with pytest.warns(None) as raised: - im.show() - assert not raised - - with pytest.warns(DeprecationWarning): - im.show(command="mock") - class MockEncoder: pass diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 7b3036979..617274a57 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -1,4 +1,3 @@ -import ctypes import os import subprocess import sys @@ -154,14 +153,17 @@ class TestImageGetPixel(AccessTest): # Check 0 im = Image.new(mode, (0, 0), None) - with pytest.raises(IndexError): + assert im.load() is not None + + error = ValueError if self._need_cffi_access else IndexError + with pytest.raises(error): im.putpixel((0, 0), c) - with pytest.raises(IndexError): + with pytest.raises(error): im.getpixel((0, 0)) # Check 0 negative index - with pytest.raises(IndexError): + with pytest.raises(error): im.putpixel((-1, -1), c) - with pytest.raises(IndexError): + with pytest.raises(error): im.getpixel((-1, -1)) # check initial color @@ -176,10 +178,10 @@ class TestImageGetPixel(AccessTest): # Check 0 im = Image.new(mode, (0, 0), c) - with pytest.raises(IndexError): + with pytest.raises(error): im.getpixel((0, 0)) # Check 0 negative index - with pytest.raises(IndexError): + with pytest.raises(error): im.getpixel((-1, -1)) def test_basic(self): @@ -205,10 +207,10 @@ class TestImageGetPixel(AccessTest): # 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)]: @@ -386,7 +388,7 @@ class TestImagePutPixelError(AccessTest): def test_putpixel_overflow_error(self, mode): im = hopper(mode) with pytest.raises(OverflowError): - im.putpixel((0, 0), 2 ** 80) + im.putpixel((0, 0), 2**80) def test_putpixel_unrecognized_mode(self): im = hopper("BGR;15") @@ -401,6 +403,8 @@ class TestEmbeddable: "not from shell", ) def test_embeddable(self): + import ctypes + with open("embed_pil.c", "w") as fh: fh.write( """ diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 436a417d1..727c282d7 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -41,7 +41,7 @@ def test_sanity(): def test_default(): im = hopper("P") - assert_image(im, "P", im.size) + assert im.mode == "P" converted_im = im.convert() assert_image(converted_im, "RGB", im.size) converted_im = im.convert() @@ -70,12 +70,24 @@ def test_16bit(): with Image.open("Tests/images/16bit.cropped.tif") as im: _test_float_conversion(im) + for color in (65535, 65536): + im = Image.new("I", (1, 1), color) + im_i16 = im.convert("I;16") + assert im_i16.getpixel((0, 0)) == 65535 + def test_16bit_workaround(): with Image.open("Tests/images/16bit.cropped.tif") as im: _test_float_conversion(im.convert("I")) +def test_opaque(): + alpha = hopper("P").convert("PA").getchannel("A") + + solid = Image.new("L", (128, 128), 255) + assert_image_equal(alpha, solid) + + def test_rgba_p(): im = hopper("RGBA") im.putalpha(hopper("L")) @@ -93,7 +105,7 @@ def test_trns_p(tmp_path): f = str(tmp_path / "temp.png") im_l = im.convert("L") - assert im_l.info["transparency"] == 0 # undone + assert im_l.info["transparency"] == 1 # undone im_l.save(f) im_rgb = im.convert("RGB") @@ -128,6 +140,10 @@ def test_trns_l(tmp_path): f = str(tmp_path / "temp.png") + im_la = im.convert("LA") + assert "transparency" not in im_la.info + im_la.save(f) + im_rgb = im.convert("RGB") assert im_rgb.info["transparency"] == (128, 128, 128) # undone im_rgb.save(f) @@ -136,7 +152,7 @@ def test_trns_l(tmp_path): assert "transparency" in im_p.info im_p.save(f) - im_p = im.convert("P", palette=Image.ADAPTIVE) + im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) assert "transparency" in im_p.info im_p.save(f) @@ -159,17 +175,31 @@ def test_trns_RGB(tmp_path): assert "transparency" not in im_rgba.info im_rgba.save(f) - im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) + im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE) assert "transparency" not in im_p.info im_p.save(f) im = Image.new("RGB", (1, 1)) im.info["transparency"] = im.getpixel((0, 0)) - im_p = im.convert("P", palette=Image.ADAPTIVE) + im_p = im.convert("P", palette=Image.Palette.ADAPTIVE) assert im_p.info["transparency"] == im_p.getpixel((0, 0)) im_p.save(f) +@pytest.mark.parametrize("convert_mode", ("L", "LA", "I")) +def test_l_macro_rounding(convert_mode): + for mode in ("P", "PA"): + im = Image.new(mode, (1, 1)) + im.palette.getcolor((0, 1, 2)) + + converted_im = im.convert(convert_mode) + px = converted_im.load() + converted_color = px[0, 0] + if convert_mode == "LA": + converted_color = converted_color[0] + assert converted_color == 1 + + def test_gif_with_rgba_palette_to_p(): # See https://github.com/python-pillow/Pillow/issues/2433 with Image.open("Tests/images/hopper.gif") as im: diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index ad0391dbe..21e438654 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -6,8 +6,8 @@ from .helper import hopper def test_copy(): - croppedCoordinates = (10, 10, 20, 20) - croppedSize = (10, 10) + 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) @@ -23,15 +23,15 @@ def test_copy(): # Internal copy method on a cropped image im = hopper(mode) - out = im.crop(croppedCoordinates).copy() + out = im.crop(cropped_coordinates).copy() assert out.mode == im.mode - assert out.size == croppedSize + assert out.size == cropped_size # Python's copy method on a cropped image im = hopper(mode) - out = copy.copy(im.crop(croppedCoordinates)) + out = copy.copy(im.crop(cropped_coordinates)) assert out.mode == im.mode - assert out.size == croppedSize + assert out.size == cropped_size def test_copy_zero(): diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index e2228758c..6574e6efd 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -47,16 +47,12 @@ def test_wide_crop(): assert crop(-25, 75, 25, 125) == (1875, 625) -def test_negative_crop(): - # Check negative crop size (@PIL171) +@pytest.mark.parametrize("box", ((8, 2, 2, 8), (2, 8, 8, 2), (8, 8, 2, 2))) +def test_negative_crop(box): + im = Image.new("RGB", (10, 10)) - im = Image.new("L", (512, 512)) - im = im.crop((400, 400, 200, 200)) - - assert im.size == (0, 0) - assert len(im.getdata()) == 0 - with pytest.raises(IndexError): - im.getdata()[0] + with pytest.raises(ValueError): + im.crop(box) def test_crop_float(): diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index df8c353f3..14a8da9f1 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -99,10 +99,10 @@ def test_rankfilter_properties(): def test_builtinfilter_p(): - builtinFilter = ImageFilter.BuiltinFilter() + builtin_filter = ImageFilter.BuiltinFilter() with pytest.raises(ValueError): - builtinFilter.filter(hopper("P")) + builtin_filter.filter(hopper("P")) def test_kernel_not_enough_coefficients(): diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index 159efd78a..36c81b40f 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -14,7 +14,7 @@ def test_sanity(): def test_roundtrip(): def getdata(mode): - im = hopper(mode).resize((32, 30), Image.NEAREST) + im = hopper(mode).resize((32, 30), Image.Resampling.NEAREST) data = im.getdata() return data[0], len(data), len(list(data)) diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index 1818adca2..58a6dacbb 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -1,3 +1,5 @@ +from PIL import Image + from .helper import hopper @@ -17,3 +19,26 @@ def test_palette(): assert palette("RGBA") is None assert palette("CMYK") is None assert palette("YCbCr") is None + + +def test_palette_rawmode(): + im = Image.new("P", (1, 1)) + im.putpalette((1, 2, 3)) + + for rawmode in ("RGB", None): + rgb = im.getpalette(rawmode) + assert rgb == [1, 2, 3] + + # Convert the RGB palette to RGBA + rgba = im.getpalette("RGBA") + assert rgba == [1, 2, 3, 255] + + im.putpalette((1, 2, 3, 4), "RGBA") + + # Convert the RGBA palette to RGB + rgb = im.getpalette("RGB") + assert rgb == [1, 2, 3] + + for rawmode in ("RGBA", None): + rgba = im.getpalette(rawmode) + assert rgba == [1, 2, 3, 4] diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 7f92c2264..670b2f4eb 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -21,6 +21,7 @@ def test_sanity(): assert m.bands == ("1",) assert m.basemode == "L" assert m.basetype == "L" + assert m.typestr == "|b1" for mode in ( "I;16", @@ -45,6 +46,7 @@ def test_sanity(): assert m.bands == ("R", "G", "B") assert m.basemode == "RGB" assert m.basetype == "L" + assert m.typestr == "|u1" def test_properties(): @@ -65,6 +67,5 @@ def test_properties(): check("RGB", "RGB", "L", 3, ("R", "G", "B")) check("RGBA", "RGB", "L", 4, ("R", "G", "B", "A")) check("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")) - check("RGBX", "RGB", "L", 4, ("R", "G", "B", "X")) check("CMYK", "RGB", "L", 4, ("C", "M", "Y", "K")) check("YCbCr", "RGB", "L", 3, ("Y", "Cb", "Cr")) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 1d3ca8135..4ea1d73ce 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -1,6 +1,6 @@ from PIL import Image -from .helper import assert_image_equal, cached_property +from .helper import CachedProperty, assert_image_equal class TestImagingPaste: @@ -34,7 +34,7 @@ class TestImagingPaste: im.paste(im2, mask) self.assert_9points_image(im, expected) - @cached_property + @CachedProperty def mask_1(self): mask = Image.new("1", (self.size, self.size)) px = mask.load() @@ -43,11 +43,11 @@ class TestImagingPaste: px[y, x] = (x + y) % 2 return mask - @cached_property + @CachedProperty def mask_L(self): - return self.gradient_L.transpose(Image.ROTATE_270) + return self.gradient_L.transpose(Image.Transpose.ROTATE_270) - @cached_property + @CachedProperty def gradient_L(self): gradient = Image.new("L", (self.size, self.size)) px = gradient.load() @@ -56,38 +56,48 @@ class TestImagingPaste: px[y, x] = (x + y) % 255 return gradient - @cached_property + @CachedProperty def gradient_RGB(self): return Image.merge( "RGB", [ self.gradient_L, - self.gradient_L.transpose(Image.ROTATE_90), - self.gradient_L.transpose(Image.ROTATE_180), + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + self.gradient_L.transpose(Image.Transpose.ROTATE_180), ], ) - @cached_property + @CachedProperty + def gradient_LA(self): + return Image.merge( + "LA", + [ + self.gradient_L, + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + ], + ) + + @CachedProperty def gradient_RGBA(self): return Image.merge( "RGBA", [ self.gradient_L, - self.gradient_L.transpose(Image.ROTATE_90), - self.gradient_L.transpose(Image.ROTATE_180), - self.gradient_L.transpose(Image.ROTATE_270), + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + self.gradient_L.transpose(Image.Transpose.ROTATE_180), + self.gradient_L.transpose(Image.Transpose.ROTATE_270), ], ) - @cached_property + @CachedProperty def gradient_RGBa(self): return Image.merge( "RGBa", [ self.gradient_L, - self.gradient_L.transpose(Image.ROTATE_90), - self.gradient_L.transpose(Image.ROTATE_180), - self.gradient_L.transpose(Image.ROTATE_270), + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + self.gradient_L.transpose(Image.Transpose.ROTATE_180), + self.gradient_L.transpose(Image.Transpose.ROTATE_270), ], ) @@ -145,6 +155,28 @@ class TestImagingPaste: ], ) + def test_image_mask_LA(self): + for mode in ("RGBA", "RGB", "L"): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.gradient_LA, + [ + (128, 191, 255, 191), + (112, 207, 206, 111), + (128, 254, 128, 1), + (208, 208, 239, 239), + (192, 191, 191, 191), + (207, 207, 112, 113), + (255, 255, 255, 255), + (239, 207, 207, 239), + (255, 191, 128, 191), + ], + ) + def test_image_mask_RGBA(self): for mode in ("RGBA", "RGB", "L"): im = Image.new(mode, (200, 200), "white") diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 366f45854..428ad116b 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -10,6 +10,7 @@ def test_sanity(): im.point(list(range(256))) im.point(list(range(256)) * 3) im.point(lambda x: x) + im.point(lambda x: x * 1.2) im = im.convert("I") with pytest.raises(ValueError): diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 54712fd6c..3d60e52a2 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -1,6 +1,8 @@ import sys from array import array +import pytest + from PIL import Image from .helper import assert_image_equal, hopper @@ -36,7 +38,7 @@ def test_long_integers(): assert put(0xFFFFFFFF) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) - if sys.maxsize > 2 ** 32: + if sys.maxsize > 2**32: assert put(sys.maxsize) == (255, 255, 255, 255) else: assert put(sys.maxsize) == (255, 255, 255, 127) @@ -47,6 +49,12 @@ def test_pypy_performance(): im.putdata(list(range(256)) * 256) +def test_mode_with_L_with_float(): + im = Image.new("L", (1, 1), 0) + im.putdata([2.0]) + assert im.getpixel((0, 0)) == 2 + + def test_mode_i(): src = hopper("L") data = list(src.getdata()) @@ -87,3 +95,18 @@ def test_array_F(): im.putdata(arr) assert len(im.getdata()) == len(arr) + + +def test_not_flattened(): + im = Image.new("L", (1, 1)) + with pytest.raises(TypeError): + im.putdata([[0]]) + with pytest.raises(TypeError): + im.putdata([[0]], 2) + + with pytest.raises(TypeError): + im = Image.new("I", (1, 1)) + im.putdata([[0]]) + with pytest.raises(TypeError): + im = Image.new("F", (1, 1)) + im.putdata([[0]]) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 5a9df11b1..3b29769a7 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -2,7 +2,7 @@ import pytest from PIL import Image, ImagePalette -from .helper import assert_image_equal, hopper +from .helper import assert_image_equal, assert_image_equal_tofile, hopper def test_putpalette(): @@ -36,9 +36,15 @@ def test_putpalette(): def test_imagepalette(): im = hopper("P") im.putpalette(ImagePalette.negative()) + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png") + im.putpalette(ImagePalette.random()) + im.putpalette(ImagePalette.sepia()) + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_sepia.png") + im.putpalette(ImagePalette.wedge()) + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png") def test_putpalette_with_alpha_values(): @@ -56,3 +62,17 @@ def test_putpalette_with_alpha_values(): im.putpalette(palette_with_alpha_values, "RGBA") assert_image_equal(im.convert("RGBA"), expected) + + +@pytest.mark.parametrize( + "mode, palette", + ( + ("RGBA", (1, 2, 3, 4)), + ("RGBAX", (1, 2, 3, 4, 0)), + ), +) +def test_rgba_palette(mode, palette): + im = Image.new("P", (1, 1)) + im.putpalette(palette, mode) + assert im.getpalette() == [1, 2, 3] + assert im.palette.colors == {(1, 2, 3, 4): 0} diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 1ceff0842..e9afd9118 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,41 +1,40 @@ import pytest +from packaging.version import parse as parse_version -from PIL import Image +from PIL import Image, features -from .helper import assert_image, assert_image_similar, hopper, is_ppc64le +from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature def test_sanity(): image = hopper() converted = image.quantize() - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 10) image = hopper() converted = image.quantize(palette=hopper("P")) - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 60) -@pytest.mark.xfail(is_ppc64le(), reason="failing on ppc64le on GHA") +@skip_unless_feature("libimagequant") def test_libimagequant_quantize(): image = hopper() - try: - converted = image.quantize(100, Image.LIBIMAGEQUANT) - except ValueError as ex: # pragma: no cover - if "dependency" in str(ex).lower(): - pytest.skip("libimagequant support not available") - else: - raise - assert_image(converted, "P", converted.size) + if is_ppc64le(): + libimagequant = parse_version(features.version_feature("libimagequant")) + if libimagequant < parse_version("4"): + pytest.skip("Fails with libimagequant earlier than 4.0.0 on ppc64le") + converted = image.quantize(100, Image.Quantize.LIBIMAGEQUANT) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 15) assert len(converted.getcolors()) == 100 def test_octree_quantize(): image = hopper() - converted = image.quantize(100, Image.FASTOCTREE) - assert_image(converted, "P", converted.size) + converted = image.quantize(100, Image.Quantize.FASTOCTREE) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 20) assert len(converted.getcolors()) == 100 @@ -52,7 +51,7 @@ def test_quantize(): with Image.open("Tests/images/caption_6_33_22.png") as image: image = image.convert("RGB") converted = image.quantize() - assert_image(converted, "P", converted.size) + assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 1) @@ -61,8 +60,9 @@ def test_quantize_no_dither(): with Image.open("Tests/images/caption_6_33_22.png") as palette: palette = palette.convert("P") - converted = image.quantize(dither=0, palette=palette) - assert_image(converted, "P", converted.size) + converted = image.quantize(dither=Image.Dither.NONE, palette=palette) + assert converted.mode == "P" + assert converted.palette.palette == palette.palette.palette def test_quantize_dither_diff(): @@ -70,12 +70,19 @@ def test_quantize_dither_diff(): with Image.open("Tests/images/caption_6_33_22.png") as palette: palette = palette.convert("P") - dither = image.quantize(dither=1, palette=palette) - nodither = image.quantize(dither=0, palette=palette) + dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette) + nodither = image.quantize(dither=Image.Dither.NONE, palette=palette) assert dither.tobytes() != nodither.tobytes() +def test_colors(): + im = hopper() + colors = 2 + converted = im.quantize(colors) + assert len(converted.palette.palette) == colors * len("RGB") + + def test_transparent_colors_equal(): im = Image.new("RGBA", (1, 2), (0, 0, 0, 0)) px = im.load() @@ -84,3 +91,35 @@ def test_transparent_colors_equal(): converted = im.quantize() converted_px = converted.load() assert converted_px[0, 0] == converted_px[0, 1] + + +@pytest.mark.parametrize( + "method, color", + ( + (Image.Quantize.MEDIANCUT, (0, 0, 0)), + (Image.Quantize.MAXCOVERAGE, (0, 0, 0)), + (Image.Quantize.FASTOCTREE, (0, 0, 0)), + (Image.Quantize.FASTOCTREE, (0, 0, 0, 0)), + ), +) +def test_palette(method, color): + im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color) + + converted = im.quantize(method=method) + converted_px = converted.load() + assert converted_px[0, 0] == converted.palette.colors[color] + + +def test_small_palette(): + # Arrange + im = hopper() + + colors = (255, 0, 0, 0, 0, 255) + p = Image.new("P", (1, 1)) + p.putpalette(colors) + + # Act + im = im.quantize(palette=p) + + # Assert + assert len(im.getcolors()) == 2 diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index b4eebc142..70dc87f0a 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -97,7 +97,7 @@ def get_image(mode): bands = [gradients_image] for _ in mode_info.bands[1:]: # rotate previous image - band = bands[-1].transpose(Image.ROTATE_90) + band = bands[-1].transpose(Image.Transpose.ROTATE_90) bands.append(band) # Correct alpha channel by transforming completely transparent pixels. # Low alpha values also emphasize error after alpha multiplication. @@ -138,24 +138,26 @@ def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1): reference = Image.new(im.mode, reduced.size) area_size = (im.size[0] // factor[0], im.size[1] // factor[1]) area_box = (0, 0, area_size[0] * factor[0], area_size[1] * factor[1]) - area = im.resize(area_size, Image.BOX, area_box) + area = im.resize(area_size, Image.Resampling.BOX, area_box) reference.paste(area, (0, 0)) if area_size[0] < reduced.size[0]: assert reduced.size[0] - area_size[0] == 1 last_column_box = (area_box[2], 0, im.size[0], area_box[3]) - last_column = im.resize((1, area_size[1]), Image.BOX, last_column_box) + last_column = im.resize( + (1, area_size[1]), Image.Resampling.BOX, last_column_box + ) reference.paste(last_column, (area_size[0], 0)) if area_size[1] < reduced.size[1]: assert reduced.size[1] - area_size[1] == 1 last_row_box = (0, area_box[3], area_box[2], im.size[1]) - last_row = im.resize((area_size[0], 1), Image.BOX, last_row_box) + last_row = im.resize((area_size[0], 1), Image.Resampling.BOX, last_row_box) reference.paste(last_row, (0, area_size[1])) if area_size[0] < reduced.size[0] and area_size[1] < reduced.size[1]: last_pixel_box = (area_box[2], area_box[3], im.size[0], im.size[1]) - last_pixel = im.resize((1, 1), Image.BOX, last_pixel_box) + last_pixel = im.resize((1, 1), Image.Resampling.BOX, last_pixel_box) reference.paste(last_pixel, area_size) assert_compare_images(reduced, reference, average_diff, max_diff) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 8bf2ce916..125422337 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -24,7 +24,7 @@ class TestImagingResampleVulnerability: ): with pytest.raises(MemoryError): # any resampling filter will do here - im.im.resize((xsize, ysize), Image.BILINEAR) + im.im.resize((xsize, ysize), Image.Resampling.BILINEAR) def test_invalid_size(self): im = hopper() @@ -103,7 +103,7 @@ class TestImagingCoreResampleAccuracy: 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.BOX) + case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off data = ("e1 e1" "e1 e1") @@ -114,7 +114,7 @@ class TestImagingCoreResampleAccuracy: 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.BILINEAR) + case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off data = ("e1 c9" "c9 b7") @@ -125,7 +125,7 @@ class TestImagingCoreResampleAccuracy: 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.HAMMING) + case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off data = ("e1 da" "da d3") @@ -136,7 +136,7 @@ class TestImagingCoreResampleAccuracy: def test_reduce_bicubic(self): for mode in ["RGBX", "RGB", "La", "L"]: case = self.make_case(mode, (12, 12), 0xE1) - case = case.resize((6, 6), Image.BICUBIC) + case = case.resize((6, 6), Image.Resampling.BICUBIC) # fmt: off data = ("e1 e3 d4" "e3 e5 d6" @@ -148,7 +148,7 @@ class TestImagingCoreResampleAccuracy: 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.LANCZOS) + case = case.resize((8, 8), Image.Resampling.LANCZOS) # fmt: off data = ("e1 e0 e4 d7" "e0 df e3 d6" @@ -161,7 +161,7 @@ class TestImagingCoreResampleAccuracy: 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.BOX) + case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off data = ("e1 e1" "e1 e1") @@ -172,7 +172,7 @@ class TestImagingCoreResampleAccuracy: 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.BILINEAR) + case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off data = ("e1 b0" "b0 98") @@ -183,7 +183,7 @@ class TestImagingCoreResampleAccuracy: 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.HAMMING) + case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off data = ("e1 d2" "d2 c5") @@ -194,7 +194,7 @@ class TestImagingCoreResampleAccuracy: 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.BICUBIC) + case = case.resize((8, 8), Image.Resampling.BICUBIC) # fmt: off data = ("e1 e5 ee b9" "e5 e9 f3 bc" @@ -207,7 +207,7 @@ class TestImagingCoreResampleAccuracy: 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.LANCZOS) + case = case.resize((12, 12), Image.Resampling.LANCZOS) data = ( "e1 e0 db ed f5 b8" "e0 df da ec f3 b7" @@ -220,7 +220,9 @@ class TestImagingCoreResampleAccuracy: 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((100, 100), Image.BOX) + im = Image.new("RGB", (8, 8), "#1688ff").resize( + (100, 100), Image.Resampling.BOX + ) ref = Image.new("RGB", (100, 100), "#1688ff") assert_image_equal(im, ref) @@ -228,7 +230,7 @@ class TestImagingCoreResampleAccuracy: class TestCoreResampleConsistency: def make_case(self, mode, fill): im = Image.new(mode, (512, 9), fill) - return im.resize((9, 512), Image.LANCZOS), im.load()[0, 0] + return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] def run_case(self, case): channel, color = case @@ -283,20 +285,20 @@ class TestCoreResampleAlphaCorrect: @pytest.mark.xfail(reason="Current implementation isn't precise enough") def test_levels_rgba(self): case = self.make_levels_case("RGBA") - self.run_levels_case(case.resize((512, 32), Image.BOX)) - self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) - self.run_levels_case(case.resize((512, 32), Image.HAMMING)) - self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) - self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) @pytest.mark.xfail(reason="Current implementation isn't precise enough") def test_levels_la(self): case = self.make_levels_case("LA") - self.run_levels_case(case.resize((512, 32), Image.BOX)) - self.run_levels_case(case.resize((512, 32), Image.BILINEAR)) - self.run_levels_case(case.resize((512, 32), Image.HAMMING)) - self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) - self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.HAMMING)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) + self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) def make_dirty_case(self, mode, clean_pixel, dirty_pixel): i = Image.new(mode, (64, 64), dirty_pixel) @@ -321,19 +323,27 @@ class TestCoreResampleAlphaCorrect: def test_dirty_pixels_rgba(self): case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BOX), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255, 255, 0)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255, 255, 0)) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.BILINEAR), (255, 255, 0) + ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.HAMMING), (255, 255, 0) + ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.BICUBIC), (255, 255, 0) + ) + self.run_dirty_case( + case.resize((20, 20), Image.Resampling.LANCZOS), (255, 255, 0) + ) def test_dirty_pixels_la(self): case = self.make_dirty_case("LA", (255, 128), (0, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BOX), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255,)) - self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BILINEAR), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.HAMMING), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.BICUBIC), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.Resampling.LANCZOS), (255,)) class TestCoreResamplePasses: @@ -346,26 +356,26 @@ class TestCoreResamplePasses: def test_horizontal(self): im = hopper("L") with self.count(1): - im.resize((im.size[0] - 10, im.size[1]), Image.BILINEAR) + im.resize((im.size[0] - 10, im.size[1]), Image.Resampling.BILINEAR) def test_vertical(self): im = hopper("L") with self.count(1): - im.resize((im.size[0], im.size[1] - 10), Image.BILINEAR) + im.resize((im.size[0], im.size[1] - 10), Image.Resampling.BILINEAR) def test_both(self): im = hopper("L") with self.count(2): - im.resize((im.size[0] - 10, im.size[1] - 10), Image.BILINEAR) + im.resize((im.size[0] - 10, im.size[1] - 10), Image.Resampling.BILINEAR) def test_box_horizontal(self): im = hopper("L") box = (20, 0, im.size[0] - 20, im.size[1]) with self.count(1): # the same size, but different box - with_box = im.resize(im.size, Image.BILINEAR, box) + with_box = im.resize(im.size, Image.Resampling.BILINEAR, box) with self.count(2): - cropped = im.crop(box).resize(im.size, Image.BILINEAR) + cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) assert_image_similar(with_box, cropped, 0.1) def test_box_vertical(self): @@ -373,9 +383,9 @@ class TestCoreResamplePasses: box = (0, 20, im.size[0], im.size[1] - 20) with self.count(1): # the same size, but different box - with_box = im.resize(im.size, Image.BILINEAR, box) + with_box = im.resize(im.size, Image.Resampling.BILINEAR, box) with self.count(2): - cropped = im.crop(box).resize(im.size, Image.BILINEAR) + cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) assert_image_similar(with_box, cropped, 0.1) @@ -388,7 +398,7 @@ class TestCoreResampleCoefficients: draw = ImageDraw.Draw(i) draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) - px = i.resize((5, i.size[1]), Image.BICUBIC).load() + px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load() if px[2, 0] != test_color // 2: assert test_color // 2 == px[2, 0] @@ -396,7 +406,7 @@ class TestCoreResampleCoefficients: # regression test for the wrong coefficients calculation # due to bug https://github.com/python-pillow/Pillow/issues/2161 im = Image.new("RGBA", (1280, 1280), (0x20, 0x40, 0x60, 0xFF)) - histogram = im.resize((256, 256), Image.BICUBIC).histogram() + histogram = im.resize((256, 256), Image.Resampling.BICUBIC).histogram() # first channel assert histogram[0x100 * 0 + 0x20] == 0x10000 @@ -412,12 +422,12 @@ class TestCoreResampleBox: def test_wrong_arguments(self): im = hopper() for resample in ( - Image.NEAREST, - Image.BOX, - Image.BILINEAR, - Image.HAMMING, - Image.BICUBIC, - Image.LANCZOS, + 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)) @@ -456,7 +466,7 @@ class TestCoreResampleBox: for y0, y1 in split_range(dst_size[1], ytiles): for x0, x1 in split_range(dst_size[0], xtiles): box = (x0 * scale[0], y0 * scale[1], x1 * scale[0], y1 * scale[1]) - tile = im.resize((x1 - x0, y1 - y0), Image.BICUBIC, box) + tile = im.resize((x1 - x0, y1 - y0), Image.Resampling.BICUBIC, box) tiled.paste(tile, (x0, y0)) return tiled @@ -467,7 +477,7 @@ class TestCoreResampleBox: with Image.open("Tests/images/flower.jpg") as im: assert im.size == (480, 360) dst_size = (251, 188) - reference = im.resize(dst_size, Image.BICUBIC) + reference = im.resize(dst_size, Image.Resampling.BICUBIC) for tiles in [(1, 1), (3, 3), (9, 7), (100, 100)]: tiled = self.resize_tiled(im, dst_size, *tiles) @@ -483,12 +493,16 @@ class TestCoreResampleBox: assert im.size == (480, 360) dst_size = (48, 36) # Reference is cropped image resized to destination - reference = im.crop((0, 0, 473, 353)).resize(dst_size, Image.BICUBIC) - # Image.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45) - supersampled = im.resize((60, 45), Image.BOX) + reference = im.crop((0, 0, 473, 353)).resize( + dst_size, Image.Resampling.BICUBIC + ) + # Image.Resampling.BOX emulates supersampling (480 / 8 = 60, 360 / 8 = 45) + supersampled = im.resize((60, 45), Image.Resampling.BOX) - with_box = supersampled.resize(dst_size, Image.BICUBIC, (0, 0, 59.125, 44.125)) - without_box = supersampled.resize(dst_size, Image.BICUBIC) + with_box = supersampled.resize( + dst_size, Image.Resampling.BICUBIC, (0, 0, 59.125, 44.125) + ) + without_box = supersampled.resize(dst_size, Image.Resampling.BICUBIC) # error with box should be much smaller than without assert_image_similar(reference, with_box, 6) @@ -496,7 +510,7 @@ class TestCoreResampleBox: assert_image_similar(reference, without_box, 5) def test_formats(self): - for resample in [Image.NEAREST, Image.BILINEAR]: + 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) @@ -514,7 +528,7 @@ class TestCoreResampleBox: ((40, 50), (10, 0, 50, 50)), ((40, 50), (10, 20, 50, 70)), ]: - res = im.resize(size, Image.LANCZOS, box) + res = im.resize(size, Image.Resampling.LANCZOS, box) assert res.size == size assert_image_equal(res, im.crop(box), f">>> {size} {box}") @@ -528,7 +542,7 @@ class TestCoreResampleBox: ((40, 50), (10.4, 0.4, 50.4, 50.4)), ((40, 50), (10.4, 20.4, 50.4, 70.4)), ]: - res = im.resize(size, Image.LANCZOS, box) + res = im.resize(size, Image.Resampling.LANCZOS, box) assert res.size == size with pytest.raises(AssertionError, match=r"difference \d"): # check that the difference at least that much @@ -538,7 +552,7 @@ class TestCoreResampleBox: # Can skip resize for one dimension im = hopper() - for flt in [Image.NEAREST, Image.BICUBIC]: + 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)), @@ -559,7 +573,7 @@ class TestCoreResampleBox: # Can skip resize for one dimension im = hopper() - for flt in [Image.NEAREST, Image.BICUBIC]: + 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)), diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 17490e1a8..6961afa60 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -35,33 +35,33 @@ class TestImagingCoreResize: "I;16", ]: # exotic mode im = hopper(mode) - r = self.resize(im, (15, 12), Image.NEAREST) + 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): - self.resize(hopper("1"), (15, 12), Image.BILINEAR) + self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR) with pytest.raises(ValueError): - self.resize(hopper("P"), (15, 12), Image.BILINEAR) + self.resize(hopper("P"), (15, 12), Image.Resampling.BILINEAR) with pytest.raises(ValueError): - self.resize(hopper("I;16"), (15, 12), Image.BILINEAR) + self.resize(hopper("I;16"), (15, 12), Image.Resampling.BILINEAR) for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]: im = hopper(mode) - r = self.resize(im, (15, 12), Image.BILINEAR) + r = self.resize(im, (15, 12), Image.Resampling.BILINEAR) assert r.mode == mode assert r.size == (15, 12) assert r.im.bands == im.im.bands def test_reduce_filters(self): for f in [ - Image.NEAREST, - Image.BOX, - Image.BILINEAR, - Image.HAMMING, - Image.BICUBIC, - Image.LANCZOS, + 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" @@ -69,12 +69,12 @@ class TestImagingCoreResize: def test_enlarge_filters(self): for f in [ - Image.NEAREST, - Image.BOX, - Image.BILINEAR, - Image.HAMMING, - Image.BICUBIC, - Image.LANCZOS, + 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" @@ -95,12 +95,12 @@ class TestImagingCoreResize: samples["dirty"].putpixel((1, 1), 128) for f in [ - Image.NEAREST, - Image.BOX, - Image.BILINEAR, - Image.HAMMING, - Image.BICUBIC, - Image.LANCZOS, + Image.Resampling.NEAREST, + Image.Resampling.BOX, + Image.Resampling.BILINEAR, + Image.Resampling.HAMMING, + Image.Resampling.BICUBIC, + Image.Resampling.LANCZOS, ]: # samples resized with current filter references = { @@ -124,12 +124,12 @@ class TestImagingCoreResize: def test_enlarge_zero(self): for f in [ - Image.NEAREST, - Image.BOX, - Image.BILINEAR, - Image.HAMMING, - Image.BICUBIC, - Image.LANCZOS, + 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" @@ -154,8 +154,8 @@ class TestImagingCoreResize: @pytest.fixture def gradients_image(): - im = Image.open("Tests/images/radial_gradients.png") - im.load() + with Image.open("Tests/images/radial_gradients.png") as im: + im.load() try: yield im finally: @@ -164,15 +164,19 @@ def gradients_image(): class TestReducingGapResize: def test_reducing_gap_values(self, gradients_image): - ref = gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=None) - im = gradients_image.resize((52, 34), Image.BICUBIC) + ref = gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, reducing_gap=None + ) + im = gradients_image.resize((52, 34), Image.Resampling.BICUBIC) assert_image_equal(ref, im) with pytest.raises(ValueError): - gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0) + gradients_image.resize((52, 34), Image.Resampling.BICUBIC, reducing_gap=0) with pytest.raises(ValueError): - gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0.99) + gradients_image.resize( + (52, 34), Image.Resampling.BICUBIC, reducing_gap=0.99 + ) def test_reducing_gap_1(self, gradients_image): for box, epsilon in [ @@ -180,9 +184,9 @@ class TestReducingGapResize: ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10), ]: - ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( - (52, 34), Image.BICUBIC, box=box, reducing_gap=1.0 + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 ) with pytest.raises(AssertionError): @@ -196,9 +200,9 @@ class TestReducingGapResize: ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1), ]: - ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( - (52, 34), Image.BICUBIC, box=box, reducing_gap=2.0 + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 ) with pytest.raises(AssertionError): @@ -212,9 +216,9 @@ class TestReducingGapResize: ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5), ]: - ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box) + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( - (52, 34), Image.BICUBIC, box=box, reducing_gap=3.0 + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 ) with pytest.raises(AssertionError): @@ -224,9 +228,9 @@ class TestReducingGapResize: 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.BICUBIC, box=box) + ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( - (52, 34), Image.BICUBIC, box=box, reducing_gap=8.0 + (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 ) assert_image_equal(ref, im) @@ -236,8 +240,10 @@ class TestReducingGapResize: ((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5), ]: - ref = gradients_image.resize((52, 34), Image.BOX, box=box) - im = gradients_image.resize((52, 34), Image.BOX, box=box, reducing_gap=1.0) + 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) @@ -258,15 +264,22 @@ class TestImageResize: with pytest.raises(ValueError): im.resize((10, 10), "unknown") + def test_load_first(self): + # load() may change the size of the image + # Test that resize() is calling it before getting the size + with Image.open("Tests/images/g4_orientation_5.tif") as im: + im = im.resize((64, 64)) + assert im.size == (64, 64) + def test_default_filter(self): for mode in "L", "RGB", "I", "F": im = hopper(mode) - assert im.resize((20, 20), Image.BICUBIC) == im.resize((20, 20)) + 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.NEAREST) == im.resize((20, 20)) + 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.NEAREST) == im.resize((20, 20)) + assert im.resize((20, 20), Image.Resampling.NEAREST) == im.resize((20, 20)) diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 2d72ffa68..f96864c53 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -46,14 +46,14 @@ def test_zero(): def test_resample(): # Target image creation, inspected by eye. # >>> im = Image.open('Tests/images/hopper.ppm') - # >>> im = im.rotate(45, resample=Image.BICUBIC, expand=True) + # >>> im = im.rotate(45, resample=Image.Resampling.BICUBIC, expand=True) # >>> im.save('Tests/images/hopper_45.png') with Image.open("Tests/images/hopper_45.png") as target: for (resample, epsilon) in ( - (Image.NEAREST, 10), - (Image.BILINEAR, 5), - (Image.BICUBIC, 0), + (Image.Resampling.NEAREST, 10), + (Image.Resampling.BILINEAR, 5), + (Image.Resampling.BICUBIC, 0), ): im = hopper() im = im.rotate(45, resample=resample, expand=True) @@ -62,7 +62,7 @@ def test_resample(): def test_center_0(): im = hopper() - im = im.rotate(45, center=(0, 0), resample=Image.BICUBIC) + im = im.rotate(45, center=(0, 0), resample=Image.Resampling.BICUBIC) with Image.open("Tests/images/hopper_45.png") as target: target_origin = target.size[1] / 2 @@ -73,7 +73,7 @@ def test_center_0(): def test_center_14(): im = hopper() - im = im.rotate(45, center=(14, 14), resample=Image.BICUBIC) + im = im.rotate(45, center=(14, 14), resample=Image.Resampling.BICUBIC) with Image.open("Tests/images/hopper_45.png") as target: target_origin = target.size[1] / 2 - 14 @@ -90,7 +90,7 @@ def test_translate(): (target_origin, target_origin, target_origin + 128, target_origin + 128) ) - im = im.rotate(45, translate=(5, 5), resample=Image.BICUBIC) + im = im.rotate(45, translate=(5, 5), resample=Image.Resampling.BICUBIC) assert_image_similar(im, target, 1) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index dd140955d..858db9a0a 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -88,6 +88,14 @@ def test_no_resize(): assert im.size == (64, 64) +def test_load_first(): + # load() may change the size of the image + # Test that thumbnail() is calling it before performing size calculations + with Image.open("Tests/images/g4_orientation_5.tif") as im: + im.thumbnail((64, 64)) + assert im.size == (64, 10) + + # valgrind test is failing with memory allocated in libjpeg @pytest.mark.valgrind_known_error(reason="Known Failing") def test_DCT_scaling_edges(): @@ -97,24 +105,24 @@ def test_DCT_scaling_edges(): thumb = fromstring(tostring(im, "JPEG", quality=99, subsampling=0)) # small reducing_gap to amplify the effect - thumb.thumbnail((32, 32), Image.BICUBIC, reducing_gap=1.0) + thumb.thumbnail((32, 32), Image.Resampling.BICUBIC, reducing_gap=1.0) - ref = im.resize((32, 32), Image.BICUBIC) + ref = im.resize((32, 32), Image.Resampling.BICUBIC) # This is still JPEG, some error is present. Without the fix it is 11.5 assert_image_similar(thumb, ref, 1.5) def test_reducing_gap_values(): im = hopper() - im.thumbnail((18, 18), Image.BICUBIC) + im.thumbnail((18, 18), Image.Resampling.BICUBIC) ref = hopper() - ref.thumbnail((18, 18), Image.BICUBIC, reducing_gap=2.0) + ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=2.0) # reducing_gap=2.0 should be the default assert_image_equal(ref, im) ref = hopper() - ref.thumbnail((18, 18), Image.BICUBIC, reducing_gap=None) + ref.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=None) with pytest.raises(AssertionError): assert_image_equal(ref, im) @@ -125,9 +133,9 @@ def test_reducing_gap_for_DCT_scaling(): with Image.open("Tests/images/hopper.jpg") as ref: # thumbnail should call draft with reducing_gap scale ref.draft(None, (18 * 3, 18 * 3)) - ref = ref.resize((18, 18), Image.BICUBIC) + ref = ref.resize((18, 18), Image.Resampling.BICUBIC) with Image.open("Tests/images/hopper.jpg") as im: - im.thumbnail((18, 18), Image.BICUBIC, reducing_gap=3.0) + im.thumbnail((18, 18), Image.Resampling.BICUBIC, reducing_gap=3.0) - assert_image_equal(ref, im) + assert_image_similar(ref, im, 1.4) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index ea208362b..ac0e74969 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -34,20 +34,22 @@ class TestImageTransform: def test_palette(self): with Image.open("Tests/images/hopper.gif") as im: - transformed = im.transform(im.size, Image.AFFINE, [1, 0, 0, 0, 1, 0]) + transformed = im.transform( + im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] + ) assert im.palette.palette == transformed.palette.palette def test_extent(self): im = hopper("RGB") (w, h) = im.size # fmt: off - transformed = im.transform(im.size, Image.EXTENT, + transformed = im.transform(im.size, Image.Transform.EXTENT, (0, 0, w//2, h//2), # ul -> lr - Image.BILINEAR) + Image.Resampling.BILINEAR) # fmt: on - scaled = im.resize((w * 2, h * 2), Image.BILINEAR).crop((0, 0, w, h)) + scaled = im.resize((w * 2, h * 2), Image.Resampling.BILINEAR).crop((0, 0, w, h)) # undone -- precision? assert_image_similar(transformed, scaled, 23) @@ -57,15 +59,18 @@ class TestImageTransform: im = hopper("RGB") (w, h) = im.size # fmt: off - transformed = im.transform(im.size, Image.QUAD, + transformed = im.transform(im.size, Image.Transform.QUAD, (0, 0, 0, h//2, # ul -> ccw around quad: w//2, h//2, w//2, 0), - Image.BILINEAR) + Image.Resampling.BILINEAR) # fmt: on scaled = im.transform( - (w, h), Image.AFFINE, (0.5, 0, 0, 0, 0.5, 0), Image.BILINEAR + (w, h), + Image.Transform.AFFINE, + (0.5, 0, 0, 0, 0.5, 0), + Image.Resampling.BILINEAR, ) assert_image_equal(transformed, scaled) @@ -80,9 +85,9 @@ class TestImageTransform: (w, h) = im.size transformed = im.transform( im.size, - Image.EXTENT, + Image.Transform.EXTENT, (0, 0, w * 2, h * 2), - Image.BILINEAR, + Image.Resampling.BILINEAR, fillcolor="red", ) @@ -93,18 +98,21 @@ class TestImageTransform: im = hopper("RGBA") (w, h) = im.size # fmt: off - transformed = im.transform(im.size, Image.MESH, + transformed = im.transform(im.size, Image.Transform.MESH, [((0, 0, w//2, h//2), # box (0, 0, 0, h, w, h, w, 0)), # ul -> ccw around quad ((w//2, h//2, w, h), # box (0, 0, 0, h, w, h, w, 0))], # ul -> ccw around quad - Image.BILINEAR) + Image.Resampling.BILINEAR) # fmt: on scaled = im.transform( - (w // 2, h // 2), Image.AFFINE, (2, 0, 0, 0, 2, 0), Image.BILINEAR + (w // 2, h // 2), + Image.Transform.AFFINE, + (2, 0, 0, 0, 2, 0), + Image.Resampling.BILINEAR, ) checker = Image.new("RGBA", im.size) @@ -137,14 +145,16 @@ class TestImageTransform: def test_alpha_premult_resize(self): def op(im, sz): - return im.resize(sz, Image.BILINEAR) + return im.resize(sz, Image.Resampling.BILINEAR) self._test_alpha_premult(op) def test_alpha_premult_transform(self): def op(im, sz): (w, h) = im.size - return im.transform(sz, Image.EXTENT, (0, 0, w, h), Image.BILINEAR) + return im.transform( + sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR + ) self._test_alpha_premult(op) @@ -171,7 +181,7 @@ class TestImageTransform: @pytest.mark.parametrize("mode", ("RGBA", "LA")) def test_nearest_resize(self, mode): def op(im, sz): - return im.resize(sz, Image.NEAREST) + return im.resize(sz, Image.Resampling.NEAREST) self._test_nearest(op, mode) @@ -179,7 +189,9 @@ class TestImageTransform: def test_nearest_transform(self, mode): def op(im, sz): (w, h) = im.size - return im.transform(sz, Image.EXTENT, (0, 0, w, h), Image.NEAREST) + return im.transform( + sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST + ) self._test_nearest(op, mode) @@ -213,13 +225,15 @@ class TestImageTransform: def test_unknown_resampling_filter(self): with hopper() as im: (w, h) = im.size - for resample in (Image.BOX, "unknown"): + for resample in (Image.Resampling.BOX, "unknown"): with pytest.raises(ValueError): - im.transform((100, 100), Image.EXTENT, (0, 0, w, h), resample) + im.transform( + (100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample + ) class TestImageTransformAffine: - transform = Image.AFFINE + transform = Image.Transform.AFFINE def _test_image(self): im = hopper("RGB") @@ -247,7 +261,11 @@ class TestImageTransformAffine: else: transposed = im - for resample in [Image.NEAREST, Image.BILINEAR, Image.BICUBIC]: + for resample in [ + Image.Resampling.NEAREST, + Image.Resampling.BILINEAR, + Image.Resampling.BICUBIC, + ]: transformed = im.transform( transposed.size, self.transform, matrix, resample ) @@ -257,13 +275,13 @@ class TestImageTransformAffine: self._test_rotate(0, None) def test_rotate_90_deg(self): - self._test_rotate(90, Image.ROTATE_90) + self._test_rotate(90, Image.Transpose.ROTATE_90) def test_rotate_180_deg(self): - self._test_rotate(180, Image.ROTATE_180) + self._test_rotate(180, Image.Transpose.ROTATE_180) def test_rotate_270_deg(self): - self._test_rotate(270, Image.ROTATE_270) + self._test_rotate(270, Image.Transpose.ROTATE_270) def _test_resize(self, scale, epsilonscale): im = self._test_image() @@ -273,9 +291,9 @@ class TestImageTransformAffine: matrix_down = [scale, 0, 0, 0, scale, 0, 0, 0] for resample, epsilon in [ - (Image.NEAREST, 0), - (Image.BILINEAR, 2), - (Image.BICUBIC, 1), + (Image.Resampling.NEAREST, 0), + (Image.Resampling.BILINEAR, 2), + (Image.Resampling.BICUBIC, 1), ]: transformed = im.transform(size_up, self.transform, matrix_up, resample) transformed = transformed.transform( @@ -306,9 +324,9 @@ class TestImageTransformAffine: matrix_down = [1, 0, x, 0, 1, y, 0, 0] for resample, epsilon in [ - (Image.NEAREST, 0), - (Image.BILINEAR, 1.5), - (Image.BICUBIC, 1), + (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( @@ -328,4 +346,4 @@ class TestImageTransformAffine: class TestImageTransformPerspective(TestImageTransformAffine): # Repeat all tests for AFFINE transformations with PERSPECTIVE - transform = Image.PERSPECTIVE + transform = Image.Transform.PERSPECTIVE diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index a004434da..6408e1564 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,12 +1,4 @@ -from PIL.Image import ( - FLIP_LEFT_RIGHT, - FLIP_TOP_BOTTOM, - ROTATE_90, - ROTATE_180, - ROTATE_270, - TRANSPOSE, - TRANSVERSE, -) +from PIL.Image import Transpose from . import helper from .helper import assert_image_equal @@ -20,7 +12,7 @@ HOPPER = { def test_flip_left_right(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(FLIP_LEFT_RIGHT) + out = im.transpose(Transpose.FLIP_LEFT_RIGHT) assert out.mode == mode assert out.size == im.size @@ -37,7 +29,7 @@ def test_flip_left_right(): def test_flip_top_bottom(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(FLIP_TOP_BOTTOM) + out = im.transpose(Transpose.FLIP_TOP_BOTTOM) assert out.mode == mode assert out.size == im.size @@ -54,7 +46,7 @@ def test_flip_top_bottom(): def test_rotate_90(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(ROTATE_90) + out = im.transpose(Transpose.ROTATE_90) assert out.mode == mode assert out.size == im.size[::-1] @@ -71,7 +63,7 @@ def test_rotate_90(): def test_rotate_180(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(ROTATE_180) + out = im.transpose(Transpose.ROTATE_180) assert out.mode == mode assert out.size == im.size @@ -88,7 +80,7 @@ def test_rotate_180(): def test_rotate_270(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(ROTATE_270) + out = im.transpose(Transpose.ROTATE_270) assert out.mode == mode assert out.size == im.size[::-1] @@ -105,7 +97,7 @@ def test_rotate_270(): def test_transpose(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(TRANSPOSE) + out = im.transpose(Transpose.TRANSPOSE) assert out.mode == mode assert out.size == im.size[::-1] @@ -122,7 +114,7 @@ def test_transpose(): def test_tranverse(): def transpose(mode): im = HOPPER[mode] - out = im.transpose(TRANSVERSE) + out = im.transpose(Transpose.TRANSVERSE) assert out.mode == mode assert out.size == im.size[::-1] @@ -143,20 +135,31 @@ def test_roundtrip(): def transpose(first, second): return im.transpose(first).transpose(second) - assert_image_equal(im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT)) - assert_image_equal(im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM)) - assert_image_equal(im, transpose(ROTATE_90, ROTATE_270)) - assert_image_equal(im, transpose(ROTATE_180, ROTATE_180)) assert_image_equal( - im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM) + im, transpose(Transpose.FLIP_LEFT_RIGHT, Transpose.FLIP_LEFT_RIGHT) ) assert_image_equal( - im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT) + 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(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT) + im.transpose(Transpose.TRANSPOSE), + transpose(Transpose.ROTATE_270, Transpose.FLIP_LEFT_RIGHT), ) assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM) + 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(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE)) diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index a19fbf239..b839a7b14 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -368,11 +368,11 @@ def test_subtract_modulo_no_clip(): def test_soft_light(): # Arrange - im1 = Image.open("Tests/images/hopper.png") - im2 = Image.open("Tests/images/hopper-XYZ.png") + with Image.open("Tests/images/hopper.png") as im1: + with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act - new = ImageChops.soft_light(im1, im2) + # Act + new = ImageChops.soft_light(im1, im2) # Assert assert new.getpixel((64, 64)) == (163, 54, 32) @@ -381,11 +381,11 @@ def test_soft_light(): def test_hard_light(): # Arrange - im1 = Image.open("Tests/images/hopper.png") - im2 = Image.open("Tests/images/hopper-XYZ.png") + with Image.open("Tests/images/hopper.png") as im1: + with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act - new = ImageChops.hard_light(im1, im2) + # Act + new = ImageChops.hard_light(im1, im2) # Assert assert new.getpixel((64, 64)) == (144, 50, 27) @@ -394,11 +394,11 @@ def test_hard_light(): def test_overlay(): # Arrange - im1 = Image.open("Tests/images/hopper.png") - im2 = Image.open("Tests/images/hopper-XYZ.png") + with Image.open("Tests/images/hopper.png") as im1: + with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act - new = ImageChops.overlay(im1, im2) + # Act + new = ImageChops.overlay(im1, im2) # Assert assert new.getpixel((64, 64)) == (159, 50, 27) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 99f3b4e03..66a72a90e 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -140,7 +140,7 @@ def test_intent(): skip_missing() assert ImageCms.getDefaultIntent(SRGB) == 0 support = ImageCms.isIntentSupported( - SRGB, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT + SRGB, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT ) assert support == 1 @@ -153,7 +153,7 @@ def test_profile_object(): # ["sRGB built-in", "", "WhitePoint : D65 (daylight)", "", ""] assert ImageCms.getDefaultIntent(p) == 0 support = ImageCms.isIntentSupported( - p, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT + p, ImageCms.Intent.ABSOLUTE_COLORIMETRIC, ImageCms.Direction.INPUT ) assert support == 1 @@ -303,7 +303,7 @@ def test_extended_information(): def assert_truncated_tuple_equal(tup1, tup2, digits=10): # Helper function to reduce precision of tuples of floats # recursively and then check equality. - power = 10 ** digits + power = 10**digits def truncate_tuple(tuple_or_float): return tuple( @@ -593,3 +593,13 @@ def test_auxiliary_channels_isolated(): ) assert_image_equal(test_image.convert(dst_format[2]), reference_image) + + +def test_constants_deprecation(): + for enum, prefix in { + ImageCms.Intent: "INTENT_", + ImageCms.Direction: "DIRECTION_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(ImageCms, prefix + name) == enum[name] diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index b5d693796..dcc44e6e3 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -191,3 +191,12 @@ def test_rounding_errors(): assert (255, 255) == ImageColor.getcolor("white", "LA") assert (163, 33) == ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA") Image.new("LA", (1, 1), "white") + + +def test_color_too_long(): + # Arrange + color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)" + + # Act / Assert + with pytest.raises(ValueError): + ImageColor.getrgb(color_too_long) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 6be8fafa1..6755d94b8 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -183,7 +183,7 @@ def test_bitmap(): im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) with Image.open("Tests/images/pil123rgba.png") as small: - small = small.resize((50, 50), Image.NEAREST) + small = small.resize((50, 50), Image.Resampling.NEAREST) # Act draw.bitmap((10, 10), small) @@ -319,7 +319,7 @@ def test_ellipse_symmetric(): im = Image.new("RGB", (width, 100)) draw = ImageDraw.Draw(im) draw.ellipse(bbox, fill="green", outline="blue") - assert_image_equal(im, im.transpose(Image.FLIP_LEFT_RIGHT)) + assert_image_equal(im, im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)) def test_ellipse_width(): @@ -467,6 +467,23 @@ def test_shape2(): assert_image_equal_tofile(im, "Tests/images/imagedraw_shape2.png") +def test_transform(): + # Arrange + im = Image.new("RGB", (100, 100), "white") + expected = im.copy() + draw = ImageDraw.Draw(im) + + # Act + s = ImageDraw.Outline() + s.line(0, 0) + s.transform((0, 0, 0, 0, 0, 0)) + + draw.shape(s, fill=1) + + # Assert + assert_image_equal(im, expected) + + def helper_pieslice(bbox, start, end): # Arrange im = Image.new("RGB", (W, H)) @@ -638,6 +655,19 @@ def test_polygon_1px_high(): assert_image_equal_tofile(im, expected) +def test_polygon_translucent(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.polygon([(20, 80), (80, 80), (80, 20)], fill=(0, 255, 0, 127)) + + # Assert + expected = "Tests/images/imagedraw_polygon_translucent.png" + assert_image_equal_tofile(im, expected) + + def helper_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -932,6 +962,18 @@ def test_triangle_right(): ) +@pytest.mark.parametrize( + "fill, suffix", + ((BLACK, "width"), (None, "width_no_fill")), +) +def test_triangle_right_width(fill, suffix): + img, draw = create_base_image_draw((100, 100)) + draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5) + assert_image_equal_tofile( + img, os.path.join(IMAGES_PATH, "triangle_right_" + suffix + ".png") + ) + + def test_line_horizontal(): img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 5), BLACK, 2) @@ -1398,3 +1440,15 @@ def test_continuous_horizontal_edges_polygon(): assert_image_equal_tofile( img, expected, "continuous horizontal edges polygon failed" ) + + +def test_discontiguous_corners_polygon(): + img, draw = create_base_image_draw((84, 68)) + draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK) + draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK) + draw.polygon( + ((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)), + BLACK, + ) + expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png") + assert_image_similar_tofile(img, expected, 1) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 892087916..0b3aecd08 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -2,7 +2,7 @@ from io import BytesIO import pytest -from PIL import EpsImagePlugin, Image, ImageFile, features +from PIL import BmpImagePlugin, EpsImagePlugin, Image, ImageFile, _binary, features from .helper import ( assert_image, @@ -23,7 +23,7 @@ class TestImageFile: def test_parser(self): def roundtrip(format): - im = hopper("L").resize((1000, 1000), Image.NEAREST) + im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") @@ -35,9 +35,9 @@ class TestImageFile: parser = ImageFile.Parser() parser.feed(data) - imOut = parser.close() + im_out = parser.close() - return im, imOut + return im, im_out assert_image_equal(*roundtrip("BMP")) im1, im2 = roundtrip("GIF") @@ -82,6 +82,19 @@ class TestImageFile: p.feed(data) assert (48, 48) == p.image.size + @skip_unless_feature("webp") + @skip_unless_feature("webp_anim") + def test_incremental_webp(self): + with ImageFile.Parser() as p: + with open("Tests/images/hopper.webp", "rb") as f: + p.feed(f.read(1024)) + + # Check that insufficient data was given in the first feed + assert not p.image + + p.feed(f.read()) + assert (128, 128) == p.image.size + @skip_unless_feature("zlib") def test_safeblock(self): im1 = hopper() @@ -94,12 +107,6 @@ class TestImageFile: assert_image_equal(im1, im2) - def test_raise_ioerror(self): - with pytest.raises(IOError): - with pytest.warns(DeprecationWarning) as record: - ImageFile.raise_ioerror(1) - assert len(record) == 1 - def test_raise_oserror(self): with pytest.raises(OSError): ImageFile.raise_oserror(1) @@ -117,6 +124,37 @@ class TestImageFile: with pytest.raises(OSError): p.close() + def test_no_format(self): + buf = BytesIO(b"\x00" * 255) + + class DummyImageFile(ImageFile.ImageFile): + def _open(self): + self.mode = "RGB" + self._size = (1, 1) + + im = DummyImageFile(buf) + assert im.format is None + assert im.get_format_mimetype() is None + + def test_oserror(self): + im = Image.new("RGB", (1, 1)) + with pytest.raises(OSError): + im.save(BytesIO(), "JPEG2000", num_resolutions=2) + + def test_truncated(self): + b = BytesIO( + b"BM000000000000" # head_data + + _binary.o32le( + ImageFile.SAFEBLOCK + 1 + 4 + ) # header_size, so BmpImagePlugin will try to read SAFEBLOCK + 1 bytes + + ( + b"0" * ImageFile.SAFEBLOCK + ) # only SAFEBLOCK bytes, so that the header is truncated + ) + with pytest.raises(OSError) as e: + BmpImagePlugin.BmpImageFile(b) + assert str(e.value) == "Truncated File Read" + @skip_unless_feature("zlib") def test_truncated_with_errors(self): with Image.open("Tests/images/truncated_image.png") as im: @@ -158,6 +196,14 @@ class MockPyDecoder(ImageFile.PyDecoder): return -1, 0 +class MockPyEncoder(ImageFile.PyEncoder): + def encode(self, buffer): + return 1, 1, b"" + + def cleanup(self): + self.cleanup_called = True + + xoff, yoff, xsize, ysize = 10, 20, 100, 100 @@ -169,53 +215,58 @@ class MockImageFile(ImageFile.ImageFile): self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] -class TestPyDecoder: - def get_decoder(self): - decoder = MockPyDecoder(None) +class CodecsTest: + @classmethod + def setup_class(cls): + cls.decoder = MockPyDecoder(None) + cls.encoder = MockPyEncoder(None) - def closure(mode, *args): - decoder.__init__(mode, *args) - return decoder + def decoder_closure(mode, *args): + cls.decoder.__init__(mode, *args) + return cls.decoder - Image.register_decoder("MOCK", closure) - return decoder + def encoder_closure(mode, *args): + cls.encoder.__init__(mode, *args) + return cls.encoder + Image.register_decoder("MOCK", decoder_closure) + Image.register_encoder("MOCK", encoder_closure) + + +class TestPyDecoder(CodecsTest): def test_setimage(self): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - d = self.get_decoder() im.load() - assert d.state.xoff == xoff - assert d.state.yoff == yoff - assert d.state.xsize == xsize - assert d.state.ysize == ysize + assert self.decoder.state.xoff == xoff + assert self.decoder.state.yoff == yoff + assert self.decoder.state.xsize == xsize + assert self.decoder.state.ysize == ysize with pytest.raises(ValueError): - d.set_as_raw(b"\x00") + self.decoder.set_as_raw(b"\x00") def test_extents_none(self): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) im.tile = [("MOCK", None, 32, None)] - d = self.get_decoder() im.load() - assert d.state.xoff == 0 - assert d.state.yoff == 0 - assert d.state.xsize == 200 - assert d.state.ysize == 200 + assert self.decoder.state.xoff == 0 + assert self.decoder.state.yoff == 0 + assert self.decoder.state.xsize == 200 + assert self.decoder.state.ysize == 200 def test_negsize(self): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] - self.get_decoder() with pytest.raises(ValueError): im.load() @@ -229,7 +280,6 @@ class TestPyDecoder: im = MockImageFile(buf) im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] - self.get_decoder() with pytest.raises(ValueError): im.load() @@ -238,14 +288,92 @@ class TestPyDecoder: with pytest.raises(ValueError): im.load() - def test_no_format(self): + def test_decode(self): + decoder = ImageFile.PyDecoder(None) + with pytest.raises(NotImplementedError): + decoder.decode(None) + + +class TestPyEncoder(CodecsTest): + def test_setimage(self): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - assert im.format is None - assert im.get_format_mimetype() is None - def test_oserror(self): - im = Image.new("RGB", (1, 1)) - with pytest.raises(OSError): - im.save(BytesIO(), "JPEG2000", num_resolutions=2) + fp = BytesIO() + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] + ) + + assert self.encoder.state.xoff == xoff + assert self.encoder.state.yoff == yoff + assert self.encoder.state.xsize == xsize + assert self.encoder.state.ysize == ysize + + def test_extents_none(self): + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + im.tile = [("MOCK", None, 32, None)] + + fp = BytesIO() + ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) + + assert self.encoder.state.xoff == 0 + assert self.encoder.state.yoff == 0 + assert self.encoder.state.xsize == 200 + assert self.encoder.state.ysize == 200 + + def test_negsize(self): + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + self.encoder.cleanup_called = False + with pytest.raises(ValueError): + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] + ) + assert self.encoder.cleanup_called + + with pytest.raises(ValueError): + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] + ) + + def test_oversize(self): + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], + ) + + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], + ) + + def test_encode(self): + encoder = ImageFile.PyEncoder(None) + with pytest.raises(NotImplementedError): + encoder.encode(None) + + bytes_consumed, errcode = encoder.encode_to_pyfd() + assert bytes_consumed == 0 + assert ImageFile.ERRORS[errcode] == "bad configuration" + + encoder._pushes_fd = True + with pytest.raises(NotImplementedError): + encoder.encode_to_pyfd() + + with pytest.raises(NotImplementedError): + encoder.encode_to_file(None, None) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 25966801f..3ae88ea7c 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -30,7 +30,7 @@ pytestmark = skip_unless_feature("freetype2") class TestImageFont: - LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC + LAYOUT_ENGINE = ImageFont.Layout.BASIC def get_font(self): return ImageFont.truetype( @@ -90,19 +90,6 @@ class TestImageFont: ImageFont.truetype(tempfile, FONT_SIZE) ImageFont.truetype(Path(tempfile), FONT_SIZE) - def test_unavailable_layout_engine(self): - have_raqm = ImageFont.core.HAVE_RAQM - ImageFont.core.HAVE_RAQM = False - - try: - ttf = ImageFont.truetype( - FONT_PATH, FONT_SIZE, layout_engine=ImageFont.LAYOUT_RAQM - ) - finally: - ImageFont.core.HAVE_RAQM = have_raqm - - assert ttf.layout_engine == ImageFont.LAYOUT_BASIC - def _render(self, font): txt = "Hello World!" ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE) @@ -157,7 +144,6 @@ class TestImageFont: draw.text((10, 10), txt, font=ttf) draw.rectangle((10, 10, 10 + size[0], 10 + size[1])) - # Epsilon ~.5 fails with FreeType 2.7 assert_image_similar_tofile( im, "Tests/images/rectangle_surrounding_text.png", 2.5 ) @@ -185,7 +171,7 @@ class TestImageFont: im = Image.new(mode, (1, 1), 0) d = ImageDraw.Draw(im) - if self.LAYOUT_ENGINE == ImageFont.LAYOUT_BASIC: + if self.LAYOUT_ENGINE == ImageFont.Layout.BASIC: length = d.textlength(text, f) assert length == length_basic else: @@ -218,8 +204,7 @@ class TestImageFont: draw = ImageDraw.Draw(im) draw.text((0, 0), TEST_TEXT, font=ttf) - # Epsilon ~.5 fails with FreeType 2.7 - assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) + assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 0.01) # Test that text() can pass on additional arguments # to multiline_text() @@ -234,9 +219,8 @@ class TestImageFont: draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align) - # Epsilon ~.5 fails with FreeType 2.7 assert_image_similar_tofile( - im, "Tests/images/multiline_text" + ext + ".png", 6.2 + im, "Tests/images/multiline_text" + ext + ".png", 0.01 ) def test_unknown_align(self): @@ -291,8 +275,7 @@ class TestImageFont: draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10) - # Epsilon ~.5 fails with FreeType 2.7 - assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 6.2) + assert_image_similar_tofile(im, "Tests/images/multiline_text_spacing.png", 2.5) def test_rotated_transposed_font(self): img_grey = Image.new("L", (100, 100)) @@ -300,7 +283,7 @@ class TestImageFont: word = "testing" font = self.get_font() - orientation = Image.ROTATE_90 + orientation = Image.Transpose.ROTATE_90 transposed_font = ImageFont.TransposedFont(font, orientation=orientation) # Original font @@ -339,7 +322,7 @@ class TestImageFont: # Arrange text = "mask this" font = self.get_font() - orientation = Image.ROTATE_90 + orientation = Image.Transpose.ROTATE_90 transposed_font = ImageFont.TransposedFont(font, orientation=orientation) # Act @@ -610,7 +593,7 @@ class TestImageFont: # Arrange t = self.get_font() # Act / Assert - if t.layout_engine == ImageFont.LAYOUT_BASIC: + if t.layout_engine == ImageFont.Layout.BASIC: with pytest.raises(KeyError): t.getmask("абвг", direction="rtl") with pytest.raises(KeyError): @@ -740,30 +723,26 @@ class TestImageFont: d.textbbox((0, 0), "test", font=default_font) @pytest.mark.parametrize( - "anchor, left, left_old, top", + "anchor, left, top", ( # test horizontal anchors - ("ls", 0, 0, -36), - ("ms", -64, -65, -36), - ("rs", -128, -129, -36), + ("ls", 0, -36), + ("ms", -64, -36), + ("rs", -128, -36), # test vertical anchors - ("ma", -64, -65, 16), - ("mt", -64, -65, 0), - ("mm", -64, -65, -17), - ("mb", -64, -65, -44), - ("md", -64, -65, -51), + ("ma", -64, 16), + ("mt", -64, 0), + ("mm", -64, -17), + ("mb", -64, -44), + ("md", -64, -51), ), ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), ) - def test_anchor(self, anchor, left, left_old, top): + def test_anchor(self, anchor, left, top): name, text = "quick", "Quick" path = f"Tests/images/test_anchor_{name}_{anchor}.png" - freetype = parse_version(features.version_module("freetype2")) - if freetype < parse_version("2.4"): - width, height = (129, 44) - left = left_old - elif self.LAYOUT_ENGINE == ImageFont.LAYOUT_RAQM: + if self.LAYOUT_ENGINE == ImageFont.Layout.RAQM: width, height = (129, 44) else: width, height = (128, 44) @@ -869,6 +848,22 @@ class TestImageFont: assert_image_equal_tofile(im, target) + def test_bitmap_font_stroke(self): + text = "Bitmap Font" + layout_name = ["basic", "raqm"][self.LAYOUT_ENGINE] + target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" + font = ImageFont.truetype( + "Tests/fonts/DejaVuSans/DejaVuSans-24-8-stripped.ttf", + 24, + layout_engine=self.LAYOUT_ENGINE, + ) + + im = Image.new("RGB", (160, 35), "white") + draw = ImageDraw.Draw(im) + draw.text((2, 2), text, "black", font, stroke_width=2, stroke_fill="red") + + assert_image_similar_tofile(im, target, 0.03) + def test_standard_embedded_color(self): txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=self.LAYOUT_ENGINE) @@ -880,7 +875,6 @@ class TestImageFont: assert_image_similar_tofile(im, "Tests/images/standard_embedded.png", 6.2) - @skip_unless_feature_version("freetype2", "2.5.0") def test_cbdt(self): try: font = ImageFont.truetype( @@ -895,11 +889,10 @@ class TestImageFont: d.text((10, 10), "\U0001f469", font=font, embedded_color=True) assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2) - except IOError as e: # pragma: no cover + except OSError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") pytest.skip("freetype compiled without libpng or CBDT support") - @skip_unless_feature_version("freetype2", "2.5.0") def test_cbdt_mask(self): try: font = ImageFont.truetype( @@ -916,11 +909,10 @@ class TestImageFont: assert_image_similar_tofile( im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2 ) - except IOError as e: # pragma: no cover + except OSError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") pytest.skip("freetype compiled without libpng or CBDT support") - @skip_unless_feature_version("freetype2", "2.5.1") def test_sbix(self): try: font = ImageFont.truetype( @@ -935,11 +927,10 @@ class TestImageFont: d.text((50, 50), "\uE901", font=font, embedded_color=True) assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) - except IOError as e: # pragma: no cover + except OSError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") pytest.skip("freetype compiled without libpng or SBIX support") - @skip_unless_feature_version("freetype2", "2.5.1") def test_sbix_mask(self): try: font = ImageFont.truetype( @@ -954,7 +945,7 @@ class TestImageFont: d.text((50, 50), "\uE901", (100, 0, 0), font=font) assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) - except IOError as e: # pragma: no cover + except OSError as e: # pragma: no cover assert str(e) in ("unimplemented feature", "unknown file format") pytest.skip("freetype compiled without libpng or SBIX support") @@ -991,10 +982,9 @@ class TestImageFont: @skip_unless_feature("raqm") class TestImageFont_RaqmLayout(TestImageFont): - LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM + LAYOUT_ENGINE = ImageFont.Layout.RAQM -@skip_unless_feature_version("freetype2", "2.4", "Different metrics") def test_render_mono_size(): # issue 4177 @@ -1003,25 +993,13 @@ def test_render_mono_size(): ttf = ImageFont.truetype( "Tests/fonts/DejaVuSans/DejaVuSans.ttf", 18, - layout_engine=ImageFont.LAYOUT_BASIC, + layout_engine=ImageFont.Layout.BASIC, ) draw.text((10, 10), "r" * 10, "black", ttf) assert_image_equal_tofile(im, "Tests/images/text_mono.gif") -def test_freetype_deprecation(monkeypatch): - # Arrange: mock features.version_module to return fake FreeType version - def fake_version_module(module): - return "2.7" - - monkeypatch.setattr(features, "version_module", fake_version_module) - - # Act / Assert - with pytest.warns(DeprecationWarning): - ImageFont.truetype(FONT_PATH, FONT_SIZE) - - @pytest.mark.parametrize( "test_file", [ @@ -1033,3 +1011,25 @@ def test_oom(test_file): font = ImageFont.truetype(BytesIO(f.read())) with pytest.raises(Image.DecompressionBombError): font.getmask("Test Text") + + +def test_raqm_missing_warning(monkeypatch): + monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) + with pytest.warns(UserWarning) as record: + font = ImageFont.truetype( + FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM + ) + assert font.layout_engine == ImageFont.Layout.BASIC + assert str(record[-1].message) == ( + "Raqm layout was requested, but Raqm is not available. " + "Falling back to basic layout." + ) + + +def test_constants_deprecation(): + for enum, prefix in { + ImageFont.Layout: "LAYOUT_", + }.items(): + for name in enum.__members__: + with pytest.warns(DeprecationWarning): + assert getattr(ImageFont, prefix + name) == enum[name] diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index f2a914ff7..ffb70cf17 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,13 +1,8 @@ import pytest -from packaging.version import parse as parse_version -from PIL import Image, ImageDraw, ImageFont, features +from PIL import Image, ImageDraw, ImageFont -from .helper import ( - assert_image_similar_tofile, - skip_unless_feature, - skip_unless_feature_version, -) +from .helper import assert_image_similar_tofile, skip_unless_feature FONT_SIZE = 20 FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" @@ -252,11 +247,6 @@ def test_getlength_combine(mode, direction, text): pytest.skip("libraqm 0.7 or greater not available") -# FreeType 2.5.1 README: Miscellaneous Changes: -# Improved computation of emulated vertical metrics for TrueType fonts. -@skip_unless_feature_version( - "freetype2", "2.5.1", "FreeType <2.5.1 has incompatible ttb metrics" -) @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) def test_anchor_ttb(anchor): text = "f" @@ -315,14 +305,6 @@ combine_tests = ( "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] ) def test_combine(name, text, dir, anchor, epsilon): - if ( - parse_version(features.version_module("freetype2")) < parse_version("2.5.1") - and dir == "ttb" - ): - # FreeType 2.5.1 README: Miscellaneous Changes: - # Improved computation of emulated vertical metrics for TrueType fonts. - pytest.skip("FreeType <2.5.1 has incompatible ttb metrics") - path = f"Tests/images/test_combine_{name}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index c36285451..fa2291582 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -6,7 +6,7 @@ import pytest from PIL import Image, ImageGrab -from .helper import assert_image, assert_image_equal_tofile, skip_unless_feature +from .helper import assert_image_equal_tofile, skip_unless_feature class TestImageGrab: @@ -14,25 +14,20 @@ class TestImageGrab: sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" ) def test_grab(self): - for im in [ - ImageGrab.grab(), - ImageGrab.grab(include_layered_windows=True), - ImageGrab.grab(all_screens=True), - ]: - assert_image(im, im.mode, im.size) + ImageGrab.grab() + ImageGrab.grab(include_layered_windows=True) + ImageGrab.grab(all_screens=True) im = ImageGrab.grab(bbox=(10, 20, 50, 80)) - assert_image(im, im.mode, (40, 60)) + assert im.size == (40, 60) @skip_unless_feature("xcb") def test_grab_x11(self): try: if sys.platform not in ("win32", "darwin"): - im = ImageGrab.grab() - assert_image(im, im.mode, im.size) + ImageGrab.grab() - im2 = ImageGrab.grab(xdisplay="") - assert_image(im2, im2.mode, im2.size) + ImageGrab.grab(xdisplay="") except OSError as e: pytest.skip(str(e)) @@ -71,8 +66,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200 assert str(e.value) == "ImageGrab.grabclipboard() is macOS and Windows only" return - im = ImageGrab.grabclipboard() - assert_image(im, im.mode, im.size) + ImageGrab.grabclipboard() @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") def test_grabclipboard_file(self): diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 239806796..39d91eade 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,9 +1,11 @@ +import pytest + from PIL import Image, ImageMath def pixel(im): if hasattr(im, "im"): - return "{} {}".format(im.mode, repr(im.getpixel((0, 0)))) + return f"{im.mode} {repr(im.getpixel((0, 0)))}" else: if isinstance(im, int): return int(im) # hack to deal with booleans @@ -50,6 +52,19 @@ def test_ops(): assert pixel(ImageMath.eval("float(B)**33", images)) == "F 8589934592.0" +@pytest.mark.parametrize( + "expression", + ( + "exec('pass')", + "(lambda: exec('pass'))()", + "(lambda: (lambda: exec('pass'))())()", + ), +) +def test_prevent_exec(expression): + with pytest.raises(ValueError): + ImageMath.eval(expression) + + def test_logical(): assert pixel(ImageMath.eval("not A", images)) == 0 assert pixel(ImageMath.eval("A and B", images)) == "L 2" diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 368c2bba1..6de953068 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -48,12 +48,8 @@ def img_string_normalize(im): return img_to_string(string_to_img(im)) -def assert_img_equal(A, B): - assert img_to_string(A) == img_to_string(B) - - -def assert_img_equal_img_string(A, Bstring): - assert img_to_string(A) == img_string_normalize(Bstring) +def assert_img_equal_img_string(a, b_string): + assert img_to_string(a) == img_string_normalize(b_string) def test_str_to_img(): diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 6aa1cf35e..87fffa7b7 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -63,6 +63,7 @@ def test_sanity(): ImageOps.grayscale(hopper("L")) ImageOps.grayscale(hopper("RGB")) + ImageOps.invert(hopper("1")) ImageOps.invert(hopper("L")) ImageOps.invert(hopper("RGB")) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 5a59b7799..475d249ed 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -16,10 +16,10 @@ def test_sanity(): def test_reload(): - im = Image.open("Tests/images/hopper.gif") - original = im.copy() - im.palette.dirty = 1 - assert_image_equal(im.convert("RGB"), original.convert("RGB")) + with Image.open("Tests/images/hopper.gif") as im: + original = im.copy() + im.palette.dirty = 1 + assert_image_equal(im.convert("RGB"), original.convert("RGB")) def test_getcolor(): diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 0835fdb43..de3920cf5 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -70,9 +70,11 @@ def test_invalid_coords(): coords = ["a", "b"] # Act / Assert - with pytest.raises(SystemError): + with pytest.raises(ValueError) as e: ImagePath.Path(coords) + assert str(e.value) == "incorrect coordinate type" + def test_path_odd_number_of_coordinates(): # Arrange @@ -90,6 +92,8 @@ def test_path_odd_number_of_coordinates(): [ ([0, 1, 2, 3], (0.0, 1.0, 2.0, 3.0)), ([3, 2, 1, 0], (1.0, 0.0, 3.0, 2.0)), + (0, (0.0, 0.0, 0.0, 0.0)), + (1, (0.0, 0.0, 0.0, 0.0)), ], ) def test_getbbox(coords, expected): @@ -170,7 +174,7 @@ def test_overflow_segfault(): # through to the sequence. Seeing this on 32-bit Windows. with pytest.raises((TypeError, MemoryError)): # post patch, this fails with a memory error - x = evil() + x = Evil() # This fails due to the invalid malloc above, # and segfaults @@ -178,7 +182,7 @@ def test_overflow_segfault(): x[i] = b"0" * 16 -class evil: +class Evil: def __init__(self): self.corrupt = Image.core.path(0x4000000000000000) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 53b1fef7c..a42240d49 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,8 +1,10 @@ +import warnings + import pytest from PIL import ImageQt -from .helper import hopper +from .helper import assert_image_similar, hopper pytestmark = pytest.mark.skipif( not ImageQt.qt_is_installed, reason="Qt bindings are not installed" @@ -30,10 +32,10 @@ def test_rgb(): def checkrgb(r, g, b): val = ImageQt.rgb(r, g, b) - val = val % 2 ** 24 # drop the alpha + val = val % 2**24 # drop the alpha assert val >> 16 == r - assert ((val >> 8) % 2 ** 8) == g - assert val % 2 ** 8 == b + assert ((val >> 8) % 2**8) == g + assert val % 2**8 == b checkrgb(0, 0, 0) checkrgb(255, 0, 0) @@ -42,12 +44,19 @@ def test_rgb(): def test_image(): - for mode in ("1", "RGB", "RGBA", "L", "P"): - ImageQt.ImageQt(hopper(mode)) + modes = ["1", "RGB", "RGBA", "L", "P"] + qt_format = ImageQt.QImage.Format if ImageQt.qt_version == "6" else ImageQt.QImage + if hasattr(qt_format, "Format_Grayscale16"): # Qt 5.13+ + modes.append("I;16") + + for mode in modes: + im = hopper(mode) + roundtripped_im = ImageQt.fromqimage(ImageQt.ImageQt(im)) + if mode not in ("RGB", "RGBA"): + im = im.convert("RGB") + assert_image_similar(roundtripped_im, im, 1) def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): ImageQt.ImageQt("Tests/images/hopper.gif") - - assert not record diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 7cf237b46..628120cc4 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -65,12 +65,12 @@ def test_libtiff(): def test_consecutive(): with Image.open("Tests/images/multipage.tiff") as im: - firstFrame = None + first_frame = None for frame in ImageSequence.Iterator(im): - if firstFrame is None: - firstFrame = frame.copy() + if first_frame is None: + first_frame = frame.copy() for frame in ImageSequence.Iterator(im): - assert_image_equal(frame, firstFrame) + assert_image_equal(frame, first_frame) break diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 5981e22c0..55d7c9479 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -51,6 +51,16 @@ def test_show(): assert ImageShow.show(im) +def test_show_without_viewers(): + viewers = ImageShow._viewers + ImageShow._viewers = [] + + im = hopper() + assert not ImageShow.show(im) + + ImageShow._viewers = viewers + + def test_viewer(): viewer = ImageShow.Viewer() @@ -79,3 +89,20 @@ def test_ipythonviewer(): im = hopper() assert test_viewer.show(im) == 1 + + +@pytest.mark.skipif( + not on_ci() or is_win32(), + reason="Only run on CIs; hangs on Windows CIs", +) +def test_file_deprecated(tmp_path): + 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() diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 9474ff6f9..5717fe150 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -51,8 +51,8 @@ def test_constant(): st = ImageStat.Stat(im) assert st.extrema[0] == (128, 128) - assert st.sum[0] == 128 ** 3 - assert st.sum2[0] == 128 ** 4 + assert st.sum[0] == 128**3 + assert st.sum2[0] == 128**4 assert st.mean[0] == 128 assert st.median[0] == 128 assert st.rms[0] == 128 diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index 928b8cbd1..9df66df2d 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -75,8 +75,16 @@ def test_photoimage_blank(): assert im_tk.width() == 100 assert im_tk.height() == 100 - # reloaded = ImageTk.getimage(im_tk) - # assert_image_equal(reloaded, im) + im = Image.new(mode, (100, 100)) + reloaded = ImageTk.getimage(im_tk) + assert_image_equal(reloaded.convert(mode), im) + + +def test_box_deprecation(): + im = hopper() + im_tk = ImageTk.PhotoImage(im) + with pytest.warns(DeprecationWarning): + im_tk.paste(im, (0, 0, 128, 128)) def test_bitmapimage(): diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index c51a66089..df1305655 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -1,4 +1,3 @@ -import ctypes from io import BytesIO from PIL import Image, ImageWin @@ -8,6 +7,7 @@ from .helper import hopper, is_win32 # see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652 if is_win32(): + import ctypes import ctypes.wintypes class BITMAPFILEHEADER(ctypes.Structure): diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index af7eae935..979806cae 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -444,6 +444,8 @@ class TestLibUnpack: self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0)) self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) + self.assert_unpack("RGBA", "BGRA;16L", 8, (6, 4, 2, 8), (14, 12, 10, 16)) + self.assert_unpack("RGBA", "BGRA;16B", 8, (5, 3, 1, 7), (13, 11, 9, 15)) self.assert_unpack( "RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12) ) diff --git a/Tests/test_map.py b/Tests/test_map.py index 752c5f268..d816bddaf 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -24,13 +24,19 @@ def test_overflow(): def test_tobytes(): + # Note that this image triggers the decompression bomb warning: + max_pixels = Image.MAX_IMAGE_PIXELS + Image.MAX_IMAGE_PIXELS = None + # Previously raised an access violation on Windows with Image.open("Tests/images/l2rgb_read.bmp") as im: with pytest.raises((ValueError, MemoryError, OSError)): im.tobytes() + Image.MAX_IMAGE_PIXELS = max_pixels -@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") + +@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_ysize(): numpy = pytest.importorskip("numpy", reason="NumPy not installed") diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 0571aabf4..6e8a2ac58 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -26,51 +26,51 @@ def test_basic(tmp_path): def basic(mode): - imIn = original.convert(mode) - verify(imIn) + im_in = original.convert(mode) + verify(im_in) - w, h = imIn.size + w, h = im_in.size - imOut = imIn.copy() - verify(imOut) # copy + im_out = im_in.copy() + verify(im_out) # copy - imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h)) - verify(imOut) # 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") - imIn.save(filename) + im_in.save(filename) - with Image.open(filename) as imOut: + with Image.open(filename) as im_out: - verify(imIn) - verify(imOut) + verify(im_in) + verify(im_out) - imOut = imIn.crop((0, 0, w, h)) - verify(imOut) + im_out = im_in.crop((0, 0, w, h)) + verify(im_out) - imOut = Image.new(mode, (w, h), None) - imOut.paste(imIn.crop((0, 0, w // 2, h)), (0, 0)) - imOut.paste(imIn.crop((w // 2, 0, w, h)), (w // 2, 0)) + 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)) - verify(imIn) - verify(imOut) + verify(im_in) + verify(im_out) - imIn = Image.new(mode, (1, 1), 1) - assert imIn.getpixel((0, 0)) == 1 + im_in = Image.new(mode, (1, 1), 1) + assert im_in.getpixel((0, 0)) == 1 - imIn.putpixel((0, 0), 2) - assert imIn.getpixel((0, 0)) == 2 + im_in.putpixel((0, 0), 2) + assert im_in.getpixel((0, 0)) == 2 if mode == "L": maximum = 255 else: maximum = 32767 - imIn = Image.new(mode, (1, 1), 256) - assert imIn.getpixel((0, 0)) == min(256, maximum) + im_in = Image.new(mode, (1, 1), 256) + assert im_in.getpixel((0, 0)) == min(256, maximum) - imIn.putpixel((0, 0), 512) - assert imIn.getpixel((0, 0)) == min(512, maximum) + im_in.putpixel((0, 0), 512) + assert im_in.getpixel((0, 0)) == min(512, maximum) basic("L") diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index def7adf3f..9735837bc 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import Image @@ -189,8 +191,9 @@ def test_putdata(): assert len(im.getdata()) == len(arr) -def test_roundtrip_eye(): - for dtype in ( +@pytest.mark.parametrize( + "dtype", + ( bool, numpy.bool8, numpy.int8, @@ -202,9 +205,11 @@ def test_roundtrip_eye(): float, numpy.float32, numpy.float64, - ): - arr = numpy.eye(10, dtype=dtype) - numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr))) + ), +) +def test_roundtrip_eye(dtype): + arr = numpy.eye(10, dtype=dtype) + numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr))) def test_zero_size(): @@ -234,6 +239,5 @@ def test_no_resource_warning_for_numpy_array(): with Image.open(test_file) as im: # Act/Assert - with pytest.warns(None) as record: + with warnings.catch_warnings(): array(im) - assert not record diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index 2d428e95f..ea9b33dfc 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -115,6 +115,6 @@ def test_pdf_repr(): assert pdf_repr(True) == b"true" assert pdf_repr(False) == b"false" assert pdf_repr(None) == b"null" - assert pdf_repr(b"a)/b\\(c") == br"(a\)/b\\\(c)" + assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)" assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index a10dcec8c..5fd045855 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -2,9 +2,12 @@ import pickle import pytest -from PIL import Image +from PIL import Image, ImageDraw, ImageFont -from .helper import skip_unless_feature +from .helper import assert_image_equal, skip_unless_feature + +FONT_SIZE = 20 +FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode): @@ -85,10 +88,55 @@ def test_pickle_la_mode_with_palette(tmp_path): @skip_unless_feature("webp") def test_pickle_tell(): # Arrange - image = Image.open("Tests/images/hopper.webp") + with Image.open("Tests/images/hopper.webp") as image: - # Act: roundtrip - unpickled_image = pickle.loads(pickle.dumps(image)) + # Act: roundtrip + unpickled_image = pickle.loads(pickle.dumps(image)) # Assert assert unpickled_image.tell() == 0 + + +def helper_assert_pickled_font_images(font1, font2): + # Arrange + im1 = Image.new(mode="RGBA", size=(300, 100)) + im2 = Image.new(mode="RGBA", size=(300, 100)) + draw1 = ImageDraw.Draw(im1) + draw2 = ImageDraw.Draw(im2) + txt = "Hello World!" + + # Act + draw1.text((10, 10), txt, font=font1) + draw2.text((10, 10), txt, font=font2) + + # Assert + assert_image_equal(im1, im2) + + +@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) +def test_pickle_font_string(protocol): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + # Act: roundtrip + pickled_font = pickle.dumps(font, protocol) + unpickled_font = pickle.loads(pickled_font) + + # Assert + helper_assert_pickled_font_images(font, unpickled_font) + + +@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) +def test_pickle_font_file(tmp_path, protocol): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + filename = str(tmp_path / "temp.pkl") + + # Act: roundtrip + with open(filename, "wb") as f: + pickle.dump(font, f, protocol) + with open(filename, "rb") as f: + unpickled_font = pickle.load(f) + + # Assert + helper_assert_pickled_font_images(font, unpickled_font) diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index f9eaf9b19..b5f9d4424 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -21,6 +21,6 @@ from PIL import Image ) def test_crashes(test_file): with open(test_file, "rb") as f: - im = Image.open(f) - with pytest.raises(OSError): - im.load() + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index 6cdb8e44d..143765b8e 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -40,6 +40,7 @@ from .helper import on_ci ) @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") @pytest.mark.filterwarnings("ignore:Metadata warning") +@pytest.mark.filterwarnings("ignore:Truncated File Read") def test_tiff_crashes(test_file): try: with Image.open(test_file) as im: diff --git a/Tests/test_util.py b/Tests/test_util.py index 5c153947f..ab2a4d0c4 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -8,7 +8,7 @@ def test_is_path(): fp = "filename.ext" # Act - it_is = _util.isPath(fp) + it_is = _util.is_path(fp) # Assert assert it_is @@ -21,7 +21,7 @@ def test_path_obj_is_path(): test_path = Path("filename.ext") # Act - it_is = _util.isPath(test_path) + it_is = _util.is_path(test_path) # Assert assert it_is @@ -48,7 +48,7 @@ def test_is_not_path(tmp_path): pass # Act - it_is_not = _util.isPath(fp) + it_is_not = _util.is_path(fp) # Assert assert not it_is_not @@ -75,7 +75,7 @@ def test_is_directory(): directory = "Tests" # Act - it_is = _util.isDirectory(directory) + it_is = _util.is_directory(directory) # Assert assert it_is @@ -86,7 +86,7 @@ def test_is_not_directory(): text = "abc" # Act - it_is_not = _util.isDirectory(text) + it_is_not = _util.is_directory(text) # Assert assert not it_is_not @@ -96,7 +96,7 @@ def test_deferred_error(): # Arrange # Act - thing = _util.deferred_error(ValueError("Some error text")) + thing = _util.DeferredError(ValueError("Some error text")) # Assert with pytest.raises(ValueError): diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index c6c7506a3..31fc2adaa 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,14 +1,15 @@ #!/bin/bash # install libimagequant -archive=libimagequant-2.15.1 +archive=libimagequant-4.0.0 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz -pushd $archive +pushd $archive/imagequant-sys -make shared -sudo cp libimagequant.so* /usr/lib/ -sudo cp libimagequant.h /usr/include/ +cargo install cargo-c +cargo cinstall --prefix=/usr --destdir=. +sudo cp usr/lib/libimagequant.so* /usr/lib/ +sudo cp usr/include/libimagequant.h /usr/include/ popd diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index 7321b80f0..914e71e53 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -3,7 +3,7 @@ archive=openjpeg-2.4.0 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index a7ce16792..992503650 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,13 +2,13 @@ # install raqm -archive=raqm-0.7.1 +archive=libraqm-0.9.0 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive -./configure --prefix=/usr && make -j4 && sudo make -j4 install +meson build --prefix=/usr && sudo ninja -C build install popd diff --git a/depends/install_raqm_cmake.sh b/depends/install_raqm_cmake.sh index c0dcd93b7..7d2c399df 100755 --- a/depends/install_raqm_cmake.sh +++ b/depends/install_raqm_cmake.sh @@ -4,7 +4,7 @@ archive=raqm-cmake-99300ff3 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 4a4e74305..a419a7646 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,9 +1,9 @@ #!/bin/bash # install webp -archive=libwebp-1.2.1 +archive=libwebp-1.2.2 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz pushd $archive diff --git a/docs/COPYING b/docs/COPYING index f2466d659..25f03b343 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2010-2021 by Alex Clark and contributors + Copyright © 2010-2022 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/Makefile b/docs/Makefile index 686f0119e..0d352302f 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = sphinx-build +SPHINXBUILD = python3 -m sphinx.cmd.build PAPER = BUILDDIR = _build @@ -41,38 +41,48 @@ help: clean: -rm -rf $(BUILDDIR)/* +install-sphinx: + python3 -c "import sphinx" > /dev/null 2>&1 || python3 -m pip install sphinx + html: + $(MAKE) install-sphinx $(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: + $(MAKE) install-sphinx $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: + $(MAKE) install-sphinx $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: + $(MAKE) install-sphinx $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: + $(MAKE) install-sphinx $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: + $(MAKE) install-sphinx $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: + $(MAKE) install-sphinx $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ @@ -82,6 +92,7 @@ qthelp: @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc" devhelp: + $(MAKE) install-sphinx $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @@ -91,11 +102,13 @@ devhelp: @echo "# devhelp" epub: + $(MAKE) install-sphinx $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: + $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @@ -103,22 +116,26 @@ latex: "(use \`make latexpdf' here to do that automatically)." latexpdf: + $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: + $(MAKE) install-sphinx $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: + $(MAKE) install-sphinx $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: + $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @@ -126,28 +143,33 @@ texinfo: "(use \`make info' here to do that automatically)." info: + $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: + $(MAKE) install-sphinx $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: + $(MAKE) install-sphinx $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: + $(MAKE) install-sphinx $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: + $(MAKE) install-sphinx $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/about.rst b/docs/about.rst index 51b583ea0..03829c133 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -12,14 +12,14 @@ The fork author's goal is to foster and support active development of PIL throug .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions .. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow -.. _Travis CI: https://travis-ci.com/github/python-pillow/pillow-wheels +.. _Travis CI: https://app.travis-ci.com/github/python-pillow/pillow-wheels .. _GitHub: https://github.com/python-pillow/Pillow .. _Python Package Index: https://pypi.org/project/Pillow/ License ------- -Like PIL, Pillow is `licensed under the open source HPND License `_ +Like PIL, Pillow is `licensed under the open source HPND License `_ Why a fork? ----------- diff --git a/docs/conf.py b/docs/conf.py index 807281965..2ed236b18 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,8 +16,6 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) -import sphinx_rtd_theme - import PIL # -- General configuration ------------------------------------------------ @@ -53,7 +51,7 @@ master_doc = "index" # General information about the project. project = "Pillow (PIL Fork)" -copyright = "1995-2011 Fredrik Lundh, 2010-2021 Alex Clark and Contributors" +copyright = "1995-2011 Fredrik Lundh, 2010-2022 Alex Clark and Contributors" author = "Fredrik Lundh, Alex Clark and Contributors" # The version info for the project you're documenting, acts as replacement for @@ -126,13 +124,15 @@ nitpicky = True # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = "sphinx_rtd_theme" -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +html_theme = "furo" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -# html_theme_options = {} +html_theme_options = { + "light_logo": "pillow-logo-dark-text.png", + "dark_logo": "pillow-logo.png", +} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] @@ -146,7 +146,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = "resources/pillow-logo.png" +# html_logo = "resources/pillow-logo.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 @@ -311,10 +311,7 @@ texinfo_documents = [ def setup(app): - app.add_js_file("js/script.js") - app.add_css_file("css/styles.css") app.add_css_file("css/dark.css") - app.add_css_file("css/light.css") # GitHub repo for sphinx-issues @@ -322,7 +319,7 @@ issues_github_path = "python-pillow/Pillow" # sphinxext.opengraph ogp_image = ( - "https://raw.githubusercontent.com/python-pillow/pillow-logo/master/" + "https://raw.githubusercontent.com/python-pillow/pillow-logo/main/" "pillow-logo-dark-text-1280x640.png" ) ogp_image_alt = "Pillow" diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 45720ccc0..d4d5907ea 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,62 +12,12 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -FreeType 2.7 -~~~~~~~~~~~~ - -.. deprecated:: 8.1.0 - -Support for FreeType 2.7 is deprecated and will be removed in Pillow 9.0.0 (2022-01-02), -when FreeType 2.8 will be the minimum supported. - -We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). - -.. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ - -Image.show command parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 7.2.0 - -The ``command`` parameter will be removed in Pillow 9.0.0 (2022-01-02). -Use a subclass of :py:class:`.ImageShow.Viewer` instead. - -Image._showxv -~~~~~~~~~~~~~ - -.. deprecated:: 7.2.0 - -``Image._showxv`` will be removed in Pillow 9.0.0 (2022-01-02). -Use :py:meth:`.Image.Image.show` instead. If custom behaviour is required, use -:py:func:`.ImageShow.register` to add a custom :py:class:`.ImageShow.Viewer` class. - -ImageFile.raise_ioerror -~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 7.2.0 - -``IOError`` was merged into ``OSError`` in Python 3.3. -So, ``ImageFile.raise_ioerror`` will be removed in Pillow 9.0.0 (2022-01-02). -Use ``ImageFile.raise_oserror`` instead. - -PILLOW_VERSION constant -~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 5.2.0 - -``PILLOW_VERSION`` will be removed in Pillow 9.0.0 (2022-01-02). -Use ``__version__`` instead. - -It was initially removed in Pillow 7.0.0, but brought back in 7.1.0 to give projects -more time to upgrade. - Tk/Tcl 8.4 ~~~~~~~~~~ .. deprecated:: 8.2.0 -Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), +Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), when Tk/Tcl 8.5 will be the minimum supported. Categories @@ -75,7 +25,7 @@ Categories .. deprecated:: 8.2.0 -``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), +``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. @@ -90,25 +40,173 @@ JpegImagePlugin.convert_dict_qtables JPEG ``quantization`` is now automatically converted, but still returned as a dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer performs any operations on the data given to it, has been deprecated and will be -removed in Pillow 10.0.0 (2023-01-02). +removed in Pillow 10.0.0 (2023-07-01). ImagePalette size parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 8.4.0 -The ``size`` parameter will be removed in Pillow 10.0.0 (2023-01-02). +The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01). Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by default, and the size parameter could be used to override that. Pillow 8.3.0 removed the default required length, also removing the need for the size parameter. +ImageShow.Viewer.show_file file argument +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.1.0 + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by +``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. +``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest +``viewer.show_file(path="test.jpg")`` instead. + +Constants +~~~~~~~~~ + +.. deprecated:: 9.1.0 + +A number of constants have been deprecated and will be removed in Pillow 10.0.0 +(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.ORDERED`` ``Image.Dither.ORDERED`` +``Image.RASTERIZE`` ``Image.Dither.RASTERIZE`` +``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG`` +``Image.WEB`` ``Image.Palette.WEB`` +``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE`` +``Image.AFFINE`` ``Image.Transform.AFFINE`` +``Image.EXTENT`` ``Image.Transform.EXTENT`` +``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE`` +``Image.QUAD`` ``Image.Transform.QUAD`` +``Image.MESH`` ``Image.Transform.MESH`` +``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT`` +``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM`` +``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90`` +``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180`` +``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270`` +``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE`` +``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE`` +``Image.BOX`` ``Image.Resampling.BOX`` +``Image.BILINEAR`` ``Image.Resampling.BILNEAR`` +``Image.LINEAR`` ``Image.Resampling.BILNEAR`` +``Image.HAMMING`` ``Image.Resampling.HAMMING`` +``Image.BICUBIC`` ``Image.Resampling.BICUBIC`` +``Image.CUBIC`` ``Image.Resampling.BICUBIC`` +``Image.LANCZOS`` ``Image.Resampling.LANCZOS`` +``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS`` +``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT`` +``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE`` +``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE`` +``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + +FitsStubImagePlugin +~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.1.0 + +The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in +Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through +:mod:`~PIL.FitsImagePlugin` instead. + +PhotoImage.paste box parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 + +The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01). + Removed features ---------------- Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +PILLOW_VERSION constant +~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.2.0 +.. versionremoved:: 9.0.0 + +Use ``__version__`` instead. + +It was initially removed in Pillow 7.0.0, but temporarily brought back in 7.1.0 +to give projects more time to upgrade. + +Image.show command parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.2.0 +.. versionremoved:: 9.0.0 + +The ``command`` parameter has been removed. Use a subclass of +:py:class:`.ImageShow.Viewer` instead. + +Image._showxv +~~~~~~~~~~~~~ + +.. deprecated:: 7.2.0 +.. versionremoved:: 9.0.0 + +Use :py:meth:`.Image.Image.show` instead. If custom behaviour is required, use +:py:func:`.ImageShow.register` to add a custom :py:class:`.ImageShow.Viewer` class. + +ImageFile.raise_ioerror +~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.2.0 +.. versionremoved:: 9.0.0 + +``IOError`` was merged into ``OSError`` in Python 3.3. +So, ``ImageFile.raise_ioerror`` has been removed. +Use ``ImageFile.raise_oserror`` instead. + +FreeType 2.7 +~~~~~~~~~~~~ + +.. deprecated:: 8.1.0 +.. versionremoved:: 9.0.0 + +Support for FreeType 2.7 has been removed. + +We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). + +.. _FreeType: https://www.freetype.org + im.offset ~~~~~~~~~ diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 78aa3ce72..ec3938b36 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -210,7 +210,9 @@ class DdsImageFile(ImageFile.ImageFile): format_description = "DirectDraw Surface" def _open(self): - magic, header_size = struct.unpack("= 2.1.0 no longer automatically imports any file in the Python path with a name ending in @@ -87,10 +86,13 @@ true color. Image.register_open(SpamImageFile.format, SpamImageFile, _accept) - Image.register_extensions(SpamImageFile.format, [ - ".spam", - ".spa", # DOS version - ]) + Image.register_extensions( + SpamImageFile.format, + [ + ".spam", + ".spa", # DOS version + ], + ) The format handler must always set the @@ -111,6 +113,7 @@ Once the plugin has been imported, it can be used: from PIL import Image import SpamImagePlugin + with Image.open("hopper.spam") as im: pass @@ -120,8 +123,12 @@ The ``tile`` attribute To be able to read the file as well as just identifying it, the ``tile`` attribute must also be set. This attribute consists of a list of tile descriptors, where each descriptor specifies how data should be loaded to a -given region in the image. In most cases, only a single descriptor is used, -covering the full image. +given region in the image. + +In most cases, only a single descriptor is used, covering the full image. +:py:class:`.PsdImagePlugin.PsdImageFile` uses multiple tiles to combine +channels within a single layer, given that the channels are stored separately, +one after the other. The tile descriptor is a 4-tuple with the following contents:: @@ -163,16 +170,16 @@ TIFF, and many others. To use the raw decoder with the image = Image.frombytes( mode, size, data, "raw", - raw mode, stride, orientation + raw_mode, stride, orientation ) When used in a tile descriptor, the parameter field should look like:: - (raw mode, stride, orientation) + (raw_mode, stride, orientation) The fields are used as follows: -**raw mode** +**raw_mode** The pixel layout used in the file, and is used to properly convert data to PIL’s internal layout. For a summary of the available formats, see the table below. @@ -321,42 +328,42 @@ The fields are used as follows: Whether the first line in the image is the top line on the screen (1), or the bottom line (-1). If omitted, the orientation defaults to 1. -.. _file-decoders: +.. _file-codecs: -Writing Your Own File Decoder in C -================================== +Writing Your Own File Codec in C +================================ -There are 3 stages in a file decoder's lifetime: +There are 3 stages in a file codec's lifetime: -1. Setup: Pillow looks for a function in the decoder registry, falling - back to a function named ``[decodername]_decoder`` on the internal - core image object. That function is called with the ``args`` tuple - from the ``tile`` setup in the ``_open`` method. +1. Setup: Pillow looks for a function in the decoder or encoder registry, + falling back to a function named ``[codecname]_decoder`` or + ``[codecname]_encoder`` on the internal core image object. That function is + called with the ``args`` tuple from the ``tile``. -2. Decoding: The decoder's decode function is repeatedly called with - chunks of image data. +2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly + called with chunks of image data. -3. Cleanup: If the decoder has registered a cleanup function, it will - be called at the end of the decoding process, even if there was an +3. Cleanup: If the codec has registered a cleanup function, it will + be called at the end of the transformation process, even if there was an exception raised. Setup ----- -The current conventions are that the decoder setup function is named -``PyImaging_[Decodername]DecoderNew`` and defined in ``decode.c``. The -python binding for it is named ``[decodername]_decoder`` and is setup -from within the ``_imaging.c`` file in the codecs section of the -function array. +The current conventions are that the codec setup function is named +``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew`` +and defined in ``decode.c`` or ``encode.c``. The Python binding for it is +named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from +within the ``_imaging.c`` file in the codecs section of the function array. -The setup function needs to call ``PyImaging_DecoderNew`` and at the -very least, set the ``decode`` function pointer. The fields of -interest in this object are: +The setup function needs to call ``PyImaging_DecoderNew`` or +``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or +``encode`` function pointer. The fields of interest in this object are: -**decode** - Function pointer to the decode function, which has access to - ``im``, ``state``, and the buffer of data to be added to the image. +**decode**/**encode** + Function pointer to the decode or encode function, which has access to + ``im``, ``state``, and the buffer of data to be transformed. **cleanup** Function pointer to the cleanup function, has access to ``state``. @@ -366,36 +373,34 @@ interest in this object are: **state** An ImagingCodecStateInstance, will be set by Pillow. The ``context`` - member is an opaque struct that can be used by the decoder to store + member is an opaque struct that can be used by the codec to store any format specific state or options. -**pulls_fd** - **EXPERIMENTAL** -- **WARNING**, interface may change. If set to 1, - ``state->fd`` will be a pointer to the Python file like object. The - decoder may use the functions in ``codec_fd.c`` to read directly - from the file like object rather than have the data pushed through a - buffer. Note that this implementation may be refactored until this - warning is removed. +**pulls_fd**/**pushes_fd** + If the decoder has ``pulls_fd`` or the encoder has ``pushes_fd`` set to 1, + ``state->fd`` will be a pointer to the Python file like object. The codec may + use the functions in ``codec_fd.c`` to read or write directly with the file + like object rather than have the data pushed through a buffer. .. versionadded:: 3.3.0 -Decoding --------- +Transforming +------------ -The decode function is called with the target (core) image, the -decoder state structure, and a buffer of data to be decoded. +The decode or encode function is called with the target (core) image, the codec +state structure, and a buffer of data to be transformed. -**Experimental** -- If ``pulls_fd`` is set, then the decode function -is called once, with an empty buffer. It is the decoder's -responsibility to decode the entire tile in that one call. The rest of -this section only applies if ``pulls_fd`` is not set. +It is the codec's responsibility to pull as much data as possible out of the +buffer and return the number of bytes consumed. The next call to the codec will +include the previous unconsumed tail. The codec function will be called +multiple times as the data processed. -It is the decoder's responsibility to pull as much data as possible -out of the buffer and return the number of bytes consumed. The next -call to the decoder will include the previous unconsumed tail. The -decoder function will be called multiple times as the data is read -from the file like object. +Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or +encode function is called once, with an empty buffer. It is the codec's +responsibility to transform the entire tile in that one call. Using this will +provide a codec with more freedom, but that freedom may mean increased memory +usage if the entire tile is held in memory at once by the codec. If an error occurs, set ``state->errcode`` and return -1. @@ -404,28 +409,49 @@ Return -1 on success, without setting the errcode. Cleanup ------- -The cleanup function is called after the decoder returns a negative -value, or if there is a read error from the file. This function should -free any allocated memory and release any resources from external -libraries. +The cleanup function is called after the codec returns a negative +value, or if there is an error. This function should free any allocated +memory and release any resources from external libraries. -.. _file-decoders-py: +.. _file-codecs-py: -Writing Your Own File Decoder in Python -======================================= +Writing Your Own File Codec in Python +===================================== -Python file decoders should derive from -:py:class:`PIL.ImageFile.PyDecoder` and should at least override the -decode method. File decoders should be registered using -:py:meth:`PIL.Image.register_decoder`. As in the C implementation of -the file decoders, there are three stages in the lifetime of a -Python-based file decoder: +Python file decoders and encoders should derive from +:py:class:`PIL.ImageFile.PyDecoder` and :py:class:`PIL.ImageFile.PyEncoder` +respectively, and should at least override the decode or encode method. +They should be registered using :py:meth:`PIL.Image.register_decoder` and +:py:meth:`PIL.Image.register_encoder`. As in the C implementation of +the file codecs, there are three stages in the lifetime of a +Python-based file codec: -1. Setup: Pillow looks for the decoder in the registry, then +1. Setup: Pillow looks for the codec in the decoder or encoder registry, then instantiates the class. -2. Decoding: The decoder instance's ``decode`` method is repeatedly - called with a buffer of data to be interpreted. +2. Transforming: The instance's ``decode`` method is repeatedly called with + a buffer of data to be interpreted, or the ``encode`` method is repeatedly + called with the size of data to be output. -3. Cleanup: The decoder instance's ``cleanup`` method is called. + Alternatively, if the decoder's ``_pulls_fd`` property (or the encoder's + ``_pushes_fd`` property) is set to ``True``, then ``decode`` and ``encode`` + will only be called once. In the decoder, ``self.fd`` can be used to access + the file-like object. Using this will provide a codec with more freedom, but + that freedom may mean increased memory usage if entire file is held in + memory at once by the codec. + In ``decode``, once the data has been interpreted, ``set_as_raw`` can be + used to populate the image. + +3. Cleanup: The instance's ``cleanup`` method is called once the transformation + is complete. This can be used to clean up any resources used by the codec. + + If you set ``_pulls_fd`` or ``_pushes_fd`` to ``True`` however, then you + probably chose to perform any cleanup tasks at the end of ``decode`` or + ``encode``. + +For an example :py:class:`PIL.ImageFile.PyDecoder`, see `DdsImagePlugin +`_. +For a plugin that uses both :py:class:`PIL.ImageFile.PyDecoder` and +:py:class:`PIL.ImageFile.PyEncoder`, see `BlpImagePlugin +`_ diff --git a/docs/index.rst b/docs/index.rst index 3348feb89..f1a721c6a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,22 +10,26 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more = 8.4 | Yes | Yes | Yes | Yes | Yes | | | | +| Pillow >= 9.0 | Yes | Yes | Yes | Yes | | | | | +----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ -| Pillow 8.0 - 8.3 | | Yes | Yes | Yes | Yes | | | | +| Pillow 8.3.2 - 8.4 | Yes | Yes | Yes | Yes | Yes | | | | ++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow 8.0 - 8.3.1 | | Yes | Yes | Yes | Yes | | | | +----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ | Pillow 7.0 - 7.2 | | | Yes | Yes | Yes | Yes | | | +----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ @@ -167,7 +169,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.12**. + above uses liblcms2. Tested with **1.19** and **2.7-2.13.1**. * **libwebp** provides the WebP format. @@ -185,7 +187,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-2.15.1** + * Pillow has been tested with libimagequant **2.6-4.0** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. @@ -213,7 +215,7 @@ Many of Pillow's features require external libraries: Once you have installed the prerequisites, run:: python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade Pillow --no-binary :all: If the prerequisites are installed in the standard library locations for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no @@ -223,7 +225,7 @@ those locations by editing :file:`setup.py` or :file:`setup.cfg`, or by adding environment variables on the command line:: - CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow + CFLAGS="-I/usr/pkg/include" python3 -m pip install --upgrade Pillow --no-binary :all: If Pillow has been previously built without the required prerequisites, it may be necessary to manually clear the pip cache or @@ -273,10 +275,6 @@ Build Options Sample usage:: - MAX_CONCURRENCY=1 python3 setup.py build_ext --enable-[feature] install - -or using pip:: - python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" @@ -293,7 +291,7 @@ tools. The easiest way to install external libraries is via `Homebrew `_. After you install Homebrew, run:: - brew install libtiff libjpeg webp little-cms2 + brew install libjpeg libtiff little-cms2 openjpeg webp To install libraqm on macOS use Homebrew to install its dependencies:: @@ -304,11 +302,11 @@ Then see ``depends/install_raqm_cmake.sh`` to install libraqm. Now install Pillow with:: python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade Pillow --no-binary :all: or from within the uncompressed source directory:: - python3 setup.py install + python3 -m pip install . Building on Windows ^^^^^^^^^^^^^^^^^^^ @@ -351,7 +349,7 @@ Prerequisites are installed on **MSYS2 MinGW 64-bit** with:: Now install Pillow with:: python3 -m pip install --upgrade pip - python3 -m pip install --upgrade Pillow + python3 -m pip install --upgrade Pillow --no-binary :all: Building on FreeBSD @@ -396,7 +394,8 @@ Prerequisites for **Ubuntu 16.04 LTS - 20.04 LTS** are installed with:: libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python3-tk \ libharfbuzz-dev libfribidi-dev libxcb1-dev -Then see ``depends/install_raqm.sh`` to install libraqm. +To install libraqm, ``sudo apt-get install meson`` and then see +``depends/install_raqm.sh``. Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: @@ -443,41 +442,44 @@ Continuous Integration Targets These platforms are built and tested for every change. -+----------------------------------+---------------------------+---------------------+ -| Operating system | Tested Python versions | Tested architecture | -+==================================+===========================+=====================+ -| Alpine | 3.8 | x86-64 | -+----------------------------------+---------------------------+---------------------+ -| Arch | 3.8 | x86-64 | -+----------------------------------+---------------------------+---------------------+ -| Amazon Linux 2 | 3.7 | x86-64 | -+----------------------------------+---------------------------+---------------------+ -| CentOS 7 | 3.6 | x86-64 | -+----------------------------------+---------------------------+---------------------+ -| CentOS 8 | 3.6 | x86-64 | -+----------------------------------+---------------------------+---------------------+ -| Debian 10 Buster | 3.7 | x86 | -+----------------------------------+---------------------------+---------------------+ -| Fedora 33 | 3.9 | x86-64 | -+----------------------------------+---------------------------+---------------------+ -| Fedora 34 | 3.9 | x86-64 | -+----------------------------------+---------------------------+---------------------+ -| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9, PyPy3 | x86-64 | -+----------------------------------+---------------------------+---------------------+ -| Ubuntu Linux 16.04 LTS (Xenial) | 3.6, 3.7, 3.8, 3.9, PyPy3 | x86-64 | -+----------------------------------+---------------------------+---------------------+ -| Ubuntu Linux 18.04 LTS (Bionic) | 3.6, 3.7, 3.8, 3.9, PyPy3 | x86-64 | -+----------------------------------+---------------------------+---------------------+ -| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | -+----------------------------------+---------------------------+---------------------+ -| Windows Server 2016 | 3.6 | x86-64 | -+----------------------------------+---------------------------+---------------------+ -| Windows Server 2019 | 3.6, 3.7, 3.8, 3.9 | x86, x86-64 | -| +---------------------------+---------------------+ -| | PyPy3 | x86 | -| +---------------------------+---------------------+ -| | 3.9/MinGW | x86, x86-64 | -+----------------------------------+---------------------------+---------------------+ ++----------------------------------+----------------------------+---------------------+ +| Operating system | Tested Python versions | Tested architecture | ++==================================+============================+=====================+ +| Alpine | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Amazon Linux 2 | 3.7 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Arch | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS 7 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS Stream 8 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS Stream 9 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Debian 10 Buster | 3.7 | x86 | ++----------------------------------+----------------------------+---------------------+ +| Debian 11 Bullseye | 3.9 | x86 | ++----------------------------------+----------------------------+---------------------+ +| Fedora 35 | 3.10 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Gentoo | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| macOS 10.15 Catalina | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 | +| +----------------------------+---------------------+ +| | 3.8 | arm64v8, ppc64le, | +| | | s390x | ++----------------------------------+----------------------------+---------------------+ +| Windows Server 2016 | 3.7 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Windows Server 2019 | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86, x86-64 | +| +----------------------------+---------------------+ +| | 3.9/MinGW | x86, x86-64 | ++----------------------------------+----------------------------+---------------------+ Other Platforms @@ -494,11 +496,15 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 11.0 Big Sur | 3.7, 3.8, 3.9 | 8.3.1 |arm | -| +---------------------------+------------------+--------------+ -| | 3.6, 3.7, 3.8, 3.9 | 8.3.1 |x86-64 | +| macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10 | 9.0.1 |arm | +----------------------------------+---------------------------+------------------+--------------+ -| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.1 |x86-64 | +| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10 | 8.4.0 |arm | +| +---------------------------+------------------+--------------+ +| | 3.7, 3.8, 3.9, 3.10 | 9.0.1 |x86-64 | +| +---------------------------+------------------+--------------+ +| | 3.6 | 8.4.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.2 |x86-64 | | +---------------------------+------------------+ | | | 3.5 | 7.2.0 | | +----------------------------------+---------------------------+------------------+--------------+ @@ -524,6 +530,8 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+---------------------------+------------------+--------------+ | CentOS 6.3 | 2.7, 3.3 | |x86 | +----------------------------------+---------------------------+------------------+--------------+ +| CentOS 8 | 3.9 | 9.0.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ | Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | +----------------------------------+---------------------------+------------------+--------------+ | Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index c80b28a98..2613b6585 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -254,7 +254,8 @@ This rotates the input image by ``theta`` degrees counter clockwise: .. automethod:: PIL.Image.Image.transform .. automethod:: PIL.Image.Image.transpose -This flips the input image by using the :data:`FLIP_LEFT_RIGHT` method. +This flips the input image by using the :data:`PIL.Image.Transpose.FLIP_LEFT_RIGHT` +method. .. code-block:: python @@ -263,9 +264,9 @@ This flips the input image by using the :data:`FLIP_LEFT_RIGHT` method. with Image.open("hopper.jpg") as im: # Flip the image from left to right - im_flipped = im.transpose(method=Image.FLIP_LEFT_RIGHT) + im_flipped = im.transpose(method=Image.Transpose.FLIP_LEFT_RIGHT) # To flip the image from top to bottom, - # use the method "Image.FLIP_TOP_BOTTOM" + # use the method "Image.Transpose.FLIP_TOP_BOTTOM" .. automethod:: PIL.Image.Image.verify @@ -389,68 +390,57 @@ Transpose methods Used to specify the :meth:`Image.transpose` method to use. -.. data:: FLIP_LEFT_RIGHT -.. data:: FLIP_TOP_BOTTOM -.. data:: ROTATE_90 -.. data:: ROTATE_180 -.. data:: ROTATE_270 -.. data:: TRANSPOSE -.. data:: TRANSVERSE +.. autoclass:: Transpose + :members: + :undoc-members: Transform methods ^^^^^^^^^^^^^^^^^ Used to specify the :meth:`Image.transform` method to use. -.. data:: AFFINE +.. py:class:: Transform - Affine transform + .. py:attribute:: AFFINE -.. data:: EXTENT + Affine transform - Cut out a rectangular subregion + .. py:attribute:: EXTENT -.. data:: PERSPECTIVE + Cut out a rectangular subregion - Perspective transform + .. py:attribute:: PERSPECTIVE -.. data:: QUAD + Perspective transform - Map a quadrilateral to a rectangle + .. py:attribute:: QUAD -.. data:: MESH + Map a quadrilateral to a rectangle - Map a number of source quadrilaterals in one operation + .. py:attribute:: MESH + + Map a number of source quadrilaterals in one operation Resampling filters ^^^^^^^^^^^^^^^^^^ See :ref:`concept-filters` for details. -.. data:: NEAREST - :noindex: -.. data:: BOX - :noindex: -.. data:: BILINEAR - :noindex: -.. data:: HAMMING - :noindex: -.. data:: BICUBIC - :noindex: -.. data:: LANCZOS - :noindex: +.. autoclass:: Resampling + :members: + :undoc-members: -Some filters are also available under the following names for backwards compatibility: +Some deprecated filters are also available under the following names: .. data:: NONE :noindex: - :value: NEAREST + :value: Resampling.NEAREST .. data:: LINEAR - :value: BILINEAR + :value: Resampling.BILINEAR .. data:: CUBIC - :value: BICUBIC + :value: Resampling.BICUBIC .. data:: ANTIALIAS - :value: LANCZOS + :value: Resampling.LANCZOS Dither modes ^^^^^^^^^^^^ @@ -458,48 +448,56 @@ Dither modes Used to specify the dithering method to use for the :meth:`~Image.convert` and :meth:`~Image.quantize` methods. -.. data:: NONE - :noindex: +.. py:class:: Dither - No dither + .. py:attribute:: NONE -.. comment: (not implemented) - .. data:: ORDERED - .. data:: RASTERIZE + No dither -.. data:: FLOYDSTEINBERG + .. py:attribute:: ORDERED - Floyd-Steinberg dither + Not implemented + + .. py:attribute:: RASTERIZE + + Not implemented + + .. py:attribute:: FLOYDSTEINBERG + + Floyd-Steinberg dither Palettes ^^^^^^^^ Used to specify the pallete to use for the :meth:`~Image.convert` method. -.. data:: WEB -.. data:: ADAPTIVE +.. autoclass:: Palette + :members: + :undoc-members: Quantization methods ^^^^^^^^^^^^^^^^^^^^ Used to specify the quantization method to use for the :meth:`~Image.quantize` method. -.. data:: MEDIANCUT +.. py:class:: Quantize - Median cut. Default method, except for RGBA images. This method does not support - RGBA images. + .. py:attribute:: MEDIANCUT -.. data:: MAXCOVERAGE + Median cut. Default method, except for RGBA images. This method does not support + RGBA images. - Maximum coverage. This method does not support RGBA images. + .. py:attribute:: MAXCOVERAGE -.. data:: FASTOCTREE + Maximum coverage. This method does not support RGBA images. - Fast octree. Default method for RGBA images. + .. py:attribute:: FASTOCTREE -.. data:: LIBIMAGEQUANT + Fast octree. Default method for RGBA images. - libimagequant + .. py:attribute:: LIBIMAGEQUANT - Check support using :py:func:`PIL.features.check_feature` - with ``feature="libimagequant"``. + libimagequant + + Check support using :py:func:`PIL.features.check_feature` with + ``feature="libimagequant"``. diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index f938e63a0..9b9b5e7b2 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -118,8 +118,8 @@ can be easily displayed in a chromaticity diagram, for example). another profile (usually overridden at run-time, but provided here for DeviceLink and embedded source profiles, see 7.2.15 of ICC.1:2010). - One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, ``ImageCms.INTENT_PERCEPTUAL``, - ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and ``ImageCms.INTENT_SATURATION``. + One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and ``ImageCms.Intent.SATURATION``. .. py:attribute:: profile_id :type: bytes @@ -313,14 +313,14 @@ can be easily displayed in a chromaticity diagram, for example). the CLUT model. The dictionary is indexed by intents - (``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, - ``ImageCms.INTENT_PERCEPTUAL``, - ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and - ``ImageCms.INTENT_SATURATION``). + (``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, + ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and + ``ImageCms.Intent.SATURATION``). The values are 3-tuples indexed by directions - (``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``, - ``ImageCms.DIRECTION_PROOF``). + (``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``, + ``ImageCms.Direction.PROOF``). The elements of the tuple are booleans. If the value is ``True``, that intent is supported for that direction. @@ -331,14 +331,14 @@ can be easily displayed in a chromaticity diagram, for example). Returns a dictionary of all supported intents and directions. The dictionary is indexed by intents - (``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, - ``ImageCms.INTENT_PERCEPTUAL``, - ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and - ``ImageCms.INTENT_SATURATION``). + (``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, + ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` and + ``ImageCms.Intent.SATURATION``). The values are 3-tuples indexed by directions - (``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``, - ``ImageCms.DIRECTION_PROOF``). + (``ImageCms.Direction.INPUT``, ``ImageCms.Direction.OUTPUT``, + ``ImageCms.Direction.PROOF``). The elements of the tuple are booleans. If the value is ``True``, that intent is supported for that direction. @@ -352,11 +352,11 @@ can be easily displayed in a chromaticity diagram, for example). Note that you can also get this information for all intents and directions with :py:attr:`.intent_supported`. - :param intent: One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, - ``ImageCms.INTENT_PERCEPTUAL``, - ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` - and ``ImageCms.INTENT_SATURATION``. - :param direction: One of ``ImageCms.DIRECTION_INPUT``, - ``ImageCms.DIRECTION_OUTPUT`` - and ``ImageCms.DIRECTION_PROOF`` + :param intent: One of ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC``, + ``ImageCms.Intent.PERCEPTUAL``, + ``ImageCms.Intent.RELATIVE_COLORIMETRIC`` + and ``ImageCms.Intent.SATURATION``. + :param direction: One of ``ImageCms.Direction.INPUT``, + ``ImageCms.Direction.OUTPUT`` + and ``ImageCms.Direction.PROOF`` :return: Boolean if the intent and direction is supported. diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 46e1595c2..b95d8d591 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -81,11 +81,12 @@ Example: Draw Partial Opacity Text .. code-block:: python from PIL import Image, ImageDraw, ImageFont + # get an image with Image.open("Pillow/Tests/images/hopper.png").convert("RGBA") as base: # make a blank image for the text, initialized to transparent text color - txt = Image.new("RGBA", base.size, (255,255,255,0)) + txt = Image.new("RGBA", base.size, (255, 255, 255, 0)) # get a font fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40) @@ -93,9 +94,9 @@ Example: Draw Partial Opacity Text d = ImageDraw.Draw(txt) # draw text, half opacity - d.text((10,10), "Hello", font=fnt, fill=(255,255,255,128)) + d.text((10, 10), "Hello", font=fnt, fill=(255, 255, 255, 128)) # draw text, full opacity - d.text((10,60), "World", font=fnt, fill=(255,255,255,255)) + d.text((10, 60), "World", font=fnt, fill=(255, 255, 255, 255)) out = Image.alpha_composite(base, txt) @@ -117,7 +118,7 @@ Example: Draw Multiline Text d = ImageDraw.Draw(out) # draw multiline text - d.multiline_text((10,10), "Hello\nWorld", font=fnt, fill=(0, 0, 0)) + d.multiline_text((10, 10), "Hello\nWorld", font=fnt, fill=(0, 0, 0)) out.show() @@ -242,7 +243,7 @@ Methods numeric values like ``[x, y, x, y, ...]``. :param fill: Color to use for the point. -.. py:method:: ImageDraw.polygon(xy, fill=None, outline=None) +.. py:method:: ImageDraw.polygon(xy, fill=None, outline=None, width=1) Draws a polygon. @@ -252,8 +253,9 @@ Methods :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. - :param outline: Color to use for the outline. :param fill: Color to use for the fill. + :param outline: Color to use for the outline. + :param width: The line width, in pixels. .. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None) @@ -556,7 +558,9 @@ Methods .. code-block:: python - hello = draw.textlength("HelloW", font) - draw.textlength("W", font) # adjusted for kerning + hello = draw.textlength("HelloW", font) - draw.textlength( + "W", font + ) # adjusted for kerning world = draw.textlength("World", font) hello_world = hello + world # adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # True diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index e0ce389e8..3cf59c610 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -40,8 +40,16 @@ Classes .. autoclass:: PIL.ImageFile.Parser() :members: +.. autoclass:: PIL.ImageFile.PyCodec() + :members: + .. autoclass:: PIL.ImageFile.PyDecoder() :members: + :show-inheritance: + +.. autoclass:: PIL.ImageFile.PyEncoder() + :members: + :show-inheritance: .. autoclass:: PIL.ImageFile.ImageFile() :member-order: bysource diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 813d325e0..8efef7cfd 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -9,7 +9,7 @@ this class store bitmap fonts, and are used with the :py:meth:`PIL.ImageDraw.ImageDraw.text` method. PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use -`pilfont.py `_ +`pilfont.py `_ from `pillow-scripts `_ to convert BDF and PCF font descriptors (X window font formats) to this format. @@ -60,12 +60,12 @@ Methods Constants --------- -.. data:: PIL.ImageFont.LAYOUT_BASIC +.. data:: PIL.ImageFont.Layout.BASIC Use basic text layout for TrueType font. Advanced features such as text direction are not supported. -.. data:: PIL.ImageFont.LAYOUT_RAQM +.. data:: PIL.ImageFont.Layout.RAQM Use Raqm text layout for TrueType font. Advanced features are supported. diff --git a/docs/reference/ImagePalette.rst b/docs/reference/ImagePalette.rst index f14c1c3a4..72ccfac7d 100644 --- a/docs/reference/ImagePalette.rst +++ b/docs/reference/ImagePalette.rst @@ -9,10 +9,6 @@ represent the color palette of palette mapped images. .. note:: - This module was never well-documented. It hasn't changed since 2001, - though, so it's probably safe for you to read the source code and puzzle - out the internals if you need to. - The :py:class:`~PIL.ImagePalette.ImagePalette` class has several methods, but they are all marked as "experimental." Read that as you will. The ``[source]`` link is there for a reason. diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst index e4d9805ab..5cedede69 100644 --- a/docs/reference/ImageShow.rst +++ b/docs/reference/ImageShow.rst @@ -17,11 +17,15 @@ All default viewers convert the image to be shown to PNG format. The following viewers may be registered on Unix-based systems, if the given command is found: + .. autoclass:: PIL.ImageShow.XDGViewer .. autoclass:: PIL.ImageShow.DisplayViewer .. autoclass:: PIL.ImageShow.GmDisplayViewer .. autoclass:: PIL.ImageShow.EogViewer .. autoclass:: PIL.ImageShow.XVViewer + To provide maximum functionality on Unix-based systems, temporary files created + from images will not be automatically removed by Pillow. + .. autofunction:: PIL.ImageShow.register .. autoclass:: PIL.ImageShow.Viewer :member-order: bysource diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst index 5bb735296..f61d12313 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -14,6 +14,16 @@ for a region of an image. statistics. You can also pass in a previously calculated histogram. :param image: A PIL image, or a precalculated histogram. + + .. note:: + + For a PIL image, calculations rely on the + :py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are + grouped into 256 bins, even if the image has more than 8 bits per + channel. So ``I`` and ``F`` mode images have a maximum ``mean``, + ``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum + of more than 255. + :param mask: An optional mask. .. py:attribute:: extrema diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index f28e58f86..d2e80fb8c 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -6,7 +6,13 @@ The PixelAccess class provides read and write access to :py:class:`PIL.Image` data at a pixel level. -.. note:: Accessing individual pixels is fairly slow. If you are looping over all of the pixels in an image, there is likely a faster way using other parts of the Pillow API. +.. note:: Accessing individual pixels is fairly slow. If you are + looping over all of the pixels in an image, there is likely + a faster way using other parts of the Pillow API. + + :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps` + have methods for many standard operations. If you wish to perform + a custom mapping, check out :py:meth:`~PIL.Image.Image.point`. Example ------- @@ -17,11 +23,12 @@ changes it. .. code-block:: python from PIL import Image - with Image.open('hopper.jpg') as im: + + with Image.open("hopper.jpg") as im: px = im.load() - print (px[4,4]) - px[4,4] = (0,0,0) - print (px[4,4]) + print(px[4, 4]) + px[4, 4] = (0, 0, 0) + print(px[4, 4]) Results in the following:: @@ -32,13 +39,13 @@ Access using negative indexes is also possible. .. code-block:: python - px[-1,-1] = (0,0,0) - print (px[-1,-1]) + px[-1, -1] = (0, 0, 0) + print(px[-1, -1]) :py:class:`PixelAccess` Class ------------------------------------ +----------------------------- .. class:: PixelAccess diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index 486c9fc21..f9eb9b524 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -7,8 +7,12 @@ The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version. .. note:: Accessing individual pixels is fairly slow. If you are - looping over all of the pixels in an image, there is likely - a faster way using other parts of the Pillow API. + looping over all of the pixels in an image, there is likely + a faster way using other parts of the Pillow API. + + :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps` + have methods for many standard operations. If you wish to perform + a custom mapping, check out :py:meth:`~PIL.Image.Image.point`. Example ------- @@ -18,11 +22,12 @@ The following script loads an image, accesses one pixel from it, then changes it .. code-block:: python from PIL import Image - with Image.open('hopper.jpg') as im: + + with Image.open("hopper.jpg") as im: px = im.load() - print (px[4,4]) - px[4,4] = (0,0,0) - print (px[4,4]) + print(px[4, 4]) + px[4, 4] = (0, 0, 0) + print(px[4, 4]) Results in the following:: @@ -33,8 +38,8 @@ Access using negative indexes is also possible. .. code-block:: python - px[-1,-1] = (0,0,0) - print (px[-1,-1]) + px[-1, -1] = (0, 0, 0) + print(px[-1, -1]) diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index 66175ea0c..dc4c2bf94 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -53,7 +53,7 @@ Then ``sudo apt-get update && sudo apt-get install libtiff5-dbgsym`` virtualenv -p python3.8-dbg ~/vpy38-dbg source ~/vpy38-dbg/bin/activate - cd ~/Pillow && pip install -r requirements.txt && make install + cd ~/Pillow && make install Test Case --------- @@ -63,6 +63,7 @@ Take your test image, and make a really simple harness. :: from PIL import Image + with Image.open(path) as im: im.load() diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 0a6381098..c66193061 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -57,7 +57,7 @@ Support for the following features can be checked: * ``transp_webp``: Support for transparency in WebP images. * ``webp_mux``: (compile time) Support for EXIF data in WebP images. * ``webp_anim``: (compile time) Support for animated WebP images. -* ``raqm``: Raqm library, required for ``ImageFont.LAYOUT_RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. +* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 1105ff76e..363a67d9b 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -9,6 +9,14 @@ Internal Modules :undoc-members: :show-inheritance: +:mod:`~PIL._deprecate` Module +----------------------------- + +.. automodule:: PIL._deprecate + :members: + :undoc-members: + :show-inheritance: + :mod:`~PIL._tkinter_finder` Module ---------------------------------- diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index ed0ab1a0c..6bfd50588 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -14,17 +14,17 @@ The following are all equivalent:: import io import pathlib - with Image.open('test.jpg') as im: + with Image.open("test.jpg") as im: ... - with Image.open(pathlib.Path('test.jpg')) as im2: + with Image.open(pathlib.Path("test.jpg")) as im2: ... - with open('test.jpg', 'rb') as f: + with open("test.jpg", "rb") as f: im3 = Image.open(f) ... - with open('test.jpg', 'rb') as f: + with open("test.jpg", "rb") as f: im4 = Image.open(io.BytesIO(f.read())) ... @@ -47,6 +47,10 @@ Image Lifecycle memory. The image can now be used independently of the underlying image file. + Any Pillow method that creates a new image instance based on another will + internally call ``load()`` on the original image and then read the data. + The new image instance will not be associated with the original image file. + If a filename or a ``Path`` object was passed to ``Image.open()``, then the file object was opened by Pillow and is considered to be used exclusively by Pillow. So if the image is a single-frame image, the file will be closed in @@ -55,10 +59,16 @@ Image Lifecycle ``Image.Image.seek()`` can load the appropriate frame. * ``Image.Image.close()`` Closes the file and destroys the core image object. - This is used in the Pillow context manager support. e.g.:: - with Image.open('test.jpg') as img: - ... # image operations here. + The Pillow context manager will also close the file, but will not destroy + the core image object. e.g.: + +.. code-block:: python + + with Image.open("test.jpg") as img: + img.load() + assert img.fp is None + img.save("test.png") The lifecycle of a single-frame image is relatively simple. The file must @@ -80,13 +90,13 @@ Complications * After a file has been closed, operations that require file access will fail:: - with open('test.jpg', 'rb') as f: + with open("test.jpg", "rb") as f: im5 = Image.open(f) - im5.load() # FAILS, closed file + im5.load() # FAILS, closed file - with Image.open('test.jpg') as im6: + with Image.open("test.jpg") as im6: pass - im6.load() # FAILS, closed file + im6.load() # FAILS, closed file Proposed File Handling diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 7094f8784..fcf4514a8 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -41,10 +41,10 @@ Plugin reference :undoc-members: :show-inheritance: -:mod:`~PIL.FitsStubImagePlugin` Module +:mod:`~PIL.FitsImagePlugin` Module -------------------------------------- -.. automodule:: PIL.FitsStubImagePlugin +.. automodule:: PIL.FitsImagePlugin :members: :undoc-members: :show-inheritance: @@ -230,8 +230,7 @@ Plugin reference .. automodule:: PIL.PngImagePlugin :members: ChunkStream, PngImageFile, PngStream, getchunks, is_cid, putchunk, - MAX_TEXT_CHUNK, MAX_TEXT_MEMORY, APNG_BLEND_OP_SOURCE, APNG_BLEND_OP_OVER, - APNG_DISPOSE_OP_NONE, APNG_DISPOSE_OP_BACKGROUND, APNG_DISPOSE_OP_PREVIOUS + Blend, Disposal, MAX_TEXT_CHUNK, MAX_TEXT_MEMORY :undoc-members: :show-inheritance: :member-order: groupwise diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index 03000528f..dda814c1f 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -14,7 +14,7 @@ Png text chunk size limits To prevent potential denial of service attacks using compressed text chunks, there are now limits to the decompressed size of text chunks decoded from PNG images. If the limits are exceeded when opening a PNG -image a ``ValueError`` will be raised. +image a :py:exc:`ValueError` will be raised. Individual text chunks are limited to :py:attr:`PIL.PngImagePlugin.MAX_TEXT_CHUNK`, set to 1MB by @@ -111,16 +111,14 @@ downscaling with libjpeg, which uses supersampling internally, not convolutions. Image transposition ------------------- -A new method :py:data:`PIL.Image.TRANSPOSE` has been added for the +A new method ``TRANSPOSE`` has been added for the :py:meth:`~PIL.Image.Image.transpose` operation in addition to -:py:data:`~PIL.Image.FLIP_LEFT_RIGHT`, :py:data:`~PIL.Image.FLIP_TOP_BOTTOM`, -:py:data:`~PIL.Image.ROTATE_90`, :py:data:`~PIL.Image.ROTATE_180`, -:py:data:`~PIL.Image.ROTATE_270`. :py:data:`~PIL.Image.TRANSPOSE` is an algebra -transpose, with an image reflected across its main diagonal. +``FLIP_LEFT_RIGHT``, ``FLIP_TOP_BOTTOM``, ``ROTATE_90``, ``ROTATE_180``, +``ROTATE_270``. ``TRANSPOSE`` is an algebra transpose, with an image reflected +across its main diagonal. -The speed of :py:data:`~PIL.Image.ROTATE_90`, :py:data:`~PIL.Image.ROTATE_270` -and :py:data:`~PIL.Image.TRANSPOSE` has been significantly improved for large -images which don't fit in the processor cache. +The speed of ``ROTATE_90``, ``ROTATE_270`` and ``TRANSPOSE`` has been significantly +improved for large images which don't fit in the processor cache. Gaussian blur and unsharp mask ------------------------------ diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index 912af3ad2..c902ccf71 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -7,7 +7,7 @@ Deprecations Categories ^^^^^^^^^^ -``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), +``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. @@ -17,7 +17,7 @@ To determine if an image has multiple frames or not, Tk/Tcl 8.4 ^^^^^^^^^^ -Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), +Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), when Tk/Tcl 8.5 will be the minimum supported. API Changes diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst index eb4883deb..0bfead144 100644 --- a/docs/releasenotes/8.3.0.rst +++ b/docs/releasenotes/8.3.0.rst @@ -10,7 +10,7 @@ JpegImagePlugin.convert_dict_qtables JPEG ``quantization`` is now automatically converted, but still returned as a dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer performs any operations on the data given to it, has been deprecated and will be -removed in Pillow 10.0.0 (2023-01-02). +removed in Pillow 10.0.0 (2023-07-01). API Changes =========== diff --git a/docs/releasenotes/8.3.2.rst b/docs/releasenotes/8.3.2.rst new file mode 100644 index 000000000..6b5c759fc --- /dev/null +++ b/docs/releasenotes/8.3.2.rst @@ -0,0 +1,41 @@ +8.3.2 +----- + +Security +======== + +* :cve:`CVE-2021-23437`: Avoid a potential ReDoS (regular expression denial of service) + in :py:class:`~PIL.ImageColor`'s :py:meth:`~PIL.ImageColor.getrgb` by raising + :py:exc:`ValueError` if the color specifier is too long. Present since Pillow 5.2.0. + +* Fix 6-byte out-of-bounds (OOB) read. The previous bounds check in ``FliDecode.c`` + incorrectly calculated the required read buffer size when copying a chunk, potentially + reading six extra bytes off the end of the allocated buffer from the heap. Present + since Pillow 7.1.0. This bug was found by Google's `OSS-Fuzz`_ `CIFuzz`_ runs. + +Other Changes +============= + +Python 3.10 wheels +^^^^^^^^^^^^^^^^^^ + +Pillow now includes binary wheels for Python 3.10. + +The Python 3.10 release candidate was released on 2021-08-03 with the final release due +2021-10-04 (:pep:`619`). The CPython core team strongly encourages maintainers of +third-party Python projects to prepare for 3.10 compatibility. And as there are `no ABI +changes`_ planned we are releasing wheels to help others prepare for 3.10, and ensure +Pillow can be used immediately on release day of 3.10.0 final. + +Fixed regressions +^^^^^^^^^^^^^^^^^ + +* Ensure TIFF ``RowsPerStrip`` is multiple of 8 for JPEG compression (:pr:`5588`). + +* Updates for :py:class:`~PIL.ImagePalette` channel order (:pr:`5599`). + +* Hide FriBiDi shim symbols to avoid conflict with real FriBiDi library (:pr:`5651`). + +.. _OSS-Fuzz: https://github.com/google/oss-fuzz +.. _CIFuzz: https://google.github.io/oss-fuzz/getting-started/continuous-integration/ +.. _no ABI changes: https://www.python.org/downloads/release/python-3100rc1/ diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst index e23a1eefe..9becf9146 100644 --- a/docs/releasenotes/8.4.0.rst +++ b/docs/releasenotes/8.4.0.rst @@ -10,7 +10,7 @@ Deprecations ImagePalette size parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``size`` parameter will be removed in Pillow 10.0.0 (2023-01-02). +The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01). Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by default, and the size parameter could be used to override that. Pillow 8.3.0 removed @@ -38,14 +38,6 @@ Added WalImageFile class :py:class:`PIL.Image.Image` instance. It now returns a dedicated :py:class:`PIL.WalImageFile.WalImageFile` class. -Security -======== - -TODO -^^^^ - -TODO - Other Changes ============= diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst new file mode 100644 index 000000000..947ccd849 --- /dev/null +++ b/docs/releasenotes/9.0.0.rst @@ -0,0 +1,171 @@ +9.0.0 +----- + +Fredrik Lundh +============= + +This release is dedicated to the memory of Fredrik Lundh, aka Effbot, who died in +November 2021. Fredrik created PIL in 1995 and he was instrumental in the early +success of Python. + +`Guido wrote `_: + + Fredrik was an early Python contributor (e.g. Elementtree and the 're' + module) and his enthusiasm for the language and community were inspiring + for all who encountered him or his work. He spent countless hours on + comp.lang.python answering questions from newbies and advanced users alike. + + He also co-founded an early Python startup, Secret Labs AB, which among + other software released an IDE named PythonWorks. Fredrik also created the + Python Imaging Library (PIL) which is still THE way to interact with images + in Python, now most often through its Pillow fork. His effbot.org site was + a valuable resource for generations of Python users, especially its Tkinter + documentation. + +Thank you, Fredrik. + +Backwards Incompatible Changes +============================== + +Python 3.6 +^^^^^^^^^^ + +Pillow has dropped support for Python 3.6, which reached end-of-life on 2021-12-23. + +PILLOW_VERSION constant +^^^^^^^^^^^^^^^^^^^^^^^ + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + +FreeType 2.7 +^^^^^^^^^^^^ + +Support for FreeType 2.7 has been removed; FreeType 2.8 is the minimum supported. + +We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe +vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). + +.. _FreeType: https://www.freetype.org + +Image.show command parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``command`` parameter has been removed. Use a subclass of +:py:class:`PIL.ImageShow.Viewer` instead. + +Image._showxv +^^^^^^^^^^^^^ + +``Image._showxv`` has been removed. Use :py:meth:`~PIL.Image.Image.show` +instead. If custom behaviour is required, use :py:meth:`~PIL.ImageShow.register` to add +a custom :py:class:`~PIL.ImageShow.Viewer` class. + +ImageFile.raise_ioerror +^^^^^^^^^^^^^^^^^^^^^^^ + +``IOError`` was merged into ``OSError`` in Python 3.3. So, ``ImageFile.raise_ioerror`` +has been removed. Use ``ImageFile.raise_oserror`` instead. + + +API Changes +=========== + +Added line width parameter to ImageDraw polygon +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An optional line ``width`` parameter has been added to ``ImageDraw.Draw.polygon``. + + +API Additions +============= + +ImageShow.XDGViewer +^^^^^^^^^^^^^^^^^^^ + +If ``xdg-open`` is present on Linux, this new :py:class:`PIL.ImageShow.Viewer` subclass +will be registered. It displays images using the application selected by the system. + +It is higher in priority than the other default :py:class:`PIL.ImageShow.Viewer` +instances, so it will be preferred by ``im.show()`` or :py:func:`.ImageShow.show()`. + +Added support for "title" argument to DisplayViewer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for the "title" argument in +:py:class:`~PIL.ImageShow.UnixViewer.DisplayViewer`, so that when ``im.show()`` or +:py:func:`.ImageShow.show()` use the ``display`` command line tool, the "title" +argument will also now be supported, e.g. ``im.show(title="My Image")`` and +``ImageShow.show(im, title="My Image")``. + +Security +======== + +Ensure JpegImagePlugin stops at the end of a truncated file +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``JpegImagePlugin`` may append an EOF marker to the end of a truncated file, so that +the last segment of the data will still be processed by the decoder. + +If the EOF marker is not detected as such however, this could lead to an infinite +loop where ``JpegImagePlugin`` keeps trying to end the file. + +Remove consecutive duplicate tiles that only differ by their offset +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To prevent attempts to slow down loading times for images, if an image has consecutive +duplicate tiles that only differ by their offset, only load the last tile. Credit to +Google's `OSS-Fuzz`_ project for finding this issue. + +Restrict builtins available to ImageMath.eval +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cve:`CVE-2022-22817`: To limit :py:class:`PIL.ImageMath` to working with images, Pillow +will now restrict the builtins available to :py:meth:`PIL.ImageMath.eval`. This will +help prevent problems arising if users evaluate arbitrary expressions, such as +``ImageMath.eval("exec(exit())")``. + +Fixed ImagePath.Path array handling +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cve:`CVE-2022-22815` (:cwe:`CWE-126`) and :cve:`CVE-2022-22816` (:cwe:`CWE-665`) were +found when initializing ``ImagePath.Path``. + +.. _OSS-Fuzz: https://github.com/google/oss-fuzz + +Other Changes +============= + +Convert subsequent GIF frames to RGB or RGBA +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since each frame of a GIF can have up to 256 colors, after the first frame it is +possible for there to be too many colors to fit in a P mode image. To allow for this, +seeking to any subsequent GIF frame will now convert the image to RGB or RGBA, +depending on whether or not the first frame had transparency. + +Switched to libjpeg-turbo in macOS and Linux wheels +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Pillow wheels from PyPI for macOS and Linux have switched from libjpeg to +libjpeg-turbo. It is a fork of libjpeg, popular for its speed. + +Added support for pickling TrueType fonts +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TrueType fonts may now be pickled and unpickled. For example: + +.. code-block:: python + + import pickle + from PIL import ImageFont + + font = ImageFont.truetype("arial.ttf", size=30) + pickled_font = pickle.dumps(font, protocol=pickle.HIGHEST_PROTOCOL) + + # Later... + unpickled_font = pickle.loads(pickled_font) + +Added support for additional TGA orientations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TGA images with top right or bottom right orientations are now supported. diff --git a/docs/releasenotes/9.0.1.rst b/docs/releasenotes/9.0.1.rst new file mode 100644 index 000000000..c1feee088 --- /dev/null +++ b/docs/releasenotes/9.0.1.rst @@ -0,0 +1,23 @@ +9.0.1 +----- + +Security +======== + +This release addresses several security problems. + +:cve:`CVE-2022-24303`: If the path to the temporary directory on Linux or macOS +contained a space, this would break removal of the temporary image file after +``im.show()`` (and related actions), and potentially remove an unrelated file. This +has been present since PIL. + +:cve:`CVE-2022-22817`: While Pillow 9.0 restricted top-level builtins available to +:py:meth:`PIL.ImageMath.eval`, it did not prevent builtins available to lambda +expressions. These are now also restricted. + +Other Changes +============= + +Pillow 9.0 added support for ``xdg-open`` as an image viewer, but there have been +reports that the temporary image file was removed too quickly to be loaded into the +final application. A delay has been added. diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst new file mode 100644 index 000000000..80ce7604f --- /dev/null +++ b/docs/releasenotes/9.1.0.rst @@ -0,0 +1,228 @@ +9.1.0 +----- + +API Changes +=========== + +Raise an error when performing a negative crop +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Performing a negative crop on an image previously just returned a ``(0, 0)`` image. Now +it will raise a ``ValueError``, to help reduce confusion if a user has unintentionally +provided the wrong arguments. + +Added specific error if path coordinate type is incorrect +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rather than returning a ``SystemError``, passing the incorrect types of coordinates into +a path will now raise a more specific ``ValueError``, with the message "incorrect +coordinate type". + +Replace requirements.txt with extras +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rather than installing all dependencies for docs and tests via ``requirements.txt``, +``extras_require`` is used instead. This installs only those needed and at the same +time as installing Pillow. + +For example: + +.. code-block:: bash + + # Install with dependencies for tests: + python3 -m pip install .[tests] + + # Or for building docs: + python3 -m pip install .[docs] + + # Or for all: + python3 -m pip install .[docs,tests] + +On macOS, the last argument may need to be wrapped in quotes, e.g. +``python3 -m pip install ".[tests]"`` + +Therefore ``requirements.txt`` has been removed along with the ``make install-req`` +command for installing its contents. + +Deprecations +============ + +Constants +^^^^^^^^^ + +A number of constants have been deprecated and will be removed in Pillow 10.0.0 +(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. + +===================================================== ============================================================ +Deprecated Use instead +===================================================== ============================================================ +``Image.NONE`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.NEAREST`` Either ``Image.Dither.NONE`` or ``Image.Resampling.NEAREST`` +``Image.ORDERED`` ``Image.Dither.ORDERED`` +``Image.RASTERIZE`` ``Image.Dither.RASTERIZE`` +``Image.FLOYDSTEINBERG`` ``Image.Dither.FLOYDSTEINBERG`` +``Image.WEB`` ``Image.Palette.WEB`` +``Image.ADAPTIVE`` ``Image.Palette.ADAPTIVE`` +``Image.AFFINE`` ``Image.Transform.AFFINE`` +``Image.EXTENT`` ``Image.Transform.EXTENT`` +``Image.PERSPECTIVE`` ``Image.Transform.PERSPECTIVE`` +``Image.QUAD`` ``Image.Transform.QUAD`` +``Image.MESH`` ``Image.Transform.MESH`` +``Image.FLIP_LEFT_RIGHT`` ``Image.Transpose.FLIP_LEFT_RIGHT`` +``Image.FLIP_TOP_BOTTOM`` ``Image.Transpose.FLIP_TOP_BOTTOM`` +``Image.ROTATE_90`` ``Image.Transpose.ROTATE_90`` +``Image.ROTATE_180`` ``Image.Transpose.ROTATE_180`` +``Image.ROTATE_270`` ``Image.Transpose.ROTATE_270`` +``Image.TRANSPOSE`` ``Image.Transpose.TRANSPOSE`` +``Image.TRANSVERSE`` ``Image.Transpose.TRANSVERSE`` +``Image.BOX`` ``Image.Resampling.BOX`` +``Image.BILINEAR`` ``Image.Resampling.BILNEAR`` +``Image.LINEAR`` ``Image.Resampling.BILNEAR`` +``Image.HAMMING`` ``Image.Resampling.HAMMING`` +``Image.BICUBIC`` ``Image.Resampling.BICUBIC`` +``Image.CUBIC`` ``Image.Resampling.BICUBIC`` +``Image.LANCZOS`` ``Image.Resampling.LANCZOS`` +``Image.ANTIALIAS`` ``Image.Resampling.LANCZOS`` +``Image.MEDIANCUT`` ``Image.Quantize.MEDIANCUT`` +``Image.MAXCOVERAGE`` ``Image.Quantize.MAXCOVERAGE`` +``Image.FASTOCTREE`` ``Image.Quantize.FASTOCTREE`` +``Image.LIBIMAGEQUANT`` ``Image.Quantize.LIBIMAGEQUANT`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + +ImageShow.Viewer.show_file file argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by +``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. +``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest +``viewer.show_file(path="test.jpg")`` instead. + +FitsStubImagePlugin +^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 9.1.0 + +The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in +Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through +:mod:`~PIL.FitsImagePlugin` instead. + +API Additions +============= + +Added get_photoshop_blocks() to parse Photoshop TIFF tag +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.TiffImagePlugin.TiffImageFile.get_photoshop_blocks` has been added, to +allow users to determine what Photoshop "Image Resource Blocks" are contained within an +image. The keys of the returned dictionary are the image resource IDs. + +At present, the information within each block is merely returned as a dictionary with a +"data" entry. This will allow more useful information to be added in the future without +breaking backwards compatibility. + +Added mct and no_jp2 options for saving JPEG 2000 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :py:meth:`PIL.Image.Image.save` method now supports the following options for +JPEG 2000: + +**mct** + If ``1`` then enable multiple component transformation when encoding, + otherwise use ``0`` for no component transformation (default). If MCT is + enabled and ``irreversible`` is ``True`` then the Irreversible Color + Transformation will be applied, otherwise encoding will use the + Reversible Color Transformation. MCT works best with a ``mode`` of + ``RGB`` and is only applicable when the image data has 3 components. + +**no_jp2** + If ``True`` then don't wrap the raw codestream in the JP2 file format when + saving, otherwise the extension of the filename will be used to determine + the format (default). + +Added PyEncoder +^^^^^^^^^^^^^^^ + +:py:class:`~PIL.ImageFile.PyEncoder` has been added, allowing for file encoders to be +written in Python. See :ref:`Writing Your Own File Codec in Python` for +more information. + +GifImagePlugin loading strategy +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This +behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as +well. + +.. code-block:: python + + from PIL import GifImagePlugin + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS + +Or subsequent frames can be kept in ``P`` mode as long as there is only a single +palette. + +.. code-block:: python + + from PIL import GifImagePlugin + GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY + +Other Changes +============= + +musllinux wheels +^^^^^^^^^^^^^^^^ + +Pillow now builds binary wheels for musllinux, suitable for Linux distributions based on the musl C standard library such as Alpine +(rather than the glibc library used by manylinux wheels). See :pep:`656`. + +ImageShow temporary files on Unix +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When calling :py:meth:`~PIL.Image.Image.show` or using :py:mod:`~PIL.ImageShow`, +a temporary file is created from the image. On Unix, Pillow will no longer delete these +files, and instead leave it to the operating system to do so. + +Image._repr_pretty_ +^^^^^^^^^^^^^^^^^^^ + +``im._repr_pretty_`` has been added to provide a representation of an image without the +identity of the object. This allows Jupyter to describe an image and have that +description stay the same on subsequent executions of the same code. + +Added BigTIFF reading +^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for reading BigTIFF images. + +Added BLP saving +^^^^^^^^^^^^^^^^ + +Support has been added for saving BLP images. ``blp_version`` can be used to specify +whether the image should be saved as BLP1 or BLP2, e.g. +``im.save("out.blp", blp_version="BLP1")``. By default, BLP2 will be used. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 55c51a401..656acef95 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,7 +14,11 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 9.1.0 + 9.0.1 + 9.0.0 8.4.0 + 8.3.2 8.3.1 8.3.0 8.2.0 diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst index bf381114e..f7271ae2b 100644 --- a/docs/releasenotes/template.rst +++ b/docs/releasenotes/template.rst @@ -34,6 +34,9 @@ TODO Security ======== +TODO +^^^^ + TODO Other Changes diff --git a/docs/releasenotes/versioning.rst b/docs/releasenotes/versioning.rst index a8c9fc998..87f2ba422 100644 --- a/docs/releasenotes/versioning.rst +++ b/docs/releasenotes/versioning.rst @@ -11,7 +11,7 @@ Pillow follows `Semantic Versioning `_: 2. MINOR version when you add functionality in a backwards compatible manner, and 3. PATCH version when you make backwards compatible bug fixes. -Quarterly releases ("`Main Release `_") +Quarterly releases ("`Main Release `_") bump at least the MINOR version, as new functionality has likely been added in the prior three months. @@ -21,10 +21,10 @@ these occur every 12-18 months, guided by `Python's EOL schedule `_, and any APIs that have been deprecated for at least a year are removed at the same time. -PATCH versions ("`Point Release `_" -or "`Embargoed Release `_") +PATCH versions ("`Point Release `_" +or "`Embargoed Release `_") are for security, installation or critical bug fixes. These are less common as it is preferred to stick to quarterly releases. -Between quarterly releases, ".dev0" is appended to the "master" branch, indicating that +Between quarterly releases, ``.dev0`` is appended to the ``main`` branch, indicating that this is not a formally released copy. diff --git a/docs/resources/css/dark.css b/docs/resources/css/dark.css index 8866c07ea..1571cbc4e 100644 --- a/docs/resources/css/dark.css +++ b/docs/resources/css/dark.css @@ -1,1996 +1,9 @@ +/* For black-on-white/transparent images at handbook/text-anchors.html */ +body[data-theme="dark"] #text-anchors img { + filter: invert(1) brightness(0.85) hue-rotate(-60deg); +} @media (prefers-color-scheme: dark) { - html { - background-color: #181a1b !important; - } - - html, body, input, textarea, select, button { - background-color: #181a1b; - } - - html, body, input, textarea, select, button { - border-color: #736b5e; - color: #e8e6e3; - } - - a { - color: #3391ff; - } - - table { - border-color: #545b5e; - } - - ::placeholder { - color: #b2aba1; - } - - input:-webkit-autofill, - textarea:-webkit-autofill, - select:-webkit-autofill { - background-color: #555b00 !important; - color: #e8e6e3 !important; - } - - ::selection { - background-color: #004daa !important; - color: #e8e6e3 !important; - } - - ::-moz-selection { - background-color: #004daa !important; - color: #e8e6e3 !important; - } - - /* Invert Style */ - .jfk-bubble.gtx-bubble, embed[type="application/pdf"] { - filter: invert(100%) hue-rotate(180deg) contrast(90%) !important; - } - - /* Override Style */ - .vimvixen-hint { - background-color: #7b5300 !important; - border-color: #d8b013 !important; - color: #f3e8c8 !important; - } - - ::placeholder { - opacity: 0.5 !important; - } - - /* Variables Style */ - :root { - --darkreader-neutral-background: #181a1b; - --darkreader-neutral-text: #e8e6e3; - --darkreader-selection-background: #004daa; - --darkreader-selection-text: #e8e6e3; - } - - /* Modified CSS */ - a:hover, - a:active { - outline-color: initial; - } - - abbr[title] { - border-bottom-color: initial; - } - - ins { - background-image: initial; - background-color: rgb(112, 112, 0); - color: rgb(232, 230, 227); - text-decoration-color: initial; - } - - mark { - background-image: initial; - background-color: rgb(204, 204, 0); - color: rgb(232, 230, 227); - } - - ul, - ol, - dl { - list-style-image: none; - } - - li { - list-style-image: initial; - } - - img { - border-color: initial; - } - - fieldset { - border-color: initial; - } - - legend { - border-color: initial; - } - - .chromeframe { - background-image: initial; - background-color: rgb(53, 57, 59); - color: rgb(232, 230, 227); - } - - .ir { - border-color: initial; - background-color: transparent; - } - - .visuallyhidden { - border-color: initial; - } - - .fa-border { - border-color: rgb(53, 57, 59); - } - - .fa-inverse { - color: rgb(232, 230, 227); - } - - .sr-only { - border-color: initial; - } - - .fa::before, - .wy-menu-vertical li span.toctree-expand::before, - .wy-menu-vertical li.on a span.toctree-expand::before, - .wy-menu-vertical li.current > a span.toctree-expand::before, - .rst-content .admonition-title::before, - .rst-content h1 .headerlink::before, - .rst-content h2 .headerlink::before, - .rst-content h3 .headerlink::before, - .rst-content h4 .headerlink::before, - .rst-content h5 .headerlink::before, - .rst-content h6 .headerlink::before, - .rst-content dl dt .headerlink::before, - .rst-content p.caption .headerlink::before, - .rst-content table > caption .headerlink::before, - .rst-content .code-block-caption .headerlink::before, - .rst-content tt.download span:first-child::before, - .rst-content code.download span:first-child::before, - .icon::before, - .wy-dropdown .caret::before, - .wy-inline-validate.wy-inline-validate-success .wy-input-context::before, - .wy-inline-validate.wy-inline-validate-danger .wy-input-context::before, - .wy-inline-validate.wy-inline-validate-warning .wy-input-context::before, - .wy-inline-validate.wy-inline-validate-info .wy-input-context::before { - text-decoration-color: inherit; - } - - a .fa, - a .wy-menu-vertical li span.toctree-expand, - .wy-menu-vertical li a span.toctree-expand, - .wy-menu-vertical li.on a span.toctree-expand, - .wy-menu-vertical li.current > a span.toctree-expand, - a .rst-content .admonition-title, - .rst-content a .admonition-title, - a .rst-content h1 .headerlink, - .rst-content h1 a .headerlink, - a .rst-content h2 .headerlink, - .rst-content h2 a .headerlink, - a .rst-content h3 .headerlink, - .rst-content h3 a .headerlink, - a .rst-content h4 .headerlink, - .rst-content h4 a .headerlink, - a .rst-content h5 .headerlink, - .rst-content h5 a .headerlink, - a .rst-content h6 .headerlink, - .rst-content h6 a .headerlink, - a .rst-content dl dt .headerlink, - .rst-content dl dt a .headerlink, - a .rst-content p.caption .headerlink, - .rst-content p.caption a .headerlink, - a .rst-content table > caption .headerlink, - .rst-content table > caption a .headerlink, - a .rst-content .code-block-caption .headerlink, - .rst-content .code-block-caption a .headerlink, - a .rst-content tt.download span:first-child, - .rst-content tt.download a span:first-child, - a .rst-content code.download span:first-child, - .rst-content code.download a span:first-child, - a .icon { - text-decoration-color: inherit; - } - - .wy-alert, - .rst-content .note, - .rst-content .attention, - .rst-content .caution, - .rst-content .danger, - .rst-content .error, - .rst-content .hint, - .rst-content .important, - .rst-content .tip, - .rst-content .warning, - .rst-content .seealso, - .rst-content .admonition-todo, - .rst-content .admonition { - background-image: initial; - background-color: rgb(32, 35, 36); - } - - .wy-alert-title, - .rst-content .admonition-title { - color: rgb(232, 230, 227); - background-image: initial; - background-color: rgb(29, 91, 131); - } - - .wy-alert.wy-alert-danger, - .rst-content .wy-alert-danger.note, - .rst-content .wy-alert-danger.attention, - .rst-content .wy-alert-danger.caution, - .rst-content .danger, - .rst-content .error, - .rst-content .wy-alert-danger.hint, - .rst-content .wy-alert-danger.important, - .rst-content .wy-alert-danger.tip, - .rst-content .wy-alert-danger.warning, - .rst-content .wy-alert-danger.seealso, - .rst-content .wy-alert-danger.admonition-todo, - .rst-content .wy-alert-danger.admonition { - background-image: initial; - background-color: rgb(52, 12, 8); - } - - .wy-alert.wy-alert-danger .wy-alert-title, - .rst-content .wy-alert-danger.note .wy-alert-title, - .rst-content .wy-alert-danger.attention .wy-alert-title, - .rst-content .wy-alert-danger.caution .wy-alert-title, - .rst-content .danger .wy-alert-title, - .rst-content .error .wy-alert-title, - .rst-content .wy-alert-danger.hint .wy-alert-title, - .rst-content .wy-alert-danger.important .wy-alert-title, - .rst-content .wy-alert-danger.tip .wy-alert-title, - .rst-content .wy-alert-danger.warning .wy-alert-title, - .rst-content .wy-alert-danger.seealso .wy-alert-title, - .rst-content .wy-alert-danger.admonition-todo .wy-alert-title, - .rst-content .wy-alert-danger.admonition .wy-alert-title, - .wy-alert.wy-alert-danger .rst-content .admonition-title, - .rst-content .wy-alert.wy-alert-danger .admonition-title, - .rst-content .wy-alert-danger.note .admonition-title, - .rst-content .wy-alert-danger.attention .admonition-title, - .rst-content .wy-alert-danger.caution .admonition-title, - .rst-content .danger .admonition-title, - .rst-content .error .admonition-title, - .rst-content .wy-alert-danger.hint .admonition-title, - .rst-content .wy-alert-danger.important .admonition-title, - .rst-content .wy-alert-danger.tip .admonition-title, - .rst-content .wy-alert-danger.warning .admonition-title, - .rst-content .wy-alert-danger.seealso .admonition-title, - .rst-content .wy-alert-danger.admonition-todo .admonition-title, - .rst-content .wy-alert-danger.admonition .admonition-title { - background-image: initial; - background-color: rgb(108, 22, 13); - } - - .wy-alert.wy-alert-warning, - .rst-content .wy-alert-warning.note, - .rst-content .attention, - .rst-content .caution, - .rst-content .wy-alert-warning.danger, - .rst-content .wy-alert-warning.error, - .rst-content .wy-alert-warning.hint, - .rst-content .wy-alert-warning.important, - .rst-content .wy-alert-warning.tip, - .rst-content .warning, - .rst-content .wy-alert-warning.seealso, - .rst-content .admonition-todo, - .rst-content .wy-alert-warning.admonition { - background-image: initial; - background-color: rgb(82, 53, 0); - } - - .wy-alert.wy-alert-warning .wy-alert-title, - .rst-content .wy-alert-warning.note .wy-alert-title, - .rst-content .attention .wy-alert-title, - .rst-content .caution .wy-alert-title, - .rst-content .wy-alert-warning.danger .wy-alert-title, - .rst-content .wy-alert-warning.error .wy-alert-title, - .rst-content .wy-alert-warning.hint .wy-alert-title, - .rst-content .wy-alert-warning.important .wy-alert-title, - .rst-content .wy-alert-warning.tip .wy-alert-title, - .rst-content .warning .wy-alert-title, - .rst-content .wy-alert-warning.seealso .wy-alert-title, - .rst-content .admonition-todo .wy-alert-title, - .rst-content .wy-alert-warning.admonition .wy-alert-title, - .wy-alert.wy-alert-warning .rst-content .admonition-title, - .rst-content .wy-alert.wy-alert-warning .admonition-title, - .rst-content .wy-alert-warning.note .admonition-title, - .rst-content .attention .admonition-title, - .rst-content .caution .admonition-title, - .rst-content .wy-alert-warning.danger .admonition-title, - .rst-content .wy-alert-warning.error .admonition-title, - .rst-content .wy-alert-warning.hint .admonition-title, - .rst-content .wy-alert-warning.important .admonition-title, - .rst-content .wy-alert-warning.tip .admonition-title, - .rst-content .warning .admonition-title, - .rst-content .wy-alert-warning.seealso .admonition-title, - .rst-content .admonition-todo .admonition-title, - .rst-content .wy-alert-warning.admonition .admonition-title { - background-image: initial; - background-color: rgb(123, 65, 14); - } - - .wy-alert.wy-alert-info, - .rst-content .note, - .rst-content .wy-alert-info.attention, - .rst-content .wy-alert-info.caution, - .rst-content .wy-alert-info.danger, - .rst-content .wy-alert-info.error, - .rst-content .wy-alert-info.hint, - .rst-content .wy-alert-info.important, - .rst-content .wy-alert-info.tip, - .rst-content .wy-alert-info.warning, - .rst-content .seealso, - .rst-content .wy-alert-info.admonition-todo, - .rst-content .wy-alert-info.admonition { - background-image: initial; - background-color: rgb(32, 35, 36); - } - - .wy-alert.wy-alert-info .wy-alert-title, - .rst-content .note .wy-alert-title, - .rst-content .wy-alert-info.attention .wy-alert-title, - .rst-content .wy-alert-info.caution .wy-alert-title, - .rst-content .wy-alert-info.danger .wy-alert-title, - .rst-content .wy-alert-info.error .wy-alert-title, - .rst-content .wy-alert-info.hint .wy-alert-title, - .rst-content .wy-alert-info.important .wy-alert-title, - .rst-content .wy-alert-info.tip .wy-alert-title, - .rst-content .wy-alert-info.warning .wy-alert-title, - .rst-content .seealso .wy-alert-title, - .rst-content .wy-alert-info.admonition-todo .wy-alert-title, - .rst-content .wy-alert-info.admonition .wy-alert-title, - .wy-alert.wy-alert-info .rst-content .admonition-title, - .rst-content .wy-alert.wy-alert-info .admonition-title, - .rst-content .note .admonition-title, - .rst-content .wy-alert-info.attention .admonition-title, - .rst-content .wy-alert-info.caution .admonition-title, - .rst-content .wy-alert-info.danger .admonition-title, - .rst-content .wy-alert-info.error .admonition-title, - .rst-content .wy-alert-info.hint .admonition-title, - .rst-content .wy-alert-info.important .admonition-title, - .rst-content .wy-alert-info.tip .admonition-title, - .rst-content .wy-alert-info.warning .admonition-title, - .rst-content .seealso .admonition-title, - .rst-content .wy-alert-info.admonition-todo .admonition-title, - .rst-content .wy-alert-info.admonition .admonition-title { - background-image: initial; - background-color: rgb(29, 91, 131); - } - - .wy-alert.wy-alert-success, - .rst-content .wy-alert-success.note, - .rst-content .wy-alert-success.attention, - .rst-content .wy-alert-success.caution, - .rst-content .wy-alert-success.danger, - .rst-content .wy-alert-success.error, - .rst-content .hint, - .rst-content .important, - .rst-content .tip, - .rst-content .wy-alert-success.warning, - .rst-content .wy-alert-success.seealso, - .rst-content .wy-alert-success.admonition-todo, - .rst-content .wy-alert-success.admonition { - background-image: initial; - background-color: rgb(9, 66, 58); - } - - .wy-alert.wy-alert-success .wy-alert-title, - .rst-content .wy-alert-success.note .wy-alert-title, - .rst-content .wy-alert-success.attention .wy-alert-title, - .rst-content .wy-alert-success.caution .wy-alert-title, - .rst-content .wy-alert-success.danger .wy-alert-title, - .rst-content .wy-alert-success.error .wy-alert-title, - .rst-content .hint .wy-alert-title, - .rst-content .important .wy-alert-title, - .rst-content .tip .wy-alert-title, - .rst-content .wy-alert-success.warning .wy-alert-title, - .rst-content .wy-alert-success.seealso .wy-alert-title, - .rst-content .wy-alert-success.admonition-todo .wy-alert-title, - .rst-content .wy-alert-success.admonition .wy-alert-title, - .wy-alert.wy-alert-success .rst-content .admonition-title, - .rst-content .wy-alert.wy-alert-success .admonition-title, - .rst-content .wy-alert-success.note .admonition-title, - .rst-content .wy-alert-success.attention .admonition-title, - .rst-content .wy-alert-success.caution .admonition-title, - .rst-content .wy-alert-success.danger .admonition-title, - .rst-content .wy-alert-success.error .admonition-title, - .rst-content .hint .admonition-title, - .rst-content .important .admonition-title, - .rst-content .tip .admonition-title, - .rst-content .wy-alert-success.warning .admonition-title, - .rst-content .wy-alert-success.seealso .admonition-title, - .rst-content .wy-alert-success.admonition-todo .admonition-title, - .rst-content .wy-alert-success.admonition .admonition-title { - background-image: initial; - background-color: rgb(21, 150, 125); - } - - .wy-alert.wy-alert-neutral, - .rst-content .wy-alert-neutral.note, - .rst-content .wy-alert-neutral.attention, - .rst-content .wy-alert-neutral.caution, - .rst-content .wy-alert-neutral.danger, - .rst-content .wy-alert-neutral.error, - .rst-content .wy-alert-neutral.hint, - .rst-content .wy-alert-neutral.important, - .rst-content .wy-alert-neutral.tip, - .rst-content .wy-alert-neutral.warning, - .rst-content .wy-alert-neutral.seealso, - .rst-content .wy-alert-neutral.admonition-todo, - .rst-content .wy-alert-neutral.admonition { - background-image: initial; - background-color: rgb(27, 36, 36); - } - - .wy-alert.wy-alert-neutral .wy-alert-title, - .rst-content .wy-alert-neutral.note .wy-alert-title, - .rst-content .wy-alert-neutral.attention .wy-alert-title, - .rst-content .wy-alert-neutral.caution .wy-alert-title, - .rst-content .wy-alert-neutral.danger .wy-alert-title, - .rst-content .wy-alert-neutral.error .wy-alert-title, - .rst-content .wy-alert-neutral.hint .wy-alert-title, - .rst-content .wy-alert-neutral.important .wy-alert-title, - .rst-content .wy-alert-neutral.tip .wy-alert-title, - .rst-content .wy-alert-neutral.warning .wy-alert-title, - .rst-content .wy-alert-neutral.seealso .wy-alert-title, - .rst-content .wy-alert-neutral.admonition-todo .wy-alert-title, - .rst-content .wy-alert-neutral.admonition .wy-alert-title, - .wy-alert.wy-alert-neutral .rst-content .admonition-title, - .rst-content .wy-alert.wy-alert-neutral .admonition-title, - .rst-content .wy-alert-neutral.note .admonition-title, - .rst-content .wy-alert-neutral.attention .admonition-title, - .rst-content .wy-alert-neutral.caution .admonition-title, - .rst-content .wy-alert-neutral.danger .admonition-title, - .rst-content .wy-alert-neutral.error .admonition-title, - .rst-content .wy-alert-neutral.hint .admonition-title, - .rst-content .wy-alert-neutral.important .admonition-title, - .rst-content .wy-alert-neutral.tip .admonition-title, - .rst-content .wy-alert-neutral.warning .admonition-title, - .rst-content .wy-alert-neutral.seealso .admonition-title, - .rst-content .wy-alert-neutral.admonition-todo .admonition-title, - .rst-content .wy-alert-neutral.admonition .admonition-title { - color: rgb(192, 186, 178); - background-image: initial; - background-color: rgb(40, 43, 45); - } - - .wy-alert.wy-alert-neutral a, - .rst-content .wy-alert-neutral.note a, - .rst-content .wy-alert-neutral.attention a, - .rst-content .wy-alert-neutral.caution a, - .rst-content .wy-alert-neutral.danger a, - .rst-content .wy-alert-neutral.error a, - .rst-content .wy-alert-neutral.hint a, - .rst-content .wy-alert-neutral.important a, - .rst-content .wy-alert-neutral.tip a, - .rst-content .wy-alert-neutral.warning a, - .rst-content .wy-alert-neutral.seealso a, - .rst-content .wy-alert-neutral.admonition-todo a, - .rst-content .wy-alert-neutral.admonition a { - color: rgb(84, 164, 217); - } - - .wy-tray-container li { - background-image: initial; - background-color: transparent; - color: rgb(232, 230, 227); - box-shadow: rgba(0, 0, 0, 0.1) 0px 5px 5px 0px; - } - - .wy-tray-container li.wy-tray-item-success { - background-image: initial; - background-color: rgb(31, 139, 77); - } - - .wy-tray-container li.wy-tray-item-info { - background-image: initial; - background-color: rgb(33, 102, 148); - } - - .wy-tray-container li.wy-tray-item-warning { - background-image: initial; - background-color: rgb(178, 94, 20); - } - - .wy-tray-container li.wy-tray-item-danger { - background-image: initial; - background-color: rgb(162, 33, 20); - } - - .btn { - color: rgb(232, 230, 227); - border-color: rgba(140, 130, 115, 0.1); - background-color: rgb(31, 139, 77); - text-decoration-color: initial; - box-shadow: rgba(24, 26, 27, 0.5) 0px 1px 2px -1px inset, - rgba(0, 0, 0, 0.1) 0px -2px 0px 0px inset; - } - - .btn-hover { - background-image: initial; - background-color: rgb(37, 114, 165); - color: rgb(232, 230, 227); - } - - .btn:hover { - background-image: initial; - background-color: rgb(35, 156, 86); - color: rgb(232, 230, 227); - } - - .btn:focus { - background-image: initial; - background-color: rgb(35, 156, 86); - outline-color: initial; - } - - .btn:active { - box-shadow: rgba(0, 0, 0, 0.05) 0px -1px 0px 0px inset, - rgba(0, 0, 0, 0.1) 0px 2px 0px 0px inset; - } - - .btn:visited { - color: rgb(232, 230, 227); - } - - .btn:disabled { - background-image: none; - box-shadow: none; - } - - .btn-disabled { - background-image: none; - box-shadow: none; - } - - .btn-disabled:hover, - .btn-disabled:focus, - .btn-disabled:active { - background-image: none; - box-shadow: none; - } - - .btn-info { - background-color: rgb(33, 102, 148) !important; - } - - .btn-info:hover { - background-color: rgb(37, 114, 165) !important; - } - - .btn-neutral { - background-color: rgb(27, 36, 36) !important; - color: rgb(192, 186, 178) !important; - } - - .btn-neutral:hover { - color: rgb(192, 186, 178); - background-color: rgb(34, 44, 44) !important; - } - - .btn-neutral:visited { - color: rgb(192, 186, 178) !important; - } - - .btn-success { - background-color: rgb(31, 139, 77) !important; - } - - .btn-success:hover { - background-color: rgb(27, 122, 68) !important; - } - - .btn-danger { - background-color: rgb(162, 33, 20) !important; - } - - .btn-danger:hover { - background-color: rgb(149, 30, 18) !important; - } - - .btn-warning { - background-color: rgb(178, 94, 20) !important; - } - - .btn-warning:hover { - background-color: rgb(165, 87, 18) !important; - } - - .btn-invert { - background-color: rgb(26, 28, 29); - } - - .btn-invert:hover { - background-color: rgb(35, 38, 40) !important; - } - - .btn-link { - color: rgb(84, 164, 217); - box-shadow: none; - background-color: transparent !important; - border-color: transparent !important; - } - - .btn-link:hover { - box-shadow: none; - background-color: transparent !important; - color: rgb(79, 162, 216) !important; - } - - .btn-link:active { - box-shadow: none; - background-color: transparent !important; - color: rgb(79, 162, 216) !important; - } - - .btn-link:visited { - color: rgb(164, 103, 188); - } - - .wy-dropdown-menu { - background-image: initial; - background-color: rgb(26, 28, 29); - border-color: rgb(60, 65, 67); - box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 2px 0px; - } - - .wy-dropdown-menu > dd > a { - color: rgb(192, 186, 178); - } - - .wy-dropdown-menu > dd > a:hover { - background-image: initial; - background-color: rgb(33, 102, 148); - color: rgb(232, 230, 227); - } - - .wy-dropdown-menu > dd.divider { - border-top-color: rgb(60, 65, 67); - } - - .wy-dropdown-menu > dd.call-to-action { - background-image: initial; - background-color: rgb(40, 43, 45); - } - - .wy-dropdown-menu > dd.call-to-action:hover { - background-image: initial; - background-color: rgb(40, 43, 45); - } - - .wy-dropdown-menu > dd.call-to-action .btn { - color: rgb(232, 230, 227); - } - - .wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu { - background-image: initial; - background-color: rgb(26, 28, 29); - } - - .wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover { - background-image: initial; - background-color: rgb(33, 102, 148); - color: rgb(232, 230, 227); - } - - .wy-dropdown-arrow::before { - border-bottom-color: rgb(51, 55, 57); - border-left-color: transparent; - border-right-color: transparent; - } - - fieldset { - border-color: initial; - } - - legend { - border-color: initial; - } - - label { - color: rgb(200, 195, 188); - } - - .wy-control-group.wy-control-group-required > label::after { - color: rgb(233, 88, 73); - } - - .wy-form-message-inline { - color: rgb(168, 160, 149); - } - - .wy-form-message { - color: rgb(168, 160, 149); - } - - input[type="text"], input[type="password"], input[type="email"], input[type="url"], input[type="date"], input[type="month"], input[type="time"], input[type="datetime"], input[type="datetime-local"], input[type="week"], input[type="number"], input[type="search"], input[type="tel"], input[type="color"] { - border-color: rgb(62, 68, 70); - box-shadow: rgb(43, 47, 49) 0px 1px 3px inset; - } - - input[type="text"]:focus, input[type="password"]:focus, input[type="email"]:focus, input[type="url"]:focus, input[type="date"]:focus, input[type="month"]:focus, input[type="time"]:focus, input[type="datetime"]:focus, input[type="datetime-local"]:focus, input[type="week"]:focus, input[type="number"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="color"]:focus { - outline-color: initial; - border-color: rgb(123, 114, 101); - } - - input.no-focus:focus { - border-color: rgb(62, 68, 70) !important; - } - - input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus { - outline-color: rgb(13, 113, 167); - } - - input[type="text"][disabled], input[type="password"][disabled], input[type="email"][disabled], input[type="url"][disabled], input[type="date"][disabled], input[type="month"][disabled], input[type="time"][disabled], input[type="datetime"][disabled], input[type="datetime-local"][disabled], input[type="week"][disabled], input[type="number"][disabled], input[type="search"][disabled], input[type="tel"][disabled], input[type="color"][disabled] { - background-color: rgb(27, 29, 30); - } - - input:focus:invalid, - textarea:focus:invalid, - select:focus:invalid { - color: rgb(233, 88, 73); - border-color: rgb(149, 31, 18); - } - - input:focus:invalid:focus, - textarea:focus:invalid:focus, - select:focus:invalid:focus { - border-color: rgb(149, 31, 18); - } - - input[type="file"]:focus:invalid:focus, input[type="radio"]:focus:invalid:focus, input[type="checkbox"]:focus:invalid:focus { - outline-color: rgb(149, 31, 18); - } - - select, - textarea { - border-color: rgb(62, 68, 70); - box-shadow: rgb(43, 47, 49) 0px 1px 3px inset; - } - - select { - border-color: rgb(62, 68, 70); - background-color: rgb(24, 26, 27); - } - - select:focus, - textarea:focus { - outline-color: initial; - } - - select[disabled], - textarea[disabled], - input[readonly], - select[readonly], - textarea[readonly] { - background-color: rgb(27, 29, 30); - } - - .wy-checkbox, - .wy-radio { - color: rgb(192, 186, 178); - } - - .wy-input-prefix .wy-input-context, - .wy-input-suffix .wy-input-context { - background-color: rgb(27, 36, 36); - border-color: rgb(62, 68, 70); - color: rgb(168, 160, 149); - } - - .wy-input-suffix .wy-input-context { - border-left-color: initial; - } - - .wy-input-prefix .wy-input-context { - border-right-color: initial; - } - - .wy-switch::before { - background-image: initial; - background-color: rgb(53, 57, 59); - } - - .wy-switch::after { - background-image: initial; - background-color: rgb(82, 88, 92); - } - - .wy-switch span { - color: rgb(200, 195, 188); - } - - .wy-switch.active::before { - background-image: initial; - background-color: rgb(24, 106, 58); - } - - .wy-switch.active::after { - background-image: initial; - background-color: rgb(31, 139, 77); - } - - .wy-control-group.wy-control-group-error .wy-form-message, - .wy-control-group.wy-control-group-error > label { - color: rgb(233, 88, 73); - } - - .wy-control-group.wy-control-group-error input[type="text"], .wy-control-group.wy-control-group-error input[type="password"], .wy-control-group.wy-control-group-error input[type="email"], .wy-control-group.wy-control-group-error input[type="url"], .wy-control-group.wy-control-group-error input[type="date"], .wy-control-group.wy-control-group-error input[type="month"], .wy-control-group.wy-control-group-error input[type="time"], .wy-control-group.wy-control-group-error input[type="datetime"], .wy-control-group.wy-control-group-error input[type="datetime-local"], .wy-control-group.wy-control-group-error input[type="week"], .wy-control-group.wy-control-group-error input[type="number"], .wy-control-group.wy-control-group-error input[type="search"], .wy-control-group.wy-control-group-error input[type="tel"], .wy-control-group.wy-control-group-error input[type="color"] { - border-color: rgb(149, 31, 18); - } - - .wy-control-group.wy-control-group-error textarea { - border-color: rgb(149, 31, 18); - } - - .wy-inline-validate.wy-inline-validate-success .wy-input-context { - color: rgb(92, 218, 145); - } - - .wy-inline-validate.wy-inline-validate-danger .wy-input-context { - color: rgb(233, 88, 73); - } - - .wy-inline-validate.wy-inline-validate-warning .wy-input-context { - color: rgb(232, 138, 54); - } - - .wy-inline-validate.wy-inline-validate-info .wy-input-context { - color: rgb(84, 164, 217); - } - - .wy-table caption, - .rst-content table.docutils caption, - .rst-content table.field-list caption { - color: rgb(232, 230, 227); - } - - .wy-table thead, - .rst-content table.docutils thead, - .rst-content table.field-list thead { - color: rgb(232, 230, 227); - } - - .wy-table thead th, - .rst-content table.docutils thead th, - .rst-content table.field-list thead th { - border-bottom-color: rgb(56, 61, 63); - } - - .wy-table td, - .rst-content table.docutils td, - .rst-content table.field-list td { - background-color: transparent; - } - - .wy-table-secondary { - color: rgb(152, 143, 129); - } - - .wy-table-tertiary { - color: rgb(152, 143, 129); - } - - .wy-table-odd td, - .wy-table-striped tr:nth-child(2n-1) td, - .rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { - background-color: rgb(27, 36, 36); - } - - .wy-table-backed { - background-color: rgb(27, 36, 36); - } - - .wy-table-bordered-all, - .rst-content table.docutils { - border-color: rgb(56, 61, 63); - } - - .wy-table-bordered-all td, - .rst-content table.docutils td { - border-bottom-color: rgb(56, 61, 63); - border-left-color: rgb(56, 61, 63); - } - - .wy-table-bordered { - border-color: rgb(56, 61, 63); - } - - .wy-table-bordered-rows td { - border-bottom-color: rgb(56, 61, 63); - } - - .wy-table-horizontal td, - .wy-table-horizontal th { - border-bottom-color: rgb(56, 61, 63); - } - - a { - color: rgb(84, 164, 217); - text-decoration-color: initial; - } - - a:hover { - color: rgb(68, 156, 214); - } - - a:visited { - color: rgb(164, 103, 188); - } - - body { - color: rgb(192, 186, 178); - background-image: initial; - background-color: rgb(33, 35, 37); - } - - .wy-text-strike { - text-decoration-color: initial; - } - - .wy-text-warning { - color: rgb(232, 138, 54) !important; - } - - a.wy-text-warning:hover { - color: rgb(236, 157, 87) !important; - } - - .wy-text-info { - color: rgb(84, 164, 217) !important; - } - - a.wy-text-info:hover { - color: rgb(79, 162, 216) !important; - } - - .wy-text-success { - color: rgb(92, 218, 145) !important; - } - - a.wy-text-success:hover { - color: rgb(73, 214, 133) !important; - } - - .wy-text-danger { - color: rgb(233, 88, 73) !important; - } - - a.wy-text-danger:hover { - color: rgb(237, 118, 104) !important; - } - - .wy-text-neutral { - color: rgb(192, 186, 178) !important; - } - - a.wy-text-neutral:hover { - color: rgb(176, 169, 159) !important; - } - - hr { - border-right-color: initial; - border-bottom-color: initial; - border-left-color: initial; - border-top-color: rgb(56, 61, 63); - } - - code, - .rst-content tt, - .rst-content code { - background-image: initial; - background-color: rgb(24, 26, 27); - border-color: rgb(56, 61, 63); - color: rgb(233, 88, 73); - } - - .wy-plain-list-disc, - .rst-content .section ul, - .rst-content .toctree-wrapper ul, - article ul { - list-style-image: initial; - } - - .wy-plain-list-disc li, - .rst-content .section ul li, - .rst-content .toctree-wrapper ul li, - article ul li { - list-style-image: initial; - } - - .wy-plain-list-disc li li, - .rst-content .section ul li li, - .rst-content .toctree-wrapper ul li li, - article ul li li { - list-style-image: initial; - } - - .wy-plain-list-disc li li li, - .rst-content .section ul li li li, - .rst-content .toctree-wrapper ul li li li, - article ul li li li { - list-style-image: initial; - } - - .wy-plain-list-disc li ol li, - .rst-content .section ul li ol li, - .rst-content .toctree-wrapper ul li ol li, - article ul li ol li { - list-style-image: initial; - } - - .wy-plain-list-decimal, - .rst-content .section ol, - .rst-content ol.arabic, - article ol { - list-style-image: initial; - } - - .wy-plain-list-decimal li, - .rst-content .section ol li, - .rst-content ol.arabic li, - article ol li { - list-style-image: initial; - } - - .wy-plain-list-decimal li ul li, - .rst-content .section ol li ul li, - .rst-content ol.arabic li ul li, - article ol li ul li { - list-style-image: initial; - } - - .wy-breadcrumbs li code, - .wy-breadcrumbs li .rst-content tt, - .rst-content .wy-breadcrumbs li tt { - border-color: initial; - background-image: none; - background-color: initial; - } - - .wy-breadcrumbs li code.literal, - .wy-breadcrumbs li .rst-content tt.literal, - .rst-content .wy-breadcrumbs li tt.literal { - color: rgb(192, 186, 178); - } - - .wy-breadcrumbs-extra { - color: rgb(184, 178, 169); - } - - .wy-menu a:hover { - text-decoration-color: initial; - } - - .wy-menu-horiz li:hover { - background-image: initial; - background-color: rgba(24, 26, 27, 0.1); - } - - .wy-menu-horiz li.divide-left { - border-left-color: rgb(119, 110, 98); - } - - .wy-menu-horiz li.divide-right { - border-right-color: rgb(119, 110, 98); - } - - .wy-menu-vertical header, - .wy-menu-vertical p.caption { - color: rgb(99, 161, 201); - } - - .wy-menu-vertical li.divide-top { - border-top-color: rgb(119, 110, 98); - } - - .wy-menu-vertical li.divide-bottom { - border-bottom-color: rgb(119, 110, 98); - } - - .wy-menu-vertical li.current { - background-image: initial; - background-color: rgb(40, 43, 45); - } - - .wy-menu-vertical li.current a { - color: rgb(152, 143, 129); - border-right-color: rgb(63, 69, 71); - } - - .wy-menu-vertical li.current a:hover { - background-image: initial; - background-color: rgb(47, 51, 53); - } - - .wy-menu-vertical li code, - .wy-menu-vertical li .rst-content tt, - .rst-content .wy-menu-vertical li tt { - border-color: initial; - background-image: inherit; - background-color: inherit; - color: inherit; - } - - .wy-menu-vertical li span.toctree-expand { - color: rgb(183, 177, 168); - } - - .wy-menu-vertical li.on a, - .wy-menu-vertical li.current > a { - color: rgb(192, 186, 178); - background-image: initial; - background-color: rgb(26, 28, 29); - border-color: initial; - } - - .wy-menu-vertical li.on a:hover, - .wy-menu-vertical li.current > a:hover { - background-image: initial; - background-color: rgb(26, 28, 29); - } - - .wy-menu-vertical li.on a:hover span.toctree-expand, - .wy-menu-vertical li.current > a:hover span.toctree-expand { - color: rgb(152, 143, 129); - } - - .wy-menu-vertical li.on a span.toctree-expand, - .wy-menu-vertical li.current > a span.toctree-expand { - color: rgb(200, 195, 188); - } - - .wy-menu-vertical li.toctree-l1.current > a { - border-bottom-color: rgb(63, 69, 71); - border-top-color: rgb(63, 69, 71); - } - - .wy-menu-vertical li.toctree-l2 a, - .wy-menu-vertical li.toctree-l3 a, - .wy-menu-vertical li.toctree-l4 a { - color: rgb(192, 186, 178); - } - - .wy-menu-vertical li.toctree-l2.current > a { - background-image: initial; - background-color: rgb(54, 59, 61); - } - - .wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a { - background-image: initial; - background-color: rgb(54, 59, 61); - } - - .wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand { - color: rgb(152, 143, 129); - } - - .wy-menu-vertical li.toctree-l2 span.toctree-expand { - color: rgb(174, 167, 156); - } - - .wy-menu-vertical li.toctree-l3.current > a { - background-image: initial; - background-color: rgb(61, 66, 69); - } - - .wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a { - background-image: initial; - background-color: rgb(61, 66, 69); - } - - .wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand { - color: rgb(152, 143, 129); - } - - .wy-menu-vertical li.toctree-l3 span.toctree-expand { - color: rgb(166, 158, 146); - } - - .wy-menu-vertical li.toctree-l2.current a, - .wy-menu-vertical li.toctree-l3.current a { - background-color: #363636; - } - - .wy-menu-vertical li ul li a { - color: rgb(208, 204, 198); - } - - .wy-menu-vertical a { - color: rgb(208, 204, 198); - } - - .wy-menu-vertical a:hover { - background-color: rgb(57, 62, 64); - } - - .wy-menu-vertical a:hover span.toctree-expand { - color: rgb(208, 204, 198); - } - - .wy-menu-vertical a:active { - background-color: rgb(33, 102, 148); - color: rgb(232, 230, 227); - } - - .wy-menu-vertical a:active span.toctree-expand { - color: rgb(232, 230, 227); - } - - .wy-side-nav-search { - background-color: rgb(33, 102, 148); - color: rgb(230, 228, 225); - } - - .wy-side-nav-search input[type="text"] { - border-color: rgb(35, 111, 160); - } - - .wy-side-nav-search img { - background-color: rgb(33, 102, 148); - } - - .wy-side-nav-search > a, - .wy-side-nav-search .wy-dropdown > a { - color: rgb(230, 228, 225); - } - - .wy-side-nav-search > a:hover, - .wy-side-nav-search .wy-dropdown > a:hover { - background-image: initial; - background-color: rgba(24, 26, 27, 0.1); - } - - .wy-side-nav-search > a img.logo, - .wy-side-nav-search .wy-dropdown > a img.logo { - background-image: initial; - background-color: transparent; - } - - .wy-side-nav-search > div.version { - color: rgba(232, 230, 227, 0.3); - } - - .wy-nav .wy-menu-vertical header { - color: rgb(84, 164, 217); - } - - .wy-nav .wy-menu-vertical a { - color: rgb(184, 178, 169); - } - - .wy-nav .wy-menu-vertical a:hover { - background-color: rgb(33, 102, 148); - color: rgb(232, 230, 227); - } - - .wy-body-for-nav { - background-image: initial; - background-color: rgb(24, 26, 27); - } - - .wy-nav-side { - color: rgb(169, 161, 150); - background-image: initial; - background-color: rgb(38, 41, 43); - } - - .wy-nav-top { - background-image: initial; - background-color: rgb(33, 102, 148); - color: rgb(232, 230, 227); - } - - .wy-nav-top a { - color: rgb(232, 230, 227); - } - - .wy-nav-top img { - background-color: rgb(33, 102, 148); - } - - .wy-nav-content-wrap { - background-image: initial; - background-color: rgb(26, 28, 29); - } - - .wy-body-mask { - background-image: initial; - background-color: rgba(0, 0, 0, 0.2); - } - - footer { - color: rgb(152, 143, 129); - } - - footer span.commit code, - footer span.commit .rst-content tt, - .rst-content footer span.commit tt { - background-image: none; - background-color: initial; - border-color: initial; - color: rgb(152, 143, 129); - } - - #search-results .search li { - border-bottom-color: rgb(56, 61, 63); - } - - #search-results .search li:first-child { - border-top-color: rgb(56, 61, 63); - } - - #search-results .context { - color: rgb(152, 143, 129); - } - - @media screen and (min-width: 1100px) { - .wy-nav-content-wrap { - background-image: initial; - background-color: rgba(0, 0, 0, 0.05); - } - - .wy-nav-content { - background-image: initial; - background-color: rgb(26, 28, 29); - } - } - .rst-versions { - color: rgb(230, 228, 225); - background-image: initial; - background-color: rgb(23, 24, 25); - } - - .rst-versions a { - color: rgb(84, 164, 217); - text-decoration-color: initial; - } - - .rst-versions .rst-current-version { - background-color: rgb(29, 31, 32); - color: rgb(92, 218, 145); - } - - .rst-versions .rst-current-version .fa, - .rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand, - .wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand, - .rst-versions .rst-current-version .rst-content .admonition-title, - .rst-content .rst-versions .rst-current-version .admonition-title, - .rst-versions .rst-current-version .rst-content h1 .headerlink, - .rst-content h1 .rst-versions .rst-current-version .headerlink, - .rst-versions .rst-current-version .rst-content h2 .headerlink, - .rst-content h2 .rst-versions .rst-current-version .headerlink, - .rst-versions .rst-current-version .rst-content h3 .headerlink, - .rst-content h3 .rst-versions .rst-current-version .headerlink, - .rst-versions .rst-current-version .rst-content h4 .headerlink, - .rst-content h4 .rst-versions .rst-current-version .headerlink, - .rst-versions .rst-current-version .rst-content h5 .headerlink, - .rst-content h5 .rst-versions .rst-current-version .headerlink, - .rst-versions .rst-current-version .rst-content h6 .headerlink, - .rst-content h6 .rst-versions .rst-current-version .headerlink, - .rst-versions .rst-current-version .rst-content dl dt .headerlink, - .rst-content dl dt .rst-versions .rst-current-version .headerlink, - .rst-versions .rst-current-version .rst-content p.caption .headerlink, - .rst-content p.caption .rst-versions .rst-current-version .headerlink, - .rst-versions .rst-current-version .rst-content table > caption .headerlink, - .rst-content table > caption .rst-versions .rst-current-version .headerlink, - .rst-versions .rst-current-version .rst-content .code-block-caption .headerlink, - .rst-content .code-block-caption .rst-versions .rst-current-version .headerlink, - .rst-versions .rst-current-version .rst-content tt.download span:first-child, - .rst-content tt.download .rst-versions .rst-current-version span:first-child, - .rst-versions .rst-current-version .rst-content code.download span:first-child, - .rst-content code.download .rst-versions .rst-current-version span:first-child, - .rst-versions .rst-current-version .icon { - color: rgb(230, 228, 225); - } - - .rst-versions .rst-current-version.rst-out-of-date { - background-color: rgb(162, 33, 20); - color: rgb(232, 230, 227); - } - - .rst-versions .rst-current-version.rst-active-old-version { - background-color: rgb(192, 156, 11); - color: rgb(232, 230, 227); - } - - .rst-versions .rst-other-versions { - color: rgb(152, 143, 129); - } - - .rst-versions .rst-other-versions hr { - border-right-color: initial; - border-bottom-color: initial; - border-left-color: initial; - border-top-color: rgb(119, 111, 98); - } - - .rst-versions .rst-other-versions dd a { - color: rgb(230, 228, 225); - } - - .rst-versions.rst-badge { - border-color: initial; - } - - .rst-content abbr[title] { - text-decoration-color: initial; - } - - .rst-content.style-external-links a.reference.external::after { - color: rgb(184, 178, 169); - } - - .rst-content pre.literal-block, .rst-content div[class^="highlight"] { - border-color: rgb(56, 61, 63); - } - - .rst-content pre.literal-block div[class^="highlight"], .rst-content div[class^="highlight"] div[class^="highlight"] { - border-color: initial; - } - - .rst-content .linenodiv pre { - border-right-color: rgb(54, 59, 61); - } - - .rst-content .admonition table { - border-color: rgba(140, 130, 115, 0.1); - } - - .rst-content .admonition table td, - .rst-content .admonition table th { - background-image: initial !important; - background-color: transparent !important; - border-color: rgba(140, 130, 115, 0.1) !important; - } - - .rst-content .section ol.loweralpha, - .rst-content .section ol.loweralpha li { - list-style-image: initial; - } - - .rst-content .section ol.upperalpha, - .rst-content .section ol.upperalpha li { - list-style-image: initial; - } - - .rst-content .toc-backref { - color: rgb(192, 186, 178); - } - - .rst-content .sidebar { - background-image: initial; - background-color: rgb(27, 36, 36); - border-color: rgb(56, 61, 63); - } - - .rst-content .sidebar .sidebar-title { - background-image: initial; - background-color: rgb(40, 43, 45); - } - - .rst-content .highlighted { - background-image: initial; - background-color: rgb(192, 156, 11); - } - - .rst-content table.docutils.citation, - .rst-content table.docutils.footnote { - background-image: none; - background-color: initial; - border-color: initial; - color: rgb(152, 143, 129); - } - - .rst-content table.docutils.citation td, - .rst-content table.docutils.citation tr, - .rst-content table.docutils.footnote td, - .rst-content table.docutils.footnote tr { - border-color: initial; - background-color: transparent !important; - } - - .rst-content table.docutils.citation tt, - .rst-content table.docutils.citation code, - .rst-content table.docutils.footnote tt, - .rst-content table.docutils.footnote code { - color: rgb(178, 172, 162); - } - - .rst-content table.docutils th { - border-color: rgb(56, 61, 63); - } - - .rst-content table.field-list { - border-color: initial; - } - - .rst-content table.field-list td { - border-color: initial; - } - - .rst-content tt, - .rst-content tt, - .rst-content code { - color: rgb(232, 230, 227); - } - - .rst-content tt.literal, - .rst-content tt.literal, - .rst-content code.literal { - color: rgb(233, 88, 73); - } - - .rst-content tt.xref, - a .rst-content tt, - .rst-content tt.xref, - .rst-content code.xref, - a .rst-content tt, - a .rst-content code { - color: rgb(192, 186, 178); - } - - .rst-content a tt, - .rst-content a tt, - .rst-content a code { - color: rgb(84, 164, 217); - } - - .rst-content dl:not(.docutils) dt { - background-image: initial; - background-color: rgb(32, 35, 36); - color: rgb(84, 164, 217); - border-top-color: rgb(28, 89, 128); - } - - .rst-content dl:not(.docutils) dt::before { - color: rgb(109, 178, 223); - } - - .rst-content dl:not(.docutils) dt .headerlink { - color: rgb(192, 186, 178); - } - - .rst-content dl:not(.docutils) dl dt { - border-top-color: initial; - border-right-color: initial; - border-bottom-color: initial; - border-left-color: rgb(62, 68, 70); - background-image: initial; - background-color: rgb(32, 35, 37); - color: rgb(178, 172, 162); - } - - .rst-content dl:not(.docutils) dl dt .headerlink { - color: rgb(192, 186, 178); - } - - .rst-content dl:not(.docutils) tt.descname, - .rst-content dl:not(.docutils) tt.descclassname, - .rst-content dl:not(.docutils) tt.descname, - .rst-content dl:not(.docutils) code.descname, - .rst-content dl:not(.docutils) tt.descclassname, - .rst-content dl:not(.docutils) code.descclassname { - background-color: transparent; - border-color: initial; - } - - .rst-content dl:not(.docutils) .optional { - color: rgb(232, 230, 227); - } - - .rst-content .viewcode-link, - .rst-content .viewcode-back { - color: rgb(92, 218, 145); - } - - .rst-content tt.download, - .rst-content code.download { - background-image: inherit; - background-color: inherit; - color: inherit; - border-color: inherit; - } - - .rst-content .guilabel { - border-color: rgb(27, 84, 122); - background-image: initial; - background-color: rgb(32, 35, 36); - } - - span[id*="MathJax-Span"] { - color: rgb(192, 186, 178); - } - - .highlight .hll { - background-color: rgb(82, 82, 0); - } - - .highlight { - background-image: initial; - background-color: rgb(61, 82, 0); - } - - .highlight .c { - color: rgb(119, 179, 195); - } - - .highlight .err { - border-color: rgb(179, 0, 0); - } - - .highlight .k { - color: rgb(126, 255, 163); - } - - .highlight .o { - color: rgb(168, 160, 149); - } - - .highlight .ch { - color: rgb(119, 179, 195); - } - - .highlight .cm { - color: rgb(119, 179, 195); - } - - .highlight .cp { - color: rgb(126, 255, 163); - } - - .highlight .cpf { - color: rgb(119, 179, 195); - } - - .highlight .c1 { - color: rgb(119, 179, 195); - } - - .highlight .cs { - color: rgb(119, 179, 195); - background-color: rgb(60, 0, 0); - } - - .highlight .gd { - color: rgb(255, 92, 92); - } - - .highlight .gr { - color: rgb(255, 26, 26); - } - - .highlight .gh { - color: rgb(127, 174, 255); - } - - .highlight .gi { - color: rgb(92, 255, 92); - } - - .highlight .go { - color: rgb(200, 195, 188); - } - - .highlight .gp { - color: rgb(246, 147, 68); - } - - .highlight .gu { - color: rgb(255, 114, 255); - } - - .highlight .gt { - color: rgb(71, 160, 255); - } - - .highlight .kc { - color: rgb(126, 255, 163); - } - - .highlight .kd { - color: rgb(126, 255, 163); - } - - .highlight .kn { - color: rgb(126, 255, 163); - } - - .highlight .kp { - color: rgb(126, 255, 163); - } - - .highlight .kr { - color: rgb(126, 255, 163); - } - - .highlight .kt { - color: rgb(255, 137, 103); - } - - .highlight .m { - color: rgb(125, 222, 174); - } - - .highlight .s { - color: rgb(123, 166, 202); - } - - .highlight .na { - color: rgb(123, 166, 202); - } - - .highlight .nb { - color: rgb(126, 255, 163); - } - - .highlight .nc { - color: rgb(81, 194, 242); - } - - .highlight .no { - color: rgb(103, 177, 215); - } - - .highlight .nd { - color: rgb(178, 172, 162); - } - - .highlight .ni { - color: rgb(217, 100, 73); - } - - .highlight .ne { - color: rgb(126, 255, 163); - } - - .highlight .nf { - color: rgb(131, 186, 249); - } - - .highlight .nl { - color: rgb(137, 193, 255); - } - - .highlight .nn { - color: rgb(81, 194, 242); - } - - .highlight .nt { - color: rgb(138, 191, 249); - } - - .highlight .nv { - color: rgb(190, 103, 215); - } - - .highlight .ow { - color: rgb(126, 255, 163); - } - - .highlight .w { - color: rgb(189, 183, 175); - } - - .highlight .mb { - color: rgb(125, 222, 174); - } - - .highlight .mf { - color: rgb(125, 222, 174); - } - - .highlight .mh { - color: rgb(125, 222, 174); - } - - .highlight .mi { - color: rgb(125, 222, 174); - } - - .highlight .mo { - color: rgb(125, 222, 174); - } - - .highlight .sa { - color: rgb(123, 166, 202); - } - - .highlight .sb { - color: rgb(123, 166, 202); - } - - .highlight .sc { - color: rgb(123, 166, 202); - } - - .highlight .dl { - color: rgb(123, 166, 202); - } - - .highlight .sd { - color: rgb(123, 166, 202); - } - - .highlight .s2 { - color: rgb(123, 166, 202); - } - - .highlight .se { - color: rgb(123, 166, 202); - } - - .highlight .sh { - color: rgb(123, 166, 202); - } - - .highlight .si { - color: rgb(117, 168, 209); - } - - .highlight .sx { - color: rgb(246, 147, 68); - } - - .highlight .sr { - color: rgb(133, 182, 224); - } - - .highlight .s1 { - color: rgb(123, 166, 202); - } - - .highlight .ss { - color: rgb(188, 230, 128); - } - - .highlight .bp { - color: rgb(126, 255, 163); - } - - .highlight .fm { - color: rgb(131, 186, 249); - } - - .highlight .vc { - color: rgb(190, 103, 215); - } - - .highlight .vg { - color: rgb(190, 103, 215); - } - - .highlight .vi { - color: rgb(190, 103, 215); - } - - .highlight .vm { - color: rgb(190, 103, 215); - } - - .highlight .il { - color: rgb(125, 222, 174); - } - - .rst-other-versions a { - border-color: initial; - } - - .ethical-sidebar .ethical-image-link, - .ethical-footer .ethical-image-link { - border-color: initial; - } - - .ethical-sidebar, - .ethical-footer { - background-color: rgb(34, 36, 38); - border-color: rgb(62, 68, 70); - color: rgb(226, 223, 219); - } - - .ethical-sidebar ul { - list-style-image: initial; - } - - .ethical-sidebar ul li { - background-color: rgb(5, 77, 121); - color: rgb(232, 230, 227); - } - - .ethical-sidebar a, - .ethical-sidebar a:visited, - .ethical-sidebar a:hover, - .ethical-sidebar a:active, - .ethical-footer a, - .ethical-footer a:visited, - .ethical-footer a:hover, - .ethical-footer a:active { - color: rgb(226, 223, 219); - text-decoration-color: initial !important; - border-bottom-color: initial !important; - } - - .ethical-callout a { - color: rgb(161, 153, 141) !important; - text-decoration-color: initial !important; - } - - .ethical-fixedfooter { - background-color: rgb(34, 36, 38); - border-top-color: rgb(66, 72, 74); - color: rgb(192, 186, 178); - } - - .ethical-fixedfooter .ethical-text::before { - background-color: rgb(61, 140, 64); - color: rgb(232, 230, 227); - } - - .ethical-fixedfooter .ethical-callout { - color: rgb(168, 160, 149); - } - - .ethical-fixedfooter a, - .ethical-fixedfooter a:hover, - .ethical-fixedfooter a:active, - .ethical-fixedfooter a:visited { - color: rgb(192, 186, 178); - text-decoration-color: initial; - } - - .ethical-rtd .ethical-sidebar { - color: rgb(184, 178, 169); - } - - .ethical-alabaster a.ethical-image-link { - border-color: initial !important; - } - - .ethical-dark-theme .ethical-sidebar { - background-color: rgb(58, 62, 65); - border-color: rgb(75, 81, 84); - color: rgb(193, 188, 180) !important; - } - - .ethical-dark-theme a, - .ethical-dark-theme a:visited { - color: rgb(216, 213, 208) !important; - border-bottom-color: initial !important; - } - - .ethical-dark-theme .ethical-callout a { - color: rgb(184, 178, 169) !important; - } - - .keep-us-sustainable { - border-color: rgb(87, 133, 38); - } - - .keep-us-sustainable a, - .keep-us-sustainable a:hover, - .keep-us-sustainable a:visited { - text-decoration-color: initial; - } - - .wy-body-for-nav .keep-us-sustainable { - color: rgb(184, 178, 169); - } - - .wy-body-for-nav .keep-us-sustainable a { - color: rgb(222, 219, 215); - } - - /* For black-on-white/transparent images at handbook/text-anchors.html */ - #text-anchors img { + body[data-theme="auto"] #text-anchors img { filter: invert(1) brightness(0.85) hue-rotate(-60deg); } } diff --git a/docs/resources/css/light.css b/docs/resources/css/light.css deleted file mode 100644 index 04edd7b16..000000000 --- a/docs/resources/css/light.css +++ /dev/null @@ -1,8 +0,0 @@ -@media (prefers-color-scheme: light) { - - .wy-menu-vertical li.toctree-l2.current a, - .wy-menu-vertical li.toctree-l3.current a { - background-color: #c9c9c9; - } - -} diff --git a/docs/resources/css/styles.css b/docs/resources/css/styles.css deleted file mode 100644 index 111f84085..000000000 --- a/docs/resources/css/styles.css +++ /dev/null @@ -1,8 +0,0 @@ -th p { - margin-bottom: 0; -} - -.rst-content tr .line-block { - font-size: 1rem; - margin-bottom: 0; -} diff --git a/docs/resources/js/script.js b/docs/resources/js/script.js deleted file mode 100644 index 5cb6494ea..000000000 --- a/docs/resources/js/script.js +++ /dev/null @@ -1,58 +0,0 @@ -jQuery(document).ready(function ($) { - setTimeout(function () { - var sectionID = 'base'; - var search = function ($section, $sidebarItem) { - $section.children('.section, .function, .method').each(function () { - if ($(this).hasClass('section')) { - sectionID = $(this).attr('id'); - search($(this), $sidebarItem.parent().find('[href="#'+sectionID+'"]')); - } else { - var $dt = $(this).children('dt'); - var id = $dt.attr('id'); - if (id === undefined) { - return; - } - - var $functionsUL = $sidebarItem.siblings('[data-sectionID='+sectionID+']'); - if (!$functionsUL.length) { - $functionsUL = $('