diff --git a/.appveyor.yml b/.appveyor.yml index 0f5dea9c5..b0740b1ac 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -6,6 +6,7 @@ init: # Uncomment previous line to get RDP access during the build. environment: + COVERAGE_CORE: sysmon EXECUTABLE: python.exe TEST_OPTIONS: DEPLOY: YES @@ -14,7 +15,7 @@ environment: ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python38-x64 - ARCHITECTURE: x64 + ARCHITECTURE: AMD64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index dd61634cd..ccd6d87ed 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==2.16.2 +cibuildwheel==2.16.5 diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt new file mode 100644 index 000000000..ed3269460 --- /dev/null +++ b/.ci/requirements-mypy.txt @@ -0,0 +1 @@ +mypy==1.7.1 diff --git a/.coveragerc b/.coveragerc index f71b6b1a2..018cc1cbf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,15 +2,19 @@ [report] # Regexes for lines to exclude from consideration -exclude_lines = - # Have to re-enable the standard pragma: - pragma: no cover - - # Don't complain if non-runnable code isn't run: +exclude_also = + # Don't complain if non-runnable code isn't run if 0: if __name__ == .__main__.: # Don't complain about debug code if DEBUG: + # Don't complain about compatibility code for missing optional dependencies + except ImportError + if TYPE_CHECKING: + @abc.abstractmethod + # Empty bodies in protocols or abstract methods + ^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$ + ^\s*\.\.\.(\s*#.*)?$ [run] omit = diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..a2be59c52 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,6 @@ +# Flake8 +8de95676e0fd89f2326b3953488ab66ff29cd2d0 +# Format with Black +53a7e3500437a9fd5826bc04758f7116bd7e52dc +# Format the C code with ClangFormat +46b7e86bab79450ec0a2866c6c0c679afb659d17 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e0e6804bf..8fc6bd0ad 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -tidelift: "pypi/Pillow" +tidelift: "pypi/pillow" diff --git a/.github/problem-matchers/gcc.json b/.github/problem-matchers/gcc.json new file mode 100644 index 000000000..8e2866afe --- /dev/null +++ b/.github/problem-matchers/gcc.json @@ -0,0 +1,18 @@ +{ + "__comment": "Based on vscode-cpptools' Extension/package.json gcc rule", + "problemMatcher": [ + { + "owner": "gcc-problem-matcher", + "pattern": [ + { + "regexp": "^\\s*(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 4d855469a..3711d91f0 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -13,6 +13,8 @@ categories: label: "Removal" - title: "Testing" label: "Testing" + - title: "Type hints" + label: "Type hints" exclude-labels: - "changelog: skip" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9fe345c8a..92e860cb5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,10 +7,12 @@ on: paths: - ".github/workflows/docs.yml" - "docs/**" + - "src/PIL/**" pull_request: paths: - ".github/workflows/docs.yml" - "docs/**" + - "src/PIL/**" workflow_dispatch: permissions: @@ -37,16 +39,26 @@ jobs: with: python-version: "3.x" cache: pip - cache-dependency-path: ".ci/*.sh" + cache-dependency-path: | + ".ci/*.sh" + "pyproject.toml" - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libimagequant + uses: actions/cache@v4 + id: cache-libimagequant + with: + path: ~/cache-libimagequant + key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Install Linux dependencies run: | .ci/install.sh env: GHA_PYTHON_VERSION: "3.x" + GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} - name: Build run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9069fc615..cc4760288 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - name: pre-commit cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pre-commit key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index f41324c4b..28124d7f7 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,7 +2,16 @@ set -e -brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm +brew install \ + freetype \ + ghostscript \ + libimagequant \ + libjpeg \ + libraqm \ + libtiff \ + little-cms2 \ + openjpeg \ + webp export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" # TODO Update condition when cffi supports 3.13 diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 8fc7bd379..a8ddef22c 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -23,6 +23,6 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next release notes as pull requests are merged into "main" - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/system-info.py b/.github/workflows/system-info.py index 57f28c620..9e97b8971 100644 --- a/.github/workflows/system-info.py +++ b/.github/workflows/system-info.py @@ -6,6 +6,7 @@ This sort of info is missing from GitHub Actions. Requested here: https://github.com/actions/virtual-environments/issues/79 """ + from __future__ import annotations import os diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 32ac6f65e..4526b9454 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: @@ -28,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + COVERAGE_CORE: sysmon + jobs: build: runs-on: windows-latest @@ -49,9 +50,8 @@ jobs: uses: actions/checkout@v4 - name: Install Cygwin - uses: cygwin/cygwin-install-action@v4 + uses: egor-tensin/setup-cygwin@v4 with: - platform: x86_64 packages: > gcc-g++ ghostscript @@ -71,6 +71,7 @@ jobs: make netpbm perl + python39=3.9.16-1 python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel @@ -82,13 +83,13 @@ jobs: zlib-devel - name: Add Lapack to PATH - uses: egor-tensin/cleanup-path@v3 + uses: egor-tensin/cleanup-path@v4 with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' - name: Select Python version run: | - ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 + ln -sf c:/tools/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/tools/cygwin/bin/python3 - name: Get latest NumPy version id: latest-numpy @@ -97,7 +98,7 @@ jobs: python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT - name: pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: 'C:\cygwin\home\runneradmin\.cache\pip' key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }} @@ -143,7 +144,7 @@ jobs: bash.exe .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v3.1.5 with: file: ./coverage.xml flags: GHA_Cygwin diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index eb27b4bf7..f40286fe4 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: @@ -103,7 +101,7 @@ jobs: MATRIX_DOCKER: ${{ matrix.docker }} - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v3.1.5 with: flags: GHA_Docker name: ${{ matrix.docker }} diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 115c2e9be..b4e479f12 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: @@ -28,6 +26,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + COVERAGE_CORE: sysmon + jobs: build: runs-on: windows-latest @@ -84,7 +85,7 @@ jobs: python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v3.1.5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 86cd5b5fa..c936be559 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -2,11 +2,12 @@ name: Test Windows on: push: + branches: + - "**" paths-ignore: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -14,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: @@ -26,13 +26,16 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + COVERAGE_CORE: sysmon + jobs: build: runs-on: windows-latest strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-alpha.3"] timeout-minutes: 30 @@ -66,8 +69,16 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py - - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma - run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma + - name: Install Python dependencies + run: > + python3 -m pip install + coverage>=7.4.2 + defusedxml + olefile + pyroma + pytest + pytest-cov + pytest-timeout - name: Install dependencies id: install @@ -89,7 +100,7 @@ jobs: - name: Cache build id: build-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: winbuild\build key: @@ -202,7 +213,7 @@ jobs: shell: pwsh - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v3.1.5 with: file: ./coverage.xml flags: GHA_Windows diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aa0e25138..643273e58 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" pull_request: @@ -16,7 +15,6 @@ on: - ".github/workflows/docs.yml" - ".github/workflows/wheels*" - ".gitmodules" - - ".travis.yml" - "docs/**" - "wheels/**" workflow_dispatch: @@ -28,6 +26,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + COVERAGE_CORE: sysmon + FORCE_COLOR: 1 + jobs: build: @@ -35,7 +37,7 @@ jobs: fail-fast: false matrix: os: [ - "macos-latest", + "macos-14", "ubuntu-latest", ] python-version: [ @@ -49,11 +51,21 @@ jobs: "3.8", ] include: - - python-version: "3.9" + - python-version: "3.11" PYTHONOPTIMIZE: 1 REVERSE: "--reverse" - - python-version: "3.8" + - python-version: "3.10" PYTHONOPTIMIZE: 2 + # M1 only available for 3.10+ + - os: "macos-latest" + python-version: "3.9" + - os: "macos-latest" + python-version: "3.8" + exclude: + - os: "macos-14" + python-version: "3.9" + - os: "macos-14" + python-version: "3.8" runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} @@ -67,17 +79,28 @@ jobs: python-version: ${{ matrix.python-version }} allow-prereleases: true cache: pip - cache-dependency-path: ".ci/*.sh" + cache-dependency-path: | + ".ci/*.sh" + "pyproject.toml" - name: Build system information run: python3 .github/workflows/system-info.py + - name: Cache libimagequant + if: startsWith(matrix.os, 'ubuntu') + uses: actions/cache@v4 + id: cache-libimagequant + with: + path: ~/cache-libimagequant + key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }} + - name: Install Linux dependencies if: startsWith(matrix.os, 'ubuntu') run: | .ci/install.sh env: GHA_PYTHON_VERSION: ${{ matrix.python-version }} + GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }} - name: Install macOS dependencies if: startsWith(matrix.os, 'macOS') @@ -86,6 +109,10 @@ jobs: env: GHA_PYTHON_VERSION: ${{ matrix.python-version }} + - name: Register gcc problem matcher + if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'" + run: echo "::add-matcher::.github/problem-matchers/gcc.json" + - name: Build run: | .ci/build.sh @@ -123,9 +150,9 @@ jobs: .ci/after_success.sh - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v3.1.5 with: - flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} + flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} gcov: true diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 3ec314873..26bf2f6d6 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -72,13 +72,11 @@ function build { build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto if [ -n "$IS_MACOS" ]; then - if [[ "$CIBW_ARCHS" == "arm64" ]]; then - build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto - build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib - build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist - if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then - cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc - fi + build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto + build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib + build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist + if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then + cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc fi else sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc @@ -131,13 +129,13 @@ untar pillow-depends-main.zip if [[ -n "$IS_MACOS" ]]; then # webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb - # libxdmcp causes an issue on macOS < 11 + # libxau and libxdmcp cause an issue on macOS < 11 # if php is installed, brew tries to reinstall these after installing openblas # remove cairo to fix building harfbuzz on arm64 # remove lcms2 and libpng to fix building openjpeg on arm64 # remove zstd to avoid inclusion on x86_64 # curl from brew requires zstd, use system curl - brew remove --ignore-dependencies webp libpng libtiff libxcb libxdmcp curl php cairo lcms2 ghostscript zstd + brew remove --ignore-dependencies webp libpng libtiff libxcb libxau libxdmcp curl php cairo lcms2 ghostscript zstd brew install pkg-config fi diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 76d42b470..1140aaaad 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -30,7 +30,64 @@ env: FORCE_COLOR: 1 jobs: - build: + build-1-QEMU-emulated-wheels: + name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - pp39 + - pp310 + - cp38 + - cp39 + - cp310 + - cp311 + - cp312 + spec: + - manylinux2014 + - manylinux_2_28 + - musllinux + exclude: + - { python-version: pp39, spec: musllinux } + - { python-version: pp310, spec: musllinux } + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Install cibuildwheel + run: | + python3 -m pip install -r .ci/requirements-cibw.txt + + - name: Build wheels + run: | + python3 -m cibuildwheel --output-dir wheelhouse + env: + # Build only the currently selected Linux architecture (so we can + # parallelise for speed). + CIBW_ARCHS: "aarch64" + # Likewise, select only one Python version per job to speed this up. + CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*" + # Extra options for manylinux. + CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }} + CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }} + + - uses: actions/upload-artifact@v4 + with: + name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }} + path: ./wheelhouse/*.whl + + build-2-native-wheels: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: @@ -39,18 +96,18 @@ jobs: include: - name: "macOS x86_64" os: macos-latest - archs: x86_64 + cibw_arch: x86_64 macosx_deployment_target: "10.10" - name: "macOS arm64" os: macos-latest - archs: arm64 + cibw_arch: arm64 macosx_deployment_target: "11.0" - name: "manylinux2014 and musllinux x86_64" os: ubuntu-latest - archs: x86_64 + cibw_arch: x86_64 - name: "manylinux_2_28 x86_64" os: ubuntu-latest - archs: x86_64 + cibw_arch: x86_64 build: "*manylinux*" manylinux: "manylinux_2_28" steps: @@ -62,12 +119,15 @@ jobs: with: python-version: "3.x" - - name: Build wheels + - name: Install cibuildwheel run: | python3 -m pip install -r .ci/requirements-cibw.txt + + - name: Build wheels + run: | python3 -m cibuildwheel --output-dir wheelhouse env: - CIBW_ARCHS: ${{ matrix.archs }} + CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BUILD: ${{ matrix.build }} CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }} CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }} @@ -75,24 +135,21 @@ jobs: CIBW_TEST_SKIP: "*-macosx_arm64" MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: - name: dist + name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }} path: ./wheelhouse/*.whl windows: - name: Windows ${{ matrix.arch }} + name: Windows ${{ matrix.cibw_arch }} runs-on: windows-latest strategy: fail-fast: false matrix: include: - - arch: x86 - cibw_arch: x86 - - arch: x64 - cibw_arch: AMD64 - - arch: ARM64 - cibw_arch: ARM64 + - cibw_arch: x86 + - cibw_arch: AMD64 + - cibw_arch: ARM64 steps: - uses: actions/checkout@v4 @@ -106,6 +163,10 @@ jobs: with: python-version: "3.x" + - name: Install cibuildwheel + run: | + python.exe -m pip install -r .ci/requirements-cibw.txt + - name: Prepare for build run: | choco install nasm --no-progress @@ -114,12 +175,7 @@ jobs: # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images - & python.exe -m pip install -r .ci/requirements-cibw.txt - - # Cannot cross-compile FriBiDi (only used for tests) - $FLAGS = ("--no-imagequant", "--architecture=${{ matrix.arch }}") - if ('${{ matrix.arch }}' -eq 'ARM64') { $FLAGS += "--no-fribidi" } - & python.exe winbuild\build_prepare.py -v @FLAGS + & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} shell: pwsh - name: Build wheels @@ -146,6 +202,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_arch }} CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd" CIBW_CACHE_PATH: "C:\\cibw" + CIBW_SKIP: pp38-* CIBW_TEST_SKIP: "*-win_arm64" CIBW_TEST_COMMAND: 'docker run --rm -v {project}:C:\pillow @@ -157,24 +214,16 @@ jobs: shell: cmd - name: Upload wheels - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: dist + name: dist-windows-${{ matrix.cibw_arch }} path: ./wheelhouse/*.whl - - name: Prepare to upload FriBiDi - if: "matrix.arch != 'ARM64'" - run: | - mkdir fribidi\${{ matrix.arch }} - copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }} - shell: cmd - - name: Upload fribidi.dll - if: "matrix.arch != 'ARM64'" - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: fribidi - path: fribidi\* + name: fribidi-windows-${{ matrix.cibw_arch }} + path: winbuild\build\bin\fribidi* sdist: runs-on: ubuntu-latest @@ -190,17 +239,26 @@ jobs: - run: make sdist - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: - name: dist + name: dist-sdist path: dist/*.tar.gz - success: - permissions: - contents: none - needs: [build, windows, sdist] + pypi-publish: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist] runs-on: ubuntu-latest - name: Wheels Successful + name: Upload release to PyPI + environment: + name: release-pypi + url: https://pypi.org/p/Pillow + permissions: + id-token: write steps: - - name: Success - run: echo Wheels Successful + - uses: actions/download-artifact@v4 + with: + pattern: dist-* + path: dist + merge-multiple: true + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d1c4b8015..c52fdcb55 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.7 + rev: v0.2.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.0 + rev: 24.1.1 hooks: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.7.6 + rev: 1.7.7 hooks: - id: bandit args: [--severity-level=high] @@ -32,6 +32,7 @@ repos: rev: v4.5.0 hooks: - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable - id: check-merge-conflict - id: check-json - id: check-toml @@ -47,12 +48,12 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.5.3 + rev: 1.7.0 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.15 + rev: v0.16 hooks: - id: validate-pyproject diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8f8250809..000000000 --- a/.travis.yml +++ /dev/null @@ -1,52 +0,0 @@ -if: tag IS present OR type = api - -env: - global: - - CIBW_ARCHS=aarch64 - - CIBW_SKIP=pp38-* - -language: python -# Default Python version is usually 3.6 -python: "3.12" -dist: jammy -services: docker - -jobs: - include: - - name: "manylinux2014 aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*manylinux*" - - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux2014 - - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux2014 - - name: "manylinux_2_28 aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*manylinux*" - - CIBW_MANYLINUX_AARCH64_IMAGE=manylinux_2_28 - - CIBW_MANYLINUX_PYPY_AARCH64_IMAGE=manylinux_2_28 - - name: "musllinux aarch64" - os: linux - arch: arm64 - env: - - CIBW_BUILD="*musllinux*" - -install: - - python3 -m pip install -r .ci/requirements-cibw.txt - -script: - - python3 -m cibuildwheel --output-dir wheelhouse - - ls -l "${TRAVIS_BUILD_DIR}/wheelhouse/" - -# Upload wheels to GitHub Releases -deploy: - provider: releases - api_key: $GITHUB_RELEASE_TOKEN - file_glob: true - file: "${TRAVIS_BUILD_DIR}/wheelhouse/*.whl" - on: - repo: python-pillow/Pillow - tags: true - skip_cleanup: true diff --git a/CHANGES.rst b/CHANGES.rst index f69c3ffa7..205ffa294 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,90 @@ Changelog (Pillow) ================== -10.2.0 (unreleased) +10.3.0 (unreleased) ------------------- +- Release GIL while calling ``WebPAnimDecoderGetNext`` #7782 + [evanmiller, radarhere] + +- Fixed reading FLI/FLC images with a prefix chunk #7804 + [twolife] + +- Update wl-paste handling and return None for some errors in grabclipboard() on Linux #7745 + [nik012003, radarhere] + +- Remove execute bit from ``setup.py`` #7760 + [hugovk] + +- Do not support using test-image-results to upload images after test failures #7739 + [radarhere] + +- Changed ImageMath.ops to be static #7721 + [radarhere] + +- Fix APNG info after seeking backwards more than twice #7701 + [esoma, radarhere] + +- Deprecate ImageCms constants and versions() function #7702 + [nulano, radarhere] + +- Added PerspectiveTransform #7699 + [radarhere] + +- Add support for reading and writing grayscale PFM images #7696 + [nulano, hugovk] + +- Add LCMS2 flags to ImageCms #7676 + [nulano, radarhere, hugovk] + +- Rename x64 to AMD64 in winbuild #7693 + [nulano] + +10.2.0 (2024-01-02) +------------------- + +- Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553 + [bgilbert, radarhere] + +- Trim glyph size in ImageFont.getmask() #7669, #7672 + [radarhere, nulano] + +- Deprecate IptcImagePlugin helpers #7664 + [nulano, hugovk, radarhere] + +- Allow uncompressed TIFF images to be saved in chunks #7650 + [radarhere] + +- Concatenate multiple JPEG EXIF markers #7496 + [radarhere] + +- Changed IPTC tile tuple to match other plugins #7661 + [radarhere] + +- Do not assign new fp attribute when exiting context manager #7566 + [radarhere] + +- Support arbitrary masks for uncompressed RGB DDS images #7589 + [radarhere, akx] + +- Support setting ROWSPERSTRIP tag #7654 + [radarhere] + +- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662 + [radarhere] + +- Optimise ``ImageColor`` using ``functools.lru_cache`` #7657 + [hugovk] + +- Restricted environment keys for ImageMath.eval() #7655 + [wiredfool, radarhere] + +- Optimise ``ImageMode.getmode`` using ``functools.lru_cache`` #7641 + [hugovk, radarhere] + +- Fix incorrect color blending for overlapping glyphs #7497 + [ZachNagengast, nulano, radarhere] + - Attempt memory mapping when tile args is a string #7565 [radarhere] diff --git a/LICENSE b/LICENSE index cf65e86d7..0069eb5bc 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-2023 by Jeffrey A. Clark (Alex) and contributors. + Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors. Like PIL, Pillow is licensed under the open source HPND License: diff --git a/README.md b/README.md index e11bd2faa..9776c40e2 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,6 @@ As of 2019, Pillow development is GitHub Actions build status (Wheels) - Travis CI wheels build status (aarch64) Code coverage @@ -67,11 +64,11 @@ As of 2019, Pillow development is src="https://zenodo.org/badge/17549/python-pillow/Pillow.svg"> Tidelift - + Newest PyPI version - Number of PyPI downloads None: (w, h) = size for x in range(w): for y in range(h): access[(x, y)] -def iterate_set(size, access): +def iterate_set(size, access) -> None: (w, h) = size for x in range(w): for y in range(h): access[(x, y)] = (x % 256, y % 256, 0) -def timer(func, label, *args): +def timer(func, label, *args) -> None: iterations = 5000 starttime = time.time() for x in range(iterations): @@ -37,7 +38,7 @@ def timer(func, label, *args): ) -def test_direct(): +def test_direct() -> None: im = hopper() im.load() # im = Image.new("RGB", (2000, 2000), (1, 3, 2)) diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py index ac46ff1eb..e0057a2c2 100644 --- a/Tests/check_fli_oob.py +++ b/Tests/check_fli_oob.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 from __future__ import annotations from PIL import Image diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 0fabcb5d3..5c89efc76 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -1,10 +1,11 @@ from __future__ import annotations + from PIL import Image TEST_FILE = "Tests/images/fli_overflow.fli" -def test_fli_overflow(): +def test_fli_overflow() -> None: # this should not crash with a malloc error or access violation with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 8c17c051d..890167039 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3 from __future__ import annotations + +from typing import Any, Callable + import pytest from PIL import Image @@ -12,31 +15,34 @@ max_iterations = 10000 pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") -def _get_mem_usage(): +def _get_mem_usage() -> float: from resource import RUSAGE_SELF, getpagesize, getrusage mem = getrusage(RUSAGE_SELF).ru_maxrss return mem * getpagesize() / 1024 / 1024 -def _test_leak(min_iterations, max_iterations, fn, *args, **kwargs): +def _test_leak( + min_iterations: int, max_iterations: int, fn: Callable[..., None], *args: Any +) -> None: mem_limit = None for i in range(max_iterations): - fn(*args, **kwargs) + fn(*args) mem = _get_mem_usage() if i < min_iterations: mem_limit = mem + 1 continue msg = f"memory usage limit exceeded after {i + 1} iterations" + assert mem_limit is not None assert mem <= mem_limit, msg -def test_leak_putdata(): +def test_leak_putdata() -> None: im = Image.new("RGB", (25, 25)) _test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) -def test_leak_getlist(): +def test_leak_getlist() -> None: im = Image.new("P", (25, 25)) _test_leak( min_iterations, diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py index 83a12e2c2..bbe35b591 100644 --- a/Tests/check_j2k_leaks.py +++ b/Tests/check_j2k_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest @@ -19,7 +20,7 @@ pytestmark = [ ] -def test_leak_load(): +def test_leak_load() -> None: from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit setrlimit(RLIMIT_STACK, (stack_size, stack_size)) @@ -29,7 +30,7 @@ def test_leak_load(): im.load() -def test_leak_save(): +def test_leak_save() -> None: from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit setrlimit(RLIMIT_STACK, (stack_size, stack_size)) diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index 982f6ea74..dbdd5a4f5 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -1,10 +1,13 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image -def test_j2k_overflow(tmp_path): +def test_j2k_overflow(tmp_path: Path) -> None: im = Image.new("RGBA", (1024, 131584)) target = str(tmp_path / "temp.jpc") with pytest.raises(OSError): diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py old mode 100755 new mode 100644 index 9afbff112..954d68bf7 --- a/Tests/check_jp2_overflow.py +++ b/Tests/check_jp2_overflow.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Reproductions/tests for OOB read errors in FliDecode.c # When run in python, all of these images should fail for @@ -14,7 +12,6 @@ # version. from __future__ import annotations - from PIL import Image repro = ("00r0_gray_l.jp2", "00r1_graya_la.jp2") diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 3cd37c7af..5f290c6cd 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest @@ -110,14 +111,14 @@ standard_chrominance_qtable = ( [standard_l_qtable, standard_chrominance_qtable], ), ) -def test_qtables_leak(qtables): +def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None: im = hopper("RGB") for _ in range(iterations): test_output = BytesIO() im.save(test_output, "JPEG", qtables=qtables) -def test_exif_leak(): +def test_exif_leak() -> None: """ pre patch: @@ -180,7 +181,7 @@ def test_exif_leak(): im.save(test_output, "JPEG", exif=exif) -def test_base_save(): +def test_base_save() -> None: """ base case: MB diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 9b83798d5..a9ce79e57 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,5 +1,8 @@ from __future__ import annotations + import sys +from pathlib import Path +from types import ModuleType import pytest @@ -15,6 +18,7 @@ from PIL import Image # 2.7 and 3.2. +numpy: ModuleType | None try: import numpy except ImportError: @@ -27,23 +31,24 @@ XDIM = 48000 pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") -def _write_png(tmp_path, xdim, ydim): +def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: f = str(tmp_path / "temp.png") im = Image.new("L", (xdim, ydim), 0) im.save(f) -def test_large(tmp_path): +def test_large(tmp_path: Path) -> None: """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) -def test_2gpx(tmp_path): +def test_2gpx(tmp_path: Path) -> None: """failed prepatch""" _write_png(tmp_path, XDIM, XDIM) @pytest.mark.skipif(numpy is None, reason="Numpy is not installed") -def test_size_greater_than_int(): +def test_size_greater_than_int() -> None: + assert numpy is not None arr = numpy.ndarray(shape=(16394, 16394)) Image.fromarray(arr) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 0ff3de8dc..f4ca8d0aa 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,5 +1,7 @@ from __future__ import annotations + import sys +from pathlib import Path import pytest @@ -23,7 +25,7 @@ XDIM = 48000 pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") -def _write_png(tmp_path, xdim, ydim): +def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) f = str(tmp_path / "temp.png") @@ -31,11 +33,11 @@ def _write_png(tmp_path, xdim, ydim): im.save(f) -def test_large(tmp_path): +def test_large(tmp_path: Path) -> None: """succeeded prepatch""" _write_png(tmp_path, XDIM, YDIM) -def test_2gpx(tmp_path): +def test_2gpx(tmp_path: Path) -> None: """failed prepatch""" _write_png(tmp_path, XDIM, XDIM) diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index ee1d7d11f..84bda53ed 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image @@ -6,7 +7,7 @@ from PIL import Image TEST_FILE = "Tests/images/libtiff_segfault.tif" -def test_libtiff_segfault(): +def test_libtiff_segfault() -> None: """This test should not segfault. It will on Pillow <= 3.1.0 and libtiff >= 4.0.0 """ diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 292fe4b7f..d65ba6abc 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -1,4 +1,5 @@ from __future__ import annotations + import zlib from io import BytesIO @@ -7,7 +8,7 @@ from PIL import Image, ImageFile, PngImagePlugin TEST_FILE = "Tests/images/png_decompression_dos.png" -def test_ignore_dos_text(): +def test_ignore_dos_text() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -23,7 +24,7 @@ def test_ignore_dos_text(): assert len(s) < 1024 * 1024, "Text chunk larger than 1M" -def test_dos_text(): +def test_dos_text() -> None: try: im = Image.open(TEST_FILE) im.load() @@ -35,7 +36,7 @@ def test_dos_text(): assert len(s) < 1024 * 1024, "Text chunk larger than 1M" -def test_dos_total_memory(): +def test_dos_total_memory() -> None: im = Image.new("L", (1, 1)) compressed_data = zlib.compress(b"a" * 1024 * 1023) @@ -52,7 +53,7 @@ def test_dos_total_memory(): try: im2 = Image.open(b) except ValueError as msg: - assert "Too much memory" in msg + assert "Too much memory" in str(msg) return total_len = 0 diff --git a/Tests/check_release_notes.py b/Tests/check_release_notes.py index ebfaffa47..cf414d7ff 100644 --- a/Tests/check_release_notes.py +++ b/Tests/check_release_notes.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from pathlib import Path diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index afe4cc3ee..4b91984f5 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -1,10 +1,11 @@ from __future__ import annotations + import sys from PIL import features -def test_wheel_modules(): +def test_wheel_modules() -> None: expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} # tkinter is not available in cibuildwheel installed CPython on Windows @@ -18,13 +19,13 @@ def test_wheel_modules(): assert set(features.get_supported_modules()) == expected_modules -def test_wheel_codecs(): +def test_wheel_codecs() -> None: expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"} assert set(features.get_supported_codecs()) == expected_codecs -def test_wheel_features(): +def test_wheel_features() -> None: expected_features = { "webp_anim", "webp_mux", diff --git a/Tests/conftest.py b/Tests/conftest.py index cd64bd755..e00d1f019 100644 --- a/Tests/conftest.py +++ b/Tests/conftest.py @@ -1,8 +1,11 @@ from __future__ import annotations + import io +import pytest -def pytest_report_header(config): + +def pytest_report_header(config: pytest.Config) -> str: try: from PIL import features @@ -13,7 +16,7 @@ def pytest_report_header(config): return f"pytest_report_header failed: {e}" -def pytest_configure(config): +def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line( "markers", "pil_noop_mark: A conditional mark where nothing special happens", diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index 2e990b709..41c76f87e 100755 --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 from __future__ import annotations + import base64 import os diff --git a/Tests/fonts/CBDTTestFont.ttf b/Tests/fonts/CBDTTestFont.ttf new file mode 100644 index 000000000..73444e8dc Binary files /dev/null and b/Tests/fonts/CBDTTestFont.ttf differ diff --git a/Tests/fonts/EBDTTestFont.ttf b/Tests/fonts/EBDTTestFont.ttf new file mode 100644 index 000000000..046e9e45c Binary files /dev/null and b/Tests/fonts/EBDTTestFont.ttf differ diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt index da559b3d3..3c8a23197 100644 --- a/Tests/fonts/LICENSE.txt +++ b/Tests/fonts/LICENSE.txt @@ -2,7 +2,6 @@ NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts NotoSans-Regular.ttf, from https://www.google.com/get/noto/ NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/ -NotoColorEmoji.ttf, from https://github.com/googlefonts/noto-emoji AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa @@ -25,3 +24,5 @@ FreeMono.ttf is licensed under GPLv3. 10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base "Public domain font. Share and enjoy." + +CBDTTestFont.ttf and EBDTTestFont.ttf from https://github.com/nulano/font-tests are public domain. diff --git a/Tests/fonts/NotoColorEmoji.ttf b/Tests/fonts/NotoColorEmoji.ttf deleted file mode 100644 index ef7b72575..000000000 Binary files a/Tests/fonts/NotoColorEmoji.ttf and /dev/null differ diff --git a/Tests/helper.py b/Tests/helper.py index b333c2fd4..b98883946 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -1,6 +1,7 @@ """ Helper functions. """ + from __future__ import annotations import logging @@ -11,6 +12,7 @@ import sys import sysconfig import tempfile from io import BytesIO +from typing import Any, Callable, Sequence import pytest from packaging.version import parse as parse_version @@ -19,42 +21,31 @@ from PIL import Image, ImageMath, features logger = logging.getLogger(__name__) - -HAS_UPLOADER = False - +uploader = None if os.environ.get("SHOW_ERRORS"): - # local img.show for errors. - HAS_UPLOADER = True - - class test_image_results: - @staticmethod - def upload(a, b): - a.show() - b.show() - + uploader = "show" elif "GITHUB_ACTIONS" in os.environ: - HAS_UPLOADER = True - - class test_image_results: - @staticmethod - def upload(a, b): - dir_errors = os.path.join(os.path.dirname(__file__), "errors") - os.makedirs(dir_errors, exist_ok=True) - tmpdir = tempfile.mkdtemp(dir=dir_errors) - a.save(os.path.join(tmpdir, "a.png")) - b.save(os.path.join(tmpdir, "b.png")) - return tmpdir - -else: - try: - import test_image_results - - HAS_UPLOADER = True - except ImportError: - pass + uploader = "github_actions" -def convert_to_comparable(a, b): +def upload(a: Image.Image, b: Image.Image) -> str | None: + if uploader == "show": + # local img.show for errors. + a.show() + b.show() + elif uploader == "github_actions": + dir_errors = os.path.join(os.path.dirname(__file__), "errors") + os.makedirs(dir_errors, exist_ok=True) + tmpdir = tempfile.mkdtemp(dir=dir_errors) + a.save(os.path.join(tmpdir, "a.png")) + b.save(os.path.join(tmpdir, "b.png")) + return tmpdir + return None + + +def convert_to_comparable( + a: Image.Image, b: Image.Image +) -> tuple[Image.Image, Image.Image]: new_a, new_b = a, b if a.mode == "P": new_a = Image.new("L", a.size) @@ -67,14 +58,18 @@ def convert_to_comparable(a, b): return new_a, new_b -def assert_deep_equal(a, b, msg=None): +def assert_deep_equal( + a: Sequence[Any], b: Sequence[Any], msg: str | None = None +) -> None: try: assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" except Exception: assert a == b, msg -def assert_image(im, mode, size, msg=None): +def assert_image( + im: Image.Image, mode: str, size: tuple[int, int], msg: str | None = None +) -> None: if mode is not None: assert im.mode == mode, ( msg or f"got mode {repr(im.mode)}, expected {repr(mode)}" @@ -86,28 +81,32 @@ def assert_image(im, mode, size, msg=None): ) -def assert_image_equal(a, b, msg=None): +def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -> None: assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" if a.tobytes() != b.tobytes(): - if HAS_UPLOADER: - try: - url = test_image_results.upload(a, b) + try: + url = upload(a, b) + if url: logger.error("URL for test images: %s", url) - except Exception: - pass + except Exception: + pass pytest.fail(msg or "got different content") -def assert_image_equal_tofile(a, filename, msg=None, mode=None): +def assert_image_equal_tofile( + a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None +) -> None: with Image.open(filename) as img: if mode: img = img.convert(mode) assert_image_equal(a, img, msg) -def assert_image_similar(a, b, epsilon, msg=None): +def assert_image_similar( + a: Image.Image, b: Image.Image, epsilon: float, msg: str | None = None +) -> None: assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" @@ -125,55 +124,68 @@ def assert_image_similar(a, b, epsilon, msg=None): + f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}" ) except Exception as e: - if HAS_UPLOADER: - try: - url = test_image_results.upload(a, b) + try: + url = upload(a, b) + if url: logger.exception("URL for test images: %s", url) - except Exception: - pass + except Exception: + pass raise e -def assert_image_similar_tofile(a, filename, epsilon, msg=None, mode=None): +def assert_image_similar_tofile( + a: Image.Image, + filename: str, + epsilon: float, + msg: str | None = None, + mode: str | None = None, +) -> None: with Image.open(filename) as img: if mode: img = img.convert(mode) assert_image_similar(a, img, epsilon, msg) -def assert_all_same(items, msg=None): +def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None: assert items.count(items[0]) == len(items), msg -def assert_not_all_same(items, msg=None): +def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None: assert items.count(items[0]) != len(items), msg -def assert_tuple_approx_equal(actuals, targets, threshold, msg): +def assert_tuple_approx_equal( + actuals: Sequence[int], targets: tuple[int, ...], threshold: int, msg: str +) -> None: """Tests if actuals has values within threshold from targets""" - value = True for i, target in enumerate(targets): - value *= target - threshold <= actuals[i] <= target + threshold - - assert value, msg + ": " + repr(actuals) + " != " + repr(targets) + if not (target - threshold <= actuals[i] <= target + threshold): + pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets)) -def skip_unless_feature(feature): +def skip_unless_feature(feature: str) -> pytest.MarkDecorator: reason = f"{feature} not available" return pytest.mark.skipif(not features.check(feature), reason=reason) -def skip_unless_feature_version(feature, version_required, reason=None): +def skip_unless_feature_version( + feature: str, required: str, reason: str | None = None +) -> pytest.MarkDecorator: if not features.check(feature): return pytest.mark.skip(f"{feature} not available") if reason is None: - reason = f"{feature} is older than {version_required}" - version_required = parse_version(version_required) + reason = f"{feature} is older than {required}" + version_required = parse_version(required) version_available = parse_version(features.version(feature)) return pytest.mark.skipif(version_available < version_required, reason=reason) -def mark_if_feature_version(mark, feature, version_blacklist, reason=None): +def mark_if_feature_version( + mark: pytest.MarkDecorator, + feature: str, + version_blacklist: str, + reason: str | None = None, +) -> pytest.MarkDecorator: if not features.check(feature): return pytest.mark.pil_noop_mark() if reason is None: @@ -194,7 +206,7 @@ class PillowLeakTestCase: iterations = 100 # count mem_limit = 512 # k - def _get_mem_usage(self): + def _get_mem_usage(self) -> float: """ Gets the RUSAGE memory usage, returns in K. Encapsulates the difference between macOS and Linux rss reporting @@ -216,7 +228,7 @@ class PillowLeakTestCase: # This is the maximum resident set size used (in kilobytes). return mem # Kb - def _test_leak(self, core): + def _test_leak(self, core: Callable[[], None]) -> None: start_mem = self._get_mem_usage() for cycle in range(self.iterations): core() @@ -228,17 +240,17 @@ class PillowLeakTestCase: # helpers -def fromstring(data): +def fromstring(data: bytes) -> Image.Image: return Image.open(BytesIO(data)) -def tostring(im, string_format, **options): +def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes: out = BytesIO() im.save(out, string_format, **options) return out.getvalue() -def hopper(mode=None, cache={}): +def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image: if mode is None: # Always return fresh not-yet-loaded version of image. # Operations on not-yet-loaded images is separate class of errors @@ -259,29 +271,31 @@ def hopper(mode=None, cache={}): return im.copy() -def djpeg_available(): +def djpeg_available() -> bool: if shutil.which("djpeg"): try: subprocess.check_call(["djpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover return False + return False -def cjpeg_available(): +def cjpeg_available() -> bool: if shutil.which("cjpeg"): try: subprocess.check_call(["cjpeg", "-version"]) return True except subprocess.CalledProcessError: # pragma: no cover return False + return False -def netpbm_available(): +def netpbm_available() -> bool: return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) -def magick_command(): +def magick_command() -> list[str] | None: if sys.platform == "win32": magickhome = os.environ.get("MAGICK_HOME") if magickhome: @@ -298,47 +312,48 @@ def magick_command(): return imagemagick if graphicsmagick and shutil.which(graphicsmagick[0]): return graphicsmagick + return None -def on_appveyor(): +def on_appveyor() -> bool: return "APPVEYOR" in os.environ -def on_github_actions(): +def on_github_actions() -> bool: return "GITHUB_ACTIONS" in os.environ -def on_ci(): +def on_ci() -> bool: # GitHub Actions and AppVeyor have "CI" return "CI" in os.environ -def is_big_endian(): +def is_big_endian() -> bool: return sys.byteorder == "big" -def is_ppc64le(): +def is_ppc64le() -> bool: import platform return platform.machine() == "ppc64le" -def is_win32(): +def is_win32() -> bool: return sys.platform.startswith("win32") -def is_pypy(): +def is_pypy() -> bool: return hasattr(sys, "pypy_translation_info") -def is_mingw(): +def is_mingw() -> bool: return sysconfig.get_platform() == "mingw" class CachedProperty: - def __init__(self, func): + def __init__(self, func: Callable[[Any], None]) -> None: self.func = func - def __get__(self, instance, cls=None): + def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any: result = instance.__dict__[self.func.__name__] = self.func(instance) return result diff --git a/Tests/images/2422.flc b/Tests/images/2422.flc new file mode 100644 index 000000000..eed5fb59e Binary files /dev/null and b/Tests/images/2422.flc differ diff --git a/Tests/images/apng/different_durations.png b/Tests/images/apng/different_durations.png new file mode 100644 index 000000000..984254b8e Binary files /dev/null and b/Tests/images/apng/different_durations.png differ diff --git a/Tests/images/bgr15.dds b/Tests/images/bgr15.dds new file mode 100644 index 000000000..ba3bbddca Binary files /dev/null and b/Tests/images/bgr15.dds differ diff --git a/Tests/images/bgr15.png b/Tests/images/bgr15.png new file mode 100644 index 000000000..a15ab5ad2 Binary files /dev/null and b/Tests/images/bgr15.png differ diff --git a/Tests/images/bitmap_font_blend.png b/Tests/images/bitmap_font_blend.png new file mode 100644 index 000000000..a5acf3667 Binary files /dev/null and b/Tests/images/bitmap_font_blend.png differ diff --git a/Tests/images/bitmap_font_stroke_basic.png b/Tests/images/bitmap_font_stroke_basic.png index 86b2d09f6..26aa3ab8e 100644 Binary files a/Tests/images/bitmap_font_stroke_basic.png 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 index 08029ce34..be273d7cb 100644 Binary files a/Tests/images/bitmap_font_stroke_raqm.png and b/Tests/images/bitmap_font_stroke_raqm.png differ diff --git a/Tests/images/cbdt.png b/Tests/images/cbdt.png new file mode 100644 index 000000000..542bb812e Binary files /dev/null and b/Tests/images/cbdt.png differ diff --git a/Tests/images/cbdt_mask.png b/Tests/images/cbdt_mask.png new file mode 100644 index 000000000..b0854a605 Binary files /dev/null and b/Tests/images/cbdt_mask.png differ diff --git a/Tests/images/cbdt_notocoloremoji.png b/Tests/images/cbdt_notocoloremoji.png deleted file mode 100644 index 1da12fba1..000000000 Binary files a/Tests/images/cbdt_notocoloremoji.png and /dev/null differ diff --git a/Tests/images/cbdt_notocoloremoji_mask.png b/Tests/images/cbdt_notocoloremoji_mask.png deleted file mode 100644 index 6d036a0b6..000000000 Binary files a/Tests/images/cbdt_notocoloremoji_mask.png and /dev/null differ diff --git a/Tests/images/create_eps.gnuplot b/Tests/images/create_eps.gnuplot index 4d7e29877..57a3c8c97 100644 --- a/Tests/images/create_eps.gnuplot +++ b/Tests/images/create_eps.gnuplot @@ -1,5 +1,3 @@ -#!/usr/bin/gnuplot - #This is the script that was used to create our sample EPS files #We used the following version of the gnuplot program #G N U P L O T diff --git a/Tests/images/default_font_freetype.png b/Tests/images/default_font_freetype.png index e00bb5d85..bc1654a25 100644 Binary files a/Tests/images/default_font_freetype.png and b/Tests/images/default_font_freetype.png differ diff --git a/Tests/images/hopper.pfm b/Tests/images/hopper.pfm new file mode 100644 index 000000000..b57661564 Binary files /dev/null and b/Tests/images/hopper.pfm differ diff --git a/Tests/images/hopper_be.pfm b/Tests/images/hopper_be.pfm new file mode 100644 index 000000000..93c75e26f Binary files /dev/null and b/Tests/images/hopper_be.pfm differ diff --git a/Tests/images/multiple_exif.jpg b/Tests/images/multiple_exif.jpg new file mode 100644 index 000000000..32e0aa301 Binary files /dev/null and b/Tests/images/multiple_exif.jpg differ diff --git a/Tests/images/test_combine_caron_below_ttb.png b/Tests/images/test_combine_caron_below_ttb.png index 5c7576de0..2b7cc89ea 100644 Binary files a/Tests/images/test_combine_caron_below_ttb.png and b/Tests/images/test_combine_caron_below_ttb.png differ diff --git a/Tests/images/test_combine_caron_below_ttb_lb.png b/Tests/images/test_combine_caron_below_ttb_lb.png index bacd6a141..3ced2dbfc 100644 Binary files a/Tests/images/test_combine_caron_below_ttb_lb.png and b/Tests/images/test_combine_caron_below_ttb_lb.png differ diff --git a/Tests/images/test_combine_caron_ttb.png b/Tests/images/test_combine_caron_ttb.png index a94be2f0a..569cc1ec3 100644 Binary files a/Tests/images/test_combine_caron_ttb.png and b/Tests/images/test_combine_caron_ttb.png differ diff --git a/Tests/images/test_combine_caron_ttb_lt.png b/Tests/images/test_combine_caron_ttb_lt.png index a94be2f0a..569cc1ec3 100644 Binary files a/Tests/images/test_combine_caron_ttb_lt.png and b/Tests/images/test_combine_caron_ttb_lt.png differ diff --git a/Tests/images/unsupported_bitcount_luminance.dds b/Tests/images/unsupported_bitcount.dds similarity index 100% rename from Tests/images/unsupported_bitcount_luminance.dds rename to Tests/images/unsupported_bitcount.dds diff --git a/Tests/images/unsupported_bitcount_rgb.dds b/Tests/images/unsupported_bitcount_rgb.dds deleted file mode 100644 index 77d527507..000000000 Binary files a/Tests/images/unsupported_bitcount_rgb.dds and /dev/null differ diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index 4e7c7deec..8788d7021 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -13,7 +13,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations import atheris @@ -24,7 +23,7 @@ with atheris.instrument_imports(): import fuzzers -def TestOneInput(data): +def TestOneInput(data: bytes) -> None: try: fuzzers.fuzz_font(data) except Exception: @@ -33,7 +32,7 @@ def TestOneInput(data): pass -def main(): +def main() -> None: fuzzers.enable_decompressionbomb_error() atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index e7cd0474a..9137391b6 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -1,5 +1,3 @@ -#!/usr/bin/python3 - # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import annotations import atheris @@ -24,7 +21,7 @@ with atheris.instrument_imports(): import fuzzers -def TestOneInput(data): +def TestOneInput(data: bytes) -> None: try: fuzzers.fuzz_image(data) except Exception: @@ -33,7 +30,7 @@ def TestOneInput(data): pass -def main(): +def main() -> None: fuzzers.enable_decompressionbomb_error() atheris.Setup(sys.argv, TestOneInput) atheris.Fuzz() diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py index 3f3c1e388..d6c1fab71 100644 --- a/Tests/oss-fuzz/fuzzers.py +++ b/Tests/oss-fuzz/fuzzers.py @@ -1,22 +1,23 @@ from __future__ import annotations + import io import warnings from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont -def enable_decompressionbomb_error(): +def enable_decompressionbomb_error() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = True warnings.filterwarnings("ignore") warnings.simplefilter("error", Image.DecompressionBombWarning) -def disable_decompressionbomb_error(): +def disable_decompressionbomb_error() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = False warnings.resetwarnings() -def fuzz_image(data): +def fuzz_image(data: bytes) -> None: # This will fail on some images in the corpus, as we have many # invalid images in the test suite. with Image.open(io.BytesIO(data)) as im: @@ -25,7 +26,7 @@ def fuzz_image(data): im.save(io.BytesIO(), "BMP") -def fuzz_font(data): +def fuzz_font(data: bytes) -> None: wrapper = io.BytesIO(data) try: font = ImageFont.truetype(wrapper) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index 68834045a..459cc1a37 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -1,4 +1,5 @@ from __future__ import annotations + import subprocess import sys @@ -23,7 +24,7 @@ if features.check("libjpeg_turbo"): "path", subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"), ) -def test_fuzz_images(path): +def test_fuzz_images(path: str) -> None: fuzzers.enable_decompressionbomb_error() try: with open(path, "rb") as f: @@ -54,7 +55,7 @@ def test_fuzz_images(path): @pytest.mark.parametrize( "path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") ) -def test_fuzz_fonts(path): +def test_fuzz_fonts(path: str) -> None: if not path: return with open(path, "rb") as f: diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index c582dfad3..c3926250f 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -1,8 +1,9 @@ from __future__ import annotations + from PIL import Image -def test_sanity(): +def test_sanity() -> None: # Make sure we have the binary extension Image.core.new("L", (100, 100)) diff --git a/Tests/test_binary.py b/Tests/test_binary.py index 62da26636..d19799a09 100644 --- a/Tests/test_binary.py +++ b/Tests/test_binary.py @@ -1,13 +1,14 @@ from __future__ import annotations + from PIL import _binary -def test_standard(): +def test_standard() -> None: assert _binary.i8(b"*") == 42 assert _binary.o8(42) == b"*" -def test_little_endian(): +def test_little_endian() -> None: assert _binary.i16le(b"\xff\xff\x00\x00") == 65535 assert _binary.i32le(b"\xff\xff\x00\x00") == 65535 @@ -15,7 +16,7 @@ def test_little_endian(): assert _binary.o32le(65535) == b"\xff\xff\x00\x00" -def test_big_endian(): +def test_big_endian() -> None: assert _binary.i16be(b"\x00\x00\xff\xff") == 0 assert _binary.i32be(b"\x00\x00\xff\xff") == 65535 diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index bed8dc3a8..0ad496135 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import warnings @@ -9,13 +10,13 @@ from .helper import assert_image_similar base = os.path.join("Tests", "images", "bmp") -def get_files(d, ext=".bmp"): +def get_files(d: str, ext: str = ".bmp") -> list[str]: return [ os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f ] -def test_bad(): +def test_bad() -> None: """These shouldn't crash/dos, but they shouldn't return anything either""" for f in get_files("b"): @@ -28,7 +29,7 @@ def test_bad(): pass -def test_questionable(): +def test_questionable() -> None: """These shouldn't crash/dos, but it's not well defined that these are in spec""" supported = [ @@ -55,7 +56,7 @@ def test_questionable(): raise -def test_good(): +def test_good() -> None: """These should all work. There's a set of target files in the html directory that we can compare against.""" @@ -79,7 +80,7 @@ def test_good(): "rgb32bf.bmp": "rgb24.png", } - def get_compare(f): + def get_compare(f: str) -> str: name = os.path.split(f)[1] if name in file_map: return os.path.join(base, "html", file_map[name]) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index e798cba3d..1f6ed6127 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFilter @@ -15,18 +16,18 @@ sample.putdata(sum([ # fmt: on -def test_imageops_box_blur(): +def test_imageops_box_blur() -> None: i = sample.filter(ImageFilter.BoxBlur(1)) assert i.mode == sample.mode assert i.size == sample.size assert isinstance(i, Image.Image) -def box_blur(image, radius=1, n=1): +def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image: return image._new(image.im.box_blur((radius, radius), n)) -def assert_image(im, data, delta=0): +def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None: it = iter(im.getdata()) for data_row in data: im_row = [next(it) for _ in range(im.size[0])] @@ -36,7 +37,13 @@ def assert_image(im, data, delta=0): next(it) -def assert_blur(im, radius, data, passes=1, delta=0): +def assert_blur( + im: Image.Image, + radius: float, + data: list[list[int]], + passes: int = 1, + delta: int = 0, +) -> None: # check grayscale image assert_image(box_blur(im, radius, passes), data, delta) rgba = Image.merge("RGBA", (im, im, im, im)) @@ -44,7 +51,7 @@ def assert_blur(im, radius, data, passes=1, delta=0): assert_image(band, data, delta) -def test_color_modes(): +def test_color_modes() -> None: with pytest.raises(ValueError): box_blur(sample.convert("1")) with pytest.raises(ValueError): @@ -64,7 +71,7 @@ def test_color_modes(): box_blur(sample.convert("YCbCr")) -def test_radius_0(): +def test_radius_0() -> None: assert_blur( sample, 0, @@ -80,7 +87,7 @@ def test_radius_0(): ) -def test_radius_0_02(): +def test_radius_0_02() -> None: assert_blur( sample, 0.02, @@ -97,7 +104,7 @@ def test_radius_0_02(): ) -def test_radius_0_05(): +def test_radius_0_05() -> None: assert_blur( sample, 0.05, @@ -114,7 +121,7 @@ def test_radius_0_05(): ) -def test_radius_0_1(): +def test_radius_0_1() -> None: assert_blur( sample, 0.1, @@ -131,7 +138,7 @@ def test_radius_0_1(): ) -def test_radius_0_5(): +def test_radius_0_5() -> None: assert_blur( sample, 0.5, @@ -148,7 +155,7 @@ def test_radius_0_5(): ) -def test_radius_1(): +def test_radius_1() -> None: assert_blur( sample, 1, @@ -165,7 +172,7 @@ def test_radius_1(): ) -def test_radius_1_5(): +def test_radius_1_5() -> None: assert_blur( sample, 1.5, @@ -182,7 +189,7 @@ def test_radius_1_5(): ) -def test_radius_bigger_then_half(): +def test_radius_bigger_then_half() -> None: assert_blur( sample, 3, @@ -199,7 +206,7 @@ def test_radius_bigger_then_half(): ) -def test_radius_bigger_then_width(): +def test_radius_bigger_then_width() -> None: assert_blur( sample, 10, @@ -214,7 +221,7 @@ def test_radius_bigger_then_width(): ) -def test_extreme_large_radius(): +def test_extreme_large_radius() -> None: assert_blur( sample, 600, @@ -229,7 +236,7 @@ def test_extreme_large_radius(): ) -def test_two_passes(): +def test_two_passes() -> None: assert_blur( sample, 1, @@ -247,7 +254,7 @@ def test_two_passes(): ) -def test_three_passes(): +def test_three_passes() -> None: assert_blur( sample, 1, diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 448ba2fac..c8886a779 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -1,5 +1,7 @@ from __future__ import annotations + from array import array +from types import ModuleType import pytest @@ -7,6 +9,7 @@ from PIL import Image, ImageFilter from .helper import assert_image_equal +numpy: ModuleType | None try: import numpy except ImportError: @@ -14,7 +17,9 @@ except ImportError: class TestColorLut3DCoreAPI: - def generate_identity_table(self, channels, size): + def generate_identity_table( + self, channels: int, size: int | tuple[int, int, int] + ) -> tuple[int, int, int, int, list[float]]: if isinstance(size, tuple): size_1d, size_2d, size_3d = size else: @@ -40,7 +45,7 @@ class TestColorLut3DCoreAPI: [item for sublist in table for item in sublist], ) - def test_wrong_args(self): + def test_wrong_args(self) -> None: im = Image.new("RGB", (10, 10), 0) with pytest.raises(ValueError, match="filter"): @@ -100,7 +105,7 @@ class TestColorLut3DCoreAPI: with pytest.raises(TypeError): im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16) - def test_correct_args(self): + def test_correct_args(self) -> None: im = Image.new("RGB", (10, 10), 0) im.im.color_lut_3d( @@ -135,7 +140,7 @@ class TestColorLut3DCoreAPI: *self.generate_identity_table(3, (3, 3, 65)), ) - def test_wrong_mode(self): + def test_wrong_mode(self) -> None: with pytest.raises(ValueError, match="wrong mode"): im = Image.new("L", (10, 10), 0) im.im.color_lut_3d( @@ -166,7 +171,7 @@ class TestColorLut3DCoreAPI: "RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ) - def test_correct_mode(self): + def test_correct_mode(self) -> None: im = Image.new("RGBA", (10, 10), 0) im.im.color_lut_3d( "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3) @@ -187,7 +192,7 @@ class TestColorLut3DCoreAPI: "RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3) ) - def test_identities(self): + def test_identities(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -223,7 +228,7 @@ class TestColorLut3DCoreAPI: ), ) - def test_identities_4_channels(self): + def test_identities_4_channels(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -246,7 +251,7 @@ class TestColorLut3DCoreAPI: ), ) - def test_copy_alpha_channel(self): + def test_copy_alpha_channel(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGBA", @@ -269,7 +274,7 @@ class TestColorLut3DCoreAPI: ), ) - def test_channels_order(self): + def test_channels_order(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -294,7 +299,7 @@ class TestColorLut3DCoreAPI: ]))) # fmt: on - def test_overflow(self): + def test_overflow(self) -> None: g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -347,7 +352,7 @@ class TestColorLut3DCoreAPI: class TestColorLut3DFilter: - def test_wrong_args(self): + def test_wrong_args(self) -> None: with pytest.raises(ValueError, match="should be either an integer"): ImageFilter.Color3DLUT("small", [1]) @@ -375,7 +380,7 @@ class TestColorLut3DFilter: with pytest.raises(ValueError, match="Only 3 or 4 output"): ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8, channels=2) - def test_convert_table(self): + def test_convert_table(self) -> None: lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) assert tuple(lut.size) == (2, 2, 2) assert lut.name == "Color 3D LUT" @@ -393,7 +398,8 @@ class TestColorLut3DFilter: assert lut.table == list(range(4)) * 8 @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy_sources(self): + def test_numpy_sources(self) -> None: + assert numpy is not None table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16) with pytest.raises(ValueError, match="should have either channels"): lut = ImageFilter.Color3DLUT((5, 6, 7), table) @@ -426,7 +432,8 @@ class TestColorLut3DFilter: assert lut.table[0] == 33 @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy_formats(self): + def test_numpy_formats(self) -> None: + assert numpy is not None g = Image.linear_gradient("L") im = Image.merge( "RGB", @@ -465,7 +472,7 @@ class TestColorLut3DFilter: lut.table = numpy.array(lut.table, dtype=numpy.int8) im.filter(lut) - def test_repr(self): + def test_repr(self) -> None: lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) assert repr(lut) == "" @@ -483,7 +490,7 @@ class TestColorLut3DFilter: class TestGenerateColorLut3D: - def test_wrong_channels_count(self): + def test_wrong_channels_count(self) -> None: with pytest.raises(ValueError, match="3 or 4 output channels"): ImageFilter.Color3DLUT.generate( 5, channels=2, callback=lambda r, g, b: (r, g, b) @@ -497,7 +504,7 @@ class TestGenerateColorLut3D: 5, channels=4, callback=lambda r, g, b: (r, g, b) ) - def test_3_channels(self): + def test_3_channels(self) -> None: lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) assert tuple(lut.size) == (5, 5, 5) assert lut.name == "Color 3D LUT" @@ -507,7 +514,7 @@ class TestGenerateColorLut3D: 1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0] # fmt: on - def test_4_channels(self): + def test_4_channels(self) -> None: lut = ImageFilter.Color3DLUT.generate( 5, channels=4, callback=lambda r, g, b: (b, r, g, (r + g + b) / 2) ) @@ -520,7 +527,7 @@ class TestGenerateColorLut3D: ] # fmt: on - def test_apply(self): + def test_apply(self) -> None: lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) g = Image.linear_gradient("L") @@ -536,7 +543,7 @@ class TestGenerateColorLut3D: class TestTransformColorLut3D: - def test_wrong_args(self): + def test_wrong_args(self) -> None: source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) with pytest.raises(ValueError, match="Only 3 or 4 output"): @@ -551,7 +558,7 @@ class TestTransformColorLut3D: with pytest.raises(TypeError): source.transform(lambda r, g, b, a: (r, g, b)) - def test_target_mode(self): + def test_target_mode(self) -> None: source = ImageFilter.Color3DLUT.generate( 2, lambda r, g, b: (r, g, b), target_mode="HSV" ) @@ -562,7 +569,7 @@ class TestTransformColorLut3D: lut = source.transform(lambda r, g, b: (r, g, b), target_mode="RGB") assert lut.mode == "RGB" - def test_3_to_3_channels(self): + def test_3_to_3_channels(self) -> None: source = ImageFilter.Color3DLUT.generate((3, 4, 5), lambda r, g, b: (r, g, b)) lut = source.transform(lambda r, g, b: (r * r, g * g, b * b)) assert tuple(lut.size) == tuple(source.size) @@ -570,7 +577,7 @@ class TestTransformColorLut3D: assert lut.table != source.table assert lut.table[:10] == [0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0] - def test_3_to_4_channels(self): + def test_3_to_4_channels(self) -> None: source = ImageFilter.Color3DLUT.generate((6, 5, 4), lambda r, g, b: (r, g, b)) lut = source.transform(lambda r, g, b: (r * r, g * g, b * b, 1), channels=4) assert tuple(lut.size) == tuple(source.size) @@ -582,7 +589,7 @@ class TestTransformColorLut3D: 0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1] # fmt: on - def test_4_to_3_channels(self): + def test_4_to_3_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4 ) @@ -598,7 +605,7 @@ class TestTransformColorLut3D: 1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0] # fmt: on - def test_4_to_4_channels(self): + def test_4_to_4_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4 ) @@ -612,7 +619,7 @@ class TestTransformColorLut3D: 0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5] # fmt: on - def test_with_normals_3_channels(self): + def test_with_normals_3_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (6, 5, 4), lambda r, g, b: (r * r, g * g, b * b) ) @@ -628,7 +635,7 @@ class TestTransformColorLut3D: 0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0] # fmt: on - def test_with_normals_4_channels(self): + def test_with_normals_4_channels(self) -> None: source = ImageFilter.Color3DLUT.generate( (3, 6, 5), lambda r, g, b: (r * r, g * g, b * b, 1), channels=4 ) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 5275652f6..2c1de8bc3 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest @@ -8,7 +9,7 @@ from PIL import Image from .helper import is_pypy -def test_get_stats(): +def test_get_stats() -> None: # Create at least one image Image.new("RGB", (10, 10)) @@ -21,7 +22,7 @@ def test_get_stats(): assert "blocks_cached" in stats -def test_reset_stats(): +def test_reset_stats() -> None: Image.core.reset_stats() stats = Image.core.get_stats() @@ -34,19 +35,19 @@ def test_reset_stats(): class TestCoreMemory: - def teardown_method(self): + def teardown_method(self) -> None: # Restore default values Image.core.set_alignment(1) Image.core.set_block_size(1024 * 1024) Image.core.set_blocks_max(0) Image.core.clear_cache() - def test_get_alignment(self): + def test_get_alignment(self) -> None: alignment = Image.core.get_alignment() assert alignment > 0 - def test_set_alignment(self): + def test_set_alignment(self) -> None: for i in [1, 2, 4, 8, 16, 32]: Image.core.set_alignment(i) alignment = Image.core.get_alignment() @@ -62,12 +63,12 @@ class TestCoreMemory: with pytest.raises(ValueError): Image.core.set_alignment(3) - def test_get_block_size(self): + def test_get_block_size(self) -> None: block_size = Image.core.get_block_size() assert block_size >= 4096 - def test_set_block_size(self): + def test_set_block_size(self) -> None: for i in [4096, 2 * 4096, 3 * 4096]: Image.core.set_block_size(i) block_size = Image.core.get_block_size() @@ -83,7 +84,7 @@ class TestCoreMemory: with pytest.raises(ValueError): Image.core.set_block_size(4000) - def test_set_block_size_stats(self): + def test_set_block_size_stats(self) -> None: Image.core.reset_stats() Image.core.set_blocks_max(0) Image.core.set_block_size(4096) @@ -95,12 +96,12 @@ class TestCoreMemory: if not is_pypy(): assert stats["freed_blocks"] >= 64 - def test_get_blocks_max(self): + def test_get_blocks_max(self) -> None: blocks_max = Image.core.get_blocks_max() assert blocks_max >= 0 - def test_set_blocks_max(self): + def test_set_blocks_max(self) -> None: for i in [0, 1, 10]: Image.core.set_blocks_max(i) blocks_max = Image.core.get_blocks_max() @@ -116,7 +117,7 @@ class TestCoreMemory: Image.core.set_blocks_max(2**29) @pytest.mark.skipif(is_pypy(), reason="Images not collected") - def test_set_blocks_max_stats(self): + def test_set_blocks_max_stats(self) -> None: Image.core.reset_stats() Image.core.set_blocks_max(128) Image.core.set_block_size(4096) @@ -131,7 +132,7 @@ class TestCoreMemory: assert stats["blocks_cached"] == 64 @pytest.mark.skipif(is_pypy(), reason="Images not collected") - def test_clear_cache_stats(self): + def test_clear_cache_stats(self) -> None: Image.core.reset_stats() Image.core.clear_cache() Image.core.set_blocks_max(128) @@ -148,7 +149,7 @@ class TestCoreMemory: assert stats["freed_blocks"] >= 48 assert stats["blocks_cached"] == 16 - def test_large_images(self): + def test_large_images(self) -> None: Image.core.reset_stats() Image.core.set_blocks_max(0) Image.core.set_block_size(4096) @@ -165,14 +166,14 @@ class TestCoreMemory: class TestEnvVars: - def teardown_method(self): + def teardown_method(self) -> None: # Restore default values Image.core.set_alignment(1) Image.core.set_block_size(1024 * 1024) Image.core.set_blocks_max(0) Image.core.clear_cache() - def test_units(self): + def test_units(self) -> None: Image._apply_env_variables({"PILLOW_BLOCKS_MAX": "2K"}) assert Image.core.get_blocks_max() == 2 * 1024 Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) @@ -186,6 +187,6 @@ class TestEnvVars: {"PILLOW_BLOCKS_MAX": "wat"}, ), ) - def test_warnings(self, var): + def test_warnings(self, var: dict[str, str]) -> None: with pytest.warns(UserWarning): Image._apply_env_variables(var) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 391948d40..9c21efa45 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image @@ -11,16 +12,16 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS class TestDecompressionBomb: - def teardown_method(self, method): + def teardown_method(self, method) -> None: Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - def test_no_warning_small_file(self): + def test_no_warning_small_file(self) -> None: # Implicit assert: no warning. # A warning would cause a failure. with Image.open(TEST_FILE): pass - def test_no_warning_no_limit(self): + def test_no_warning_no_limit(self) -> None: # Arrange # Turn limit off Image.MAX_IMAGE_PIXELS = None @@ -32,7 +33,7 @@ class TestDecompressionBomb: with Image.open(TEST_FILE): pass - def test_warning(self): + def test_warning(self) -> None: # Set limit to trigger warning on the test file Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 @@ -41,7 +42,7 @@ class TestDecompressionBomb: with Image.open(TEST_FILE): pass - def test_exception(self): + def test_exception(self) -> None: # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1 @@ -50,22 +51,22 @@ class TestDecompressionBomb: with Image.open(TEST_FILE): pass - def test_exception_ico(self): + def test_exception_ico(self) -> None: with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/decompression_bomb.ico"): pass - def test_exception_gif(self): + def test_exception_gif(self) -> None: with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/decompression_bomb.gif"): pass - def test_exception_gif_extents(self): + def test_exception_gif_extents(self) -> None: with Image.open("Tests/images/decompression_bomb_extents.gif") as im: with pytest.raises(Image.DecompressionBombError): im.seek(1) - def test_exception_gif_zero_width(self): + def test_exception_gif_zero_width(self) -> None: # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 @@ -74,7 +75,7 @@ class TestDecompressionBomb: with Image.open("Tests/images/zero_width.gif"): pass - def test_exception_bmp(self): + def test_exception_bmp(self) -> None: with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/bmp/b/reallybig.bmp"): pass @@ -82,15 +83,15 @@ class TestDecompressionBomb: class TestDecompressionCrop: @classmethod - def setup_class(cls): + def setup_class(cls) -> None: width, height = 128, 128 Image.MAX_IMAGE_PIXELS = height * width * 4 - 1 @classmethod - def teardown_class(cls): + def teardown_class(cls) -> None: Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT - def test_enlarge_crop(self): + def test_enlarge_crop(self) -> None: # Crops can extend the extents, therefore we should have the # same decompression bomb warnings on them. with hopper() as src: @@ -98,7 +99,7 @@ class TestDecompressionCrop: with pytest.warns(Image.DecompressionBombWarning): src.crop(box) - def test_crop_decompression_checks(self): + def test_crop_decompression_checks(self) -> None: im = Image.new("RGB", (100, 100)) for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index d45a6603c..584d8f91d 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import _deprecate @@ -19,12 +20,12 @@ from PIL import _deprecate ), ], ) -def test_version(version, expected): +def test_version(version: int | None, expected: str) -> None: with pytest.warns(DeprecationWarning, match=expected): _deprecate.deprecate("Old thing", version, "new thing") -def test_unknown_version(): +def test_unknown_version() -> None: expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?" with pytest.raises(ValueError, match=expected): _deprecate.deprecate("Old thing", 12345, "new thing") @@ -45,13 +46,13 @@ def test_unknown_version(): ), ], ) -def test_old_version(deprecated, plural, expected): +def test_old_version(deprecated: str, plural: bool, expected: str) -> None: expected = r"" with pytest.raises(RuntimeError, match=expected): _deprecate.deprecate(deprecated, 1, plural=plural) -def test_plural(): +def test_plural() -> None: expected = ( r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Use new thing instead\." @@ -60,7 +61,7 @@ def test_plural(): _deprecate.deprecate("Old things", 11, "new thing", plural=True) -def test_replacement_and_action(): +def test_replacement_and_action() -> None: expected = "Use only one of 'replacement' and 'action'" with pytest.raises(ValueError, match=expected): _deprecate.deprecate( @@ -75,7 +76,7 @@ def test_replacement_and_action(): "Upgrade to new thing.", ], ) -def test_action(action): +def test_action(action: str) -> None: expected = ( r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Upgrade to new thing\." @@ -84,7 +85,7 @@ def test_action(action): _deprecate.deprecate("Old thing", 11, action=action) -def test_no_replacement_or_action(): +def test_no_replacement_or_action() -> None: expected = ( r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)" ) diff --git a/Tests/test_features.py b/Tests/test_features.py index 8f0e4b418..8d2d198ff 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -1,6 +1,8 @@ from __future__ import annotations + import io import re +from typing import Callable import pytest @@ -14,7 +16,7 @@ except ImportError: pass -def test_check(): +def test_check() -> None: # Check the correctness of the convenience function for module in features.modules: assert features.check_module(module) == features.check(module) @@ -24,11 +26,11 @@ def test_check(): assert features.check_feature(feature) == features.check(feature) -def test_version(): +def test_version() -> None: # Check the correctness of the convenience function # and the format of version numbers - def test(name, function): + def test(name: str, function: Callable[[str], bool]) -> None: version = features.version(name) if not features.check(name): assert version is None @@ -46,56 +48,56 @@ def test_version(): @skip_unless_feature("webp") -def test_webp_transparency(): +def test_webp_transparency() -> None: assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY @skip_unless_feature("webp") -def test_webp_mux(): +def test_webp_mux() -> None: assert features.check("webp_mux") == _webp.HAVE_WEBPMUX @skip_unless_feature("webp") -def test_webp_anim(): +def test_webp_anim() -> None: assert features.check("webp_anim") == _webp.HAVE_WEBPANIM @skip_unless_feature("libjpeg_turbo") -def test_libjpeg_turbo_version(): +def test_libjpeg_turbo_version() -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo")) @skip_unless_feature("libimagequant") -def test_libimagequant_version(): +def test_libimagequant_version() -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant")) @pytest.mark.parametrize("feature", features.modules) -def test_check_modules(feature): +def test_check_modules(feature: str) -> None: assert features.check_module(feature) in [True, False] @pytest.mark.parametrize("feature", features.codecs) -def test_check_codecs(feature): +def test_check_codecs(feature: str) -> None: assert features.check_codec(feature) in [True, False] -def test_check_warns_on_nonexistent(): +def test_check_warns_on_nonexistent() -> None: with pytest.warns(UserWarning) as cm: has_feature = features.check("typo") assert has_feature is False assert str(cm[-1].message) == "Unknown feature 'typo'." -def test_supported_modules(): +def test_supported_modules() -> None: assert isinstance(features.get_supported_modules(), list) assert isinstance(features.get_supported_codecs(), list) assert isinstance(features.get_supported_features(), list) assert isinstance(features.get_supported(), list) -def test_unsupported_codec(): +def test_unsupported_codec() -> None: # Arrange codec = "unsupported_codec" # Act / Assert @@ -105,7 +107,7 @@ def test_unsupported_codec(): features.version_codec(codec) -def test_unsupported_module(): +def test_unsupported_module() -> None: # Arrange module = "unsupported_module" # Act / Assert @@ -115,7 +117,7 @@ def test_unsupported_module(): features.version_module(module) -def test_pilinfo(): +def test_pilinfo() -> None: buf = io.StringIO() features.pilinfo(buf) out = buf.getvalue() diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 60d951636..395165b36 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image, ImageSequence, PngImagePlugin @@ -7,7 +10,7 @@ from PIL import Image, ImageSequence, PngImagePlugin # APNG browser support tests and fixtures via: # https://philip.html5.org/tests/apng/tests.html # (referenced from https://wiki.mozilla.org/APNG_Specification) -def test_apng_basic(): +def test_apng_basic() -> None: with Image.open("Tests/images/apng/single_frame.png") as im: assert not im.is_animated assert im.n_frames == 1 @@ -44,14 +47,14 @@ def test_apng_basic(): "filename", ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), ) -def test_apng_fdat(filename): +def test_apng_fdat(filename: str) -> None: with Image.open(filename) as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_dispose(): +def test_apng_dispose() -> None: with Image.open("Tests/images/apng/dispose_op_none.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -83,7 +86,7 @@ def test_apng_dispose(): assert im.getpixel((64, 32)) == (0, 0, 0, 0) -def test_apng_dispose_region(): +def test_apng_dispose_region() -> None: with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -105,7 +108,7 @@ def test_apng_dispose_region(): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_dispose_op_previous_frame(): +def test_apng_dispose_op_previous_frame() -> None: # Test that the dispose settings being used are from the previous frame # # Image created with: @@ -130,14 +133,14 @@ def test_apng_dispose_op_previous_frame(): assert im.getpixel((0, 0)) == (255, 0, 0, 255) -def test_apng_dispose_op_background_p_mode(): +def test_apng_dispose_op_background_p_mode() -> None: with Image.open("Tests/images/apng/dispose_op_background_p_mode.png") as im: im.seek(1) im.load() assert im.size == (128, 64) -def test_apng_blend(): +def test_apng_blend() -> None: with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -164,20 +167,20 @@ def test_apng_blend(): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_blend_transparency(): +def test_apng_blend_transparency() -> None: with Image.open("Tests/images/blend_transparency.png") as im: im.seek(1) assert im.getpixel((0, 0)) == (255, 0, 0) -def test_apng_chunk_order(): +def test_apng_chunk_order() -> None: with Image.open("Tests/images/apng/fctl_actl.png") as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_delay(): +def test_apng_delay() -> None: with Image.open("Tests/images/apng/delay.png") as im: im.seek(1) assert im.info.get("duration") == 500.0 @@ -217,7 +220,7 @@ def test_apng_delay(): assert im.info.get("duration") == 1000.0 -def test_apng_num_plays(): +def test_apng_num_plays() -> None: with Image.open("Tests/images/apng/num_plays.png") as im: assert im.info.get("loop") == 0 @@ -225,7 +228,7 @@ def test_apng_num_plays(): assert im.info.get("loop") == 1 -def test_apng_mode(): +def test_apng_mode() -> None: with Image.open("Tests/images/apng/mode_16bit.png") as im: assert im.mode == "RGBA" im.seek(im.n_frames - 1) @@ -266,7 +269,7 @@ def test_apng_mode(): assert im.getpixel((64, 32)) == (0, 0, 255, 128) -def test_apng_chunk_errors(): +def test_apng_chunk_errors() -> None: with Image.open("Tests/images/apng/chunk_no_actl.png") as im: assert not im.is_animated @@ -291,7 +294,7 @@ def test_apng_chunk_errors(): im.seek(im.n_frames - 1) -def test_apng_syntax_errors(): +def test_apng_syntax_errors() -> None: with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: assert not im.is_animated @@ -335,14 +338,14 @@ def test_apng_syntax_errors(): "sequence_fdat_fctl.png", ), ) -def test_apng_sequence_errors(test_file): +def test_apng_sequence_errors(test_file: str) -> None: with pytest.raises(SyntaxError): with Image.open(f"Tests/images/apng/{test_file}") as im: im.seek(im.n_frames - 1) im.load() -def test_apng_save(tmp_path): +def test_apng_save(tmp_path: Path) -> None: with Image.open("Tests/images/apng/single_frame.png") as im: test_file = str(tmp_path / "temp.png") im.save(test_file, save_all=True) @@ -373,7 +376,7 @@ def test_apng_save(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_save_alpha(tmp_path): +def test_apng_save_alpha(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) @@ -387,7 +390,7 @@ def test_apng_save_alpha(tmp_path): assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127) -def test_apng_save_split_fdat(tmp_path): +def test_apng_save_split_fdat(tmp_path: Path) -> None: # test to make sure we do not generate sequence errors when writing # frames with image data spanning multiple fdAT chunks (in this case # both the default image and first animation frame will span multiple @@ -411,7 +414,7 @@ def test_apng_save_split_fdat(tmp_path): assert exception is None -def test_apng_save_duration_loop(tmp_path): +def test_apng_save_duration_loop(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") with Image.open("Tests/images/apng/delay.png") as im: frames = [] @@ -474,7 +477,7 @@ def test_apng_save_duration_loop(tmp_path): assert im.info["duration"] == 600 -def test_apng_save_disposal(tmp_path): +def test_apng_save_disposal(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") size = (128, 64) red = Image.new("RGBA", size, (255, 0, 0, 255)) @@ -575,7 +578,7 @@ def test_apng_save_disposal(tmp_path): assert im.getpixel((64, 32)) == (0, 0, 0, 0) -def test_apng_save_disposal_previous(tmp_path): +def test_apng_save_disposal_previous(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") size = (128, 64) blue = Image.new("RGBA", size, (0, 0, 255, 255)) @@ -597,7 +600,7 @@ def test_apng_save_disposal_previous(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) -def test_apng_save_blend(tmp_path): +def test_apng_save_blend(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.png") size = (128, 64) red = Image.new("RGBA", size, (255, 0, 0, 255)) @@ -665,7 +668,7 @@ def test_apng_save_blend(tmp_path): assert im.getpixel((0, 0)) == (0, 255, 0, 255) -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open("Tests/images/apng/delay.png") im.seek(1) im.close() @@ -677,7 +680,9 @@ def test_seek_after_close(): @pytest.mark.parametrize("mode", ("RGBA", "RGB", "P")) @pytest.mark.parametrize("default_image", (True, False)) @pytest.mark.parametrize("duplicate", (True, False)) -def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_path): +def test_different_modes_in_later_frames( + mode: str, default_image: bool, duplicate: bool, tmp_path: Path +) -> None: test_file = str(tmp_path / "temp.png") im = Image.new("L", (1, 1)) @@ -689,3 +694,12 @@ def test_different_modes_in_later_frames(mode, default_image, duplicate, tmp_pat ) with Image.open(test_file) as reloaded: assert reloaded.mode == mode + + +def test_apng_repeated_seeks_give_correct_info() -> None: + with Image.open("Tests/images/apng/different_durations.png") as im: + for i in range(3): + im.seek(0) + assert im.info["duration"] == 4000 + im.seek(1) + assert im.info["duration"] == 1000 diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 4c1e38d1d..1e2f20c40 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image @@ -11,7 +14,7 @@ from .helper import ( ) -def test_load_blp1(): +def test_load_blp1() -> None: with Image.open("Tests/images/blp/blp1_jpeg.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png") @@ -19,22 +22,22 @@ def test_load_blp1(): im.load() -def test_load_blp2_raw(): +def test_load_blp2_raw() -> None: with Image.open("Tests/images/blp/blp2_raw.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_raw.png") -def test_load_blp2_dxt1(): +def test_load_blp2_dxt1() -> None: with Image.open("Tests/images/blp/blp2_dxt1.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1.png") -def test_load_blp2_dxt1a(): +def test_load_blp2_dxt1a() -> None: with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: f = str(tmp_path / "temp.blp") for version in ("BLP1", "BLP2"): @@ -68,7 +71,7 @@ def test_save(tmp_path): "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", ], ) -def test_crashes(test_file): +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 4cc92c5f6..1eaff0c7d 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,5 +1,7 @@ from __future__ import annotations + import io +from pathlib import Path import pytest @@ -13,8 +15,8 @@ from .helper import ( ) -def test_sanity(tmp_path): - def roundtrip(im): +def test_sanity(tmp_path: Path) -> None: + def roundtrip(im: Image.Image) -> None: outfile = str(tmp_path / "temp.bmp") im.save(outfile, "BMP") @@ -34,20 +36,20 @@ def test_sanity(tmp_path): roundtrip(hopper("RGB")) -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): BmpImagePlugin.BmpImageFile(fp) -def test_fallback_if_mmap_errors(): +def test_fallback_if_mmap_errors() -> None: # This image has been truncated, # so that the buffer is not large enough when using mmap with Image.open("Tests/images/mmap_error.bmp") as im: assert_image_equal_tofile(im, "Tests/images/pal8_offset.bmp") -def test_save_to_bytes(): +def test_save_to_bytes() -> None: output = io.BytesIO() im = hopper() im.save(output, "BMP") @@ -59,7 +61,7 @@ def test_save_to_bytes(): assert reloaded.format == "BMP" -def test_small_palette(tmp_path): +def test_small_palette(tmp_path: Path) -> None: im = Image.new("P", (1, 1)) colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] im.putpalette(colors) @@ -71,7 +73,7 @@ def test_small_palette(tmp_path): assert reloaded.getpalette() == colors -def test_save_too_large(tmp_path): +def test_save_too_large(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.bmp") with Image.new("RGB", (1, 1)) as im: im._size = (37838, 37838) @@ -79,7 +81,7 @@ def test_save_too_large(tmp_path): im.save(outfile) -def test_dpi(): +def test_dpi() -> None: dpi = (72, 72) output = io.BytesIO() @@ -91,7 +93,7 @@ def test_dpi(): assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161) -def test_save_bmp_with_dpi(tmp_path): +def test_save_bmp_with_dpi(tmp_path: Path) -> None: # Test for #1301 # Arrange outfile = str(tmp_path / "temp.jpg") @@ -109,7 +111,7 @@ def test_save_bmp_with_dpi(tmp_path): assert reloaded.format == "JPEG" -def test_save_float_dpi(tmp_path): +def test_save_float_dpi(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.bmp") with Image.open("Tests/images/hopper.bmp") as im: im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) @@ -117,7 +119,7 @@ def test_save_float_dpi(tmp_path): assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306) -def test_load_dib(): +def test_load_dib() -> None: # test for #1293, Imagegrab returning Unsupported Bitfields Format with Image.open("Tests/images/clipboard.dib") as im: assert im.format == "DIB" @@ -126,7 +128,7 @@ def test_load_dib(): assert_image_equal_tofile(im, "Tests/images/clipboard_target.png") -def test_save_dib(tmp_path): +def test_save_dib(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.dib") with Image.open("Tests/images/clipboard.dib") as im: @@ -138,7 +140,7 @@ def test_save_dib(tmp_path): assert_image_equal(im, reloaded) -def test_rgba_bitfields(): +def test_rgba_bitfields() -> None: # This test image has been manually hexedited # to change the bitfield compression in the header from XBGR to RGBA with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: @@ -156,7 +158,7 @@ def test_rgba_bitfields(): ) -def test_rle8(): +def test_rle8() -> None: with Image.open("Tests/images/hopper_rle8.bmp") as im: assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.bmp", 12) @@ -176,7 +178,7 @@ def test_rle8(): im.load() -def test_rle4(): +def test_rle4() -> None: with Image.open("Tests/images/bmp/g/pal4rle.bmp") as im: assert_image_similar_tofile(im, "Tests/images/bmp/g/pal4.bmp", 12) @@ -192,7 +194,7 @@ def test_rle4(): ("Tests/images/bmp/g/pal8rle.bmp", 1064), ), ) -def test_rle8_eof(file_name, length): +def test_rle8_eof(file_name: str, length: int) -> None: with open(file_name, "rb") as fp: data = fp.read(length) with Image.open(io.BytesIO(data)) as im: @@ -200,7 +202,7 @@ def test_rle8_eof(file_name, length): im.load() -def test_offset(): +def test_offset() -> None: # 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: diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 5780232a2..3dd24533a 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import BufrStubImagePlugin, Image @@ -8,7 +11,7 @@ from .helper import hopper TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -19,7 +22,7 @@ def test_open(): assert im.size == (1, 1) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -28,7 +31,7 @@ def test_invalid_file(): BufrStubImagePlugin.BufrStubImageFile(invalid_file) -def test_load(): +def test_load() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler @@ -36,7 +39,7 @@ def test_load(): im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: # Arrange im = hopper() tmpfile = str(tmp_path / "temp.bufr") @@ -46,13 +49,13 @@ def test_save(tmp_path): im.save(tmpfile) -def test_handler(tmp_path): +def test_handler(tmp_path: Path) -> None: class TestHandler: opened = False loaded = False saved = False - def open(self, im): + def open(self, im) -> None: self.opened = True def load(self, im): @@ -60,7 +63,7 @@ def test_handler(tmp_path): im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename): + def save(self, im, fp, filename) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 0da5d3824..813b444db 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ContainerIO, Image @@ -8,19 +9,19 @@ from .helper import hopper TEST_FILE = "Tests/images/dummy.container" -def test_sanity(): +def test_sanity() -> None: dir(Image) dir(ContainerIO) -def test_isatty(): +def test_isatty() -> None: with hopper() as im: container = ContainerIO.ContainerIO(im, 0, 0) assert container.isatty() is False -def test_seek_mode_0(): +def test_seek_mode_0() -> None: # Arrange mode = 0 with open(TEST_FILE, "rb") as fh: @@ -34,7 +35,7 @@ def test_seek_mode_0(): assert container.tell() == 33 -def test_seek_mode_1(): +def test_seek_mode_1() -> None: # Arrange mode = 1 with open(TEST_FILE, "rb") as fh: @@ -48,7 +49,7 @@ def test_seek_mode_1(): assert container.tell() == 66 -def test_seek_mode_2(): +def test_seek_mode_2() -> None: # Arrange mode = 2 with open(TEST_FILE, "rb") as fh: @@ -63,7 +64,7 @@ def test_seek_mode_2(): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n0(bytesmode): +def test_read_n0(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -79,7 +80,7 @@ def test_read_n0(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n(bytesmode): +def test_read_n(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -95,7 +96,7 @@ def test_read_n(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_eof(bytesmode): +def test_read_eof(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -111,7 +112,7 @@ def test_read_eof(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readline(bytesmode): +def test_readline(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 0, 120) @@ -126,7 +127,7 @@ def test_readline(bytesmode): @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readlines(bytesmode): +def test_readlines(bytesmode: bool) -> None: # Arrange expected = [ "This is line 1\n", diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 08c3257f9..dbf1b866d 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import CurImagePlugin, Image @@ -6,7 +7,7 @@ from PIL import CurImagePlugin, Image TEST_FILE = "Tests/images/deerstalker.cur" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: assert im.size == (32, 32) assert isinstance(im, CurImagePlugin.CurImageFile) @@ -16,7 +17,7 @@ def test_sanity(): assert im.getpixel((16, 16)) == (84, 87, 86, 255) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 25e4badbc..65337cad9 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest @@ -11,7 +12,7 @@ from .helper import assert_image_equal, hopper, is_pypy TEST_FILE = "Tests/images/hopper.dcx" -def test_sanity(): +def test_sanity() -> None: # Arrange # Act @@ -24,8 +25,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_FILE) im.load() @@ -33,26 +34,26 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): DcxImagePlugin.DcxImageFile(fp) -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act @@ -62,13 +63,13 @@ def test_tell(): assert frame == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_FILE) as im: assert im.n_frames == 1 assert not im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(TEST_FILE) as im: n_frames = im.n_frames @@ -81,7 +82,7 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_seek_too_far(): +def test_seek_too_far() -> None: # Arrange with Image.open(TEST_FILE) as im: frame = 999 # too big on purpose diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 2d60fbb64..ebc0e89a1 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -1,6 +1,9 @@ """Test DdsImagePlugin""" + from __future__ import annotations + from io import BytesIO +from pathlib import Path import pytest @@ -32,6 +35,7 @@ TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SR TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds" TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds" TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" +TEST_FILE_UNCOMPRESSED_BGR15 = "Tests/images/bgr15.dds" TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" @@ -44,7 +48,7 @@ TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" TEST_FILE_DX10_BC1_TYPELESS, ), ) -def test_sanity_dxt1_bc1(image_path): +def test_sanity_dxt1_bc1(image_path: str) -> None: """Check DXT1 and BC1 images can be opened""" with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: target = target.convert("RGBA") @@ -58,7 +62,7 @@ def test_sanity_dxt1_bc1(image_path): assert_image_equal(im, target) -def test_sanity_dxt3(): +def test_sanity_dxt3() -> None: """Check DXT3 images can be opened""" with Image.open(TEST_FILE_DXT3) as im: @@ -71,7 +75,7 @@ def test_sanity_dxt3(): assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png")) -def test_sanity_dxt5(): +def test_sanity_dxt5() -> None: """Check DXT5 images can be opened""" with Image.open(TEST_FILE_DXT5) as im: @@ -92,7 +96,7 @@ def test_sanity_dxt5(): TEST_FILE_BC4U, ), ) -def test_sanity_ati1_bc4u(image_path): +def test_sanity_ati1_bc4u(image_path: str) -> None: """Check ATI1 and BC4U images can be opened""" with Image.open(image_path) as im: @@ -113,7 +117,7 @@ def test_sanity_ati1_bc4u(image_path): TEST_FILE_DX10_BC4_TYPELESS, ), ) -def test_dx10_bc4(image_path): +def test_dx10_bc4(image_path: str) -> None: """Check DX10 BC4 images can be opened""" with Image.open(image_path) as im: @@ -134,7 +138,7 @@ def test_dx10_bc4(image_path): TEST_FILE_BC5U, ), ) -def test_sanity_ati2_bc5u(image_path): +def test_sanity_ati2_bc5u(image_path: str) -> None: """Check ATI2 and BC5U images can be opened""" with Image.open(image_path) as im: @@ -158,7 +162,7 @@ def test_sanity_ati2_bc5u(image_path): (TEST_FILE_BC5S, TEST_FILE_BC5S), ), ) -def test_dx10_bc5(image_path, expected_path): +def test_dx10_bc5(image_path: str, expected_path: str) -> None: """Check DX10 BC5 images can be opened""" with Image.open(image_path) as im: @@ -172,7 +176,7 @@ def test_dx10_bc5(image_path, expected_path): @pytest.mark.parametrize("image_path", (TEST_FILE_BC6H, TEST_FILE_BC6HS)) -def test_dx10_bc6h(image_path): +def test_dx10_bc6h(image_path: str) -> None: """Check DX10 BC6H/BC6HS images can be opened""" with Image.open(image_path) as im: @@ -185,7 +189,7 @@ def test_dx10_bc6h(image_path): assert_image_equal_tofile(im, image_path.replace(".dds", ".png")) -def test_dx10_bc7(): +def test_dx10_bc7() -> None: """Check DX10 images can be opened""" with Image.open(TEST_FILE_DX10_BC7) as im: @@ -198,7 +202,7 @@ def test_dx10_bc7(): assert_image_equal_tofile(im, TEST_FILE_DX10_BC7.replace(".dds", ".png")) -def test_dx10_bc7_unorm_srgb(): +def test_dx10_bc7_unorm_srgb() -> None: """Check DX10 unsigned normalized integer images can be opened""" with Image.open(TEST_FILE_DX10_BC7_UNORM_SRGB) as im: @@ -214,7 +218,7 @@ def test_dx10_bc7_unorm_srgb(): ) -def test_dx10_r8g8b8a8(): +def test_dx10_r8g8b8a8() -> None: """Check DX10 images can be opened""" with Image.open(TEST_FILE_DX10_R8G8B8A8) as im: @@ -227,7 +231,7 @@ def test_dx10_r8g8b8a8(): assert_image_equal_tofile(im, TEST_FILE_DX10_R8G8B8A8.replace(".dds", ".png")) -def test_dx10_r8g8b8a8_unorm_srgb(): +def test_dx10_r8g8b8a8_unorm_srgb() -> None: """Check DX10 unsigned normalized integer images can be opened""" with Image.open(TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB) as im: @@ -249,10 +253,11 @@ def test_dx10_r8g8b8a8_unorm_srgb(): ("L", (128, 128), TEST_FILE_UNCOMPRESSED_L), ("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA), ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB), + ("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_BGR15), ("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA), ], ) -def test_uncompressed(mode, size, test_file): +def test_uncompressed(mode: str, size: tuple[int, int], test_file: str) -> None: """Check uncompressed images can be opened""" with Image.open(test_file) as im: @@ -263,7 +268,7 @@ def test_uncompressed(mode, size, test_file): assert_image_equal_tofile(im, test_file.replace(".dds", ".png")) -def test__accept_true(): +def test__accept_true() -> None: """Check valid prefix""" # Arrange prefix = b"DDS etc" @@ -275,7 +280,7 @@ def test__accept_true(): assert output -def test__accept_false(): +def test__accept_false() -> None: """Check invalid prefix""" # Arrange prefix = b"something invalid" @@ -287,19 +292,19 @@ def test__accept_false(): assert not output -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): DdsImagePlugin.DdsImageFile(invalid_file) -def test_short_header(): +def test_short_header() -> None: """Check a short header""" with open(TEST_FILE_DXT5, "rb") as f: img_file = f.read() - def short_header(): + def short_header() -> None: with Image.open(BytesIO(img_file[:119])): pass # pragma: no cover @@ -307,13 +312,13 @@ def test_short_header(): short_header() -def test_short_file(): +def test_short_file() -> None: """Check that the appropriate error is thrown for a short file""" with open(TEST_FILE_DXT5, "rb") as f: img_file = f.read() - def short_file(): + def short_file() -> None: with Image.open(BytesIO(img_file[:-100])) as im: im.load() @@ -321,7 +326,7 @@ def test_short_file(): short_file() -def test_dxt5_colorblock_alpha_issue_4142(): +def test_dxt5_colorblock_alpha_issue_4142() -> None: """Check that colorblocks are decoded correctly in DXT5""" with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: @@ -336,21 +341,14 @@ def test_dxt5_colorblock_alpha_issue_4142(): assert px[2] != 0 -def test_palette(): +def test_palette() -> None: with Image.open("Tests/images/palette.dds") as im: assert_image_equal_tofile(im, "Tests/images/transparent.gif") -@pytest.mark.parametrize( - "test_file", - ( - "Tests/images/unsupported_bitcount_rgb.dds", - "Tests/images/unsupported_bitcount_luminance.dds", - ), -) -def test_unsupported_bitcount(test_file): +def test_unsupported_bitcount() -> None: with pytest.raises(OSError): - with Image.open(test_file): + with Image.open("Tests/images/unsupported_bitcount.dds"): pass @@ -361,13 +359,13 @@ def test_unsupported_bitcount(test_file): "Tests/images/unimplemented_pfflags.dds", ), ) -def test_not_implemented(test_file): +def test_not_implemented(test_file: str) -> None: with pytest.raises(NotImplementedError): with Image.open(test_file): pass -def test_save_unsupported_mode(tmp_path): +def test_save_unsupported_mode(tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") im = hopper("HSV") with pytest.raises(OSError): @@ -383,7 +381,7 @@ def test_save_unsupported_mode(tmp_path): ("RGBA", "Tests/images/pil123rgba.png"), ], ) -def test_save(mode, test_file, tmp_path): +def test_save(mode: str, test_file: str, tmp_path: Path) -> None: out = str(tmp_path / "temp.dds") with Image.open(test_file) as im: assert im.mode == mode diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index c479c384a..00f5f39e8 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,5 +1,7 @@ from __future__ import annotations + import io +from pathlib import Path import pytest @@ -82,7 +84,7 @@ simple_eps_file_with_long_binary_data = ( ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) ) @pytest.mark.parametrize("scale", (1, 2)) -def test_sanity(filename, size, scale): +def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: expected_size = tuple(s * scale for s in size) with Image.open(filename) as image: image.load(scale=scale) @@ -92,7 +94,7 @@ def test_sanity(filename, size, scale): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_load(): +def test_load() -> None: with Image.open(FILE1) as im: assert im.load()[0, 0] == (255, 255, 255) @@ -100,7 +102,7 @@ def test_load(): assert im.load()[0, 0] == (255, 255, 255) -def test_binary(): +def test_binary() -> None: if HAS_GHOSTSCRIPT: assert EpsImagePlugin.gs_binary is not None else: @@ -114,41 +116,41 @@ def test_binary(): assert EpsImagePlugin.gs_windows_binary is not None -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(invalid_file) -def test_binary_header_only(): +def test_binary_header_only() -> None: data = io.BytesIO(simple_binary_header) with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_version_comment(prefix): +def test_missing_version_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_boundingbox_comment(prefix): +def test_missing_boundingbox_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment(prefix): +def test_invalid_boundingbox_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) with pytest.raises(OSError, match="cannot determine EPS bounding box"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix): +def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None: data = io.BytesIO( prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) ) @@ -159,21 +161,21 @@ def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix): @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_ascii_comment_too_long(prefix): +def test_ascii_comment_too_long(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) with pytest.raises(SyntaxError, match="not an EPS file"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_long_binary_data(prefix): +def test_long_binary_data(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) EpsImagePlugin.EpsImageFile(data) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_load_long_binary_data(prefix): +def test_load_long_binary_data(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) with Image.open(data) as img: img.load() @@ -186,7 +188,7 @@ def test_load_long_binary_data(prefix): pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_cmyk(): +def test_cmyk() -> None: with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: assert cmyk_image.mode == "CMYK" assert cmyk_image.size == (100, 100) @@ -202,7 +204,7 @@ def test_cmyk(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_showpage(): +def test_showpage() -> None: # See https://github.com/python-pillow/Pillow/issues/2615 with Image.open("Tests/images/reqd_showpage.eps") as plot_image: with Image.open("Tests/images/reqd_showpage.png") as target: @@ -213,7 +215,7 @@ def test_showpage(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_transparency(): +def test_transparency() -> None: with Image.open("Tests/images/reqd_showpage.eps") as plot_image: plot_image.load(transparency=True) assert plot_image.mode == "RGBA" @@ -224,7 +226,7 @@ def test_transparency(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_file_object(tmp_path): +def test_file_object(tmp_path: Path) -> None: # issue 479 with Image.open(FILE1) as image1: with open(str(tmp_path / "temp.eps"), "wb") as fh: @@ -232,7 +234,7 @@ def test_file_object(tmp_path): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_bytesio_object(): +def test_bytesio_object() -> None: with open(FILE1, "rb") as f: img_bytes = io.BytesIO(f.read()) @@ -245,12 +247,12 @@ def test_bytesio_object(): assert_image_similar(img, image1_scale1_compare, 5) -def test_1_mode(): +def test_1_mode() -> None: with Image.open("Tests/images/1.eps") as im: assert im.mode == "1" -def test_image_mode_not_supported(tmp_path): +def test_image_mode_not_supported(tmp_path: Path) -> None: im = hopper("RGBA") tmpfile = str(tmp_path / "temp.eps") with pytest.raises(ValueError): @@ -259,7 +261,7 @@ def test_image_mode_not_supported(tmp_path): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @skip_unless_feature("zlib") -def test_render_scale1(): +def test_render_scale1() -> None: # We need png support for these render test # Zero bounding box @@ -270,7 +272,7 @@ def test_render_scale1(): image1_scale1_compare.load() assert_image_similar(image1_scale1, image1_scale1_compare, 5) - # Non-Zero bounding box + # Non-zero bounding box with Image.open(FILE2) as image2_scale1: image2_scale1.load() with Image.open(FILE2_COMPARE) as image2_scale1_compare: @@ -281,7 +283,7 @@ def test_render_scale1(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @skip_unless_feature("zlib") -def test_render_scale2(): +def test_render_scale2() -> None: # We need png support for these render test # Zero bounding box @@ -292,7 +294,7 @@ def test_render_scale2(): image1_scale2_compare.load() assert_image_similar(image1_scale2, image1_scale2_compare, 5) - # Non-Zero bounding box + # Non-zero bounding box with Image.open(FILE2) as image2_scale2: image2_scale2.load(scale=2) with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: @@ -303,7 +305,7 @@ def test_render_scale2(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) -def test_resize(filename): +def test_resize(filename: str) -> None: with Image.open(filename) as im: new_size = (100, 100) im = im.resize(new_size) @@ -312,7 +314,7 @@ def test_resize(filename): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2)) -def test_thumbnail(filename): +def test_thumbnail(filename: str) -> None: # Issue #619 with Image.open(filename) as im: new_size = (100, 100) @@ -320,20 +322,20 @@ def test_thumbnail(filename): assert max(im.size) == max(new_size) -def test_read_binary_preview(): +def test_read_binary_preview() -> None: # Issue 302 # open image with binary preview with Image.open(FILE3): pass -def test_readline_psfile(tmp_path): +def test_readline_psfile(tmp_path: Path) -> None: # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' line_endings = ["\r\n", "\n", "\n\r", "\r"] strings = ["something", "else", "baz", "bif"] - def _test_readline(t, ending): + def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None: ending = "Failure with line ending: %s" % ( "".join("%s" % ord(s) for s in ending) ) @@ -342,13 +344,13 @@ def test_readline_psfile(tmp_path): assert t.readline().strip("\r\n") == "baz", ending assert t.readline().strip("\r\n") == "bif", ending - def _test_readline_io_psfile(test_string, ending): + def _test_readline_io_psfile(test_string: str, ending: str) -> None: f = io.BytesIO(test_string.encode("latin-1")) with pytest.warns(DeprecationWarning): t = EpsImagePlugin.PSFile(f) _test_readline(t, ending) - def _test_readline_file_psfile(test_string, ending): + def _test_readline_file_psfile(test_string: str, ending: str) -> None: f = str(tmp_path / "temp.txt") with open(f, "wb") as w: w.write(test_string.encode("latin-1")) @@ -364,7 +366,7 @@ def test_readline_psfile(tmp_path): _test_readline_file_psfile(s, ending) -def test_psfile_deprecation(): +def test_psfile_deprecation() -> None: with pytest.warns(DeprecationWarning): EpsImagePlugin.PSFile(None) @@ -374,7 +376,7 @@ def test_psfile_deprecation(): "line_ending", (b"\r\n", b"\n", b"\n\r", b"\r"), ) -def test_readline(prefix, line_ending): +def test_readline(prefix: bytes, line_ending: bytes) -> None: simple_file = prefix + line_ending.join(simple_eps_file_with_comments) data = io.BytesIO(simple_file) test_file = EpsImagePlugin.EpsImageFile(data) @@ -392,14 +394,14 @@ def test_readline(prefix, line_ending): "Tests/images/illuCS6_preview.eps", ), ) -def test_open_eps(filename): +def test_open_eps(filename: str) -> None: # https://github.com/python-pillow/Pillow/issues/1104 with Image.open(filename) as img: assert img.mode == "RGB" @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_emptyline(): +def test_emptyline() -> None: # Test file includes an empty line in the header data emptyline_file = "Tests/images/zero_bb_emptyline.eps" @@ -415,14 +417,14 @@ def test_emptyline(): "test_file", ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ) -def test_timeout(test_file): +def test_timeout(test_file: str) -> None: with open(test_file, "rb") as f: with pytest.raises(Image.UnidentifiedImageError): with Image.open(f): pass -def test_bounding_box_in_trailer(): +def test_bounding_box_in_trailer() -> None: # Check bounding boxes are parsed in the same way # when specified in the header and the trailer with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open( @@ -431,7 +433,7 @@ def test_bounding_box_in_trailer(): assert trailer_image.size == header_image.size -def test_eof_before_bounding_box(): +def test_eof_before_bounding_box() -> None: with pytest.raises(OSError): with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"): pass diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 1383f9c5c..cce0b05cd 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest @@ -10,7 +11,7 @@ from .helper import assert_image_equal, hopper TEST_FILE = "Tests/images/hopper.fits" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -21,7 +22,7 @@ def test_open(): assert_image_equal(im, hopper("L")) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -30,14 +31,14 @@ def test_invalid_file(): FitsImagePlugin.FitsImageFile(invalid_file) -def test_truncated_fits(): +def test_truncated_fits() -> None: # 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(): +def test_naxis_zero() -> None: # This test image has been manually hexedited # to set the number of data axes to zero with pytest.raises(ValueError): @@ -45,7 +46,7 @@ def test_naxis_zero(): pass -def test_comment(): +def test_comment() -> None: image_data = b"SIMPLE = T / comment string" with pytest.raises(OSError): FitsImagePlugin.FitsImageFile(BytesIO(image_data)) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 10bf36cc2..f86fb8d09 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,9 +1,10 @@ from __future__ import annotations + import warnings import pytest -from PIL import FliImagePlugin, Image +from PIL import FliImagePlugin, Image, ImageFile from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy @@ -11,11 +12,14 @@ from .helper import assert_image_equal, assert_image_equal_tofile, is_pypy # save as...-> hopper.fli, default options. static_test_file = "Tests/images/hopper.fli" -# From https://samples.libav.org/fli-flc/ +# From https://samples.ffmpeg.org/fli-flc/ animated_test_file = "Tests/images/a.fli" +# From https://samples.ffmpeg.org/fli-flc/ +animated_test_file_with_prefix_chunk = "Tests/images/2422.flc" -def test_sanity(): + +def test_sanity() -> None: with Image.open(static_test_file) as im: im.load() assert im.mode == "P" @@ -31,9 +35,27 @@ def test_sanity(): assert im.is_animated +def test_prefix_chunk() -> None: + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + with Image.open(animated_test_file_with_prefix_chunk) as im: + assert im.mode == "P" + assert im.size == (320, 200) + assert im.format == "FLI" + assert im.info["duration"] == 171 + assert im.is_animated + + palette = im.getpalette() + assert palette[3:6] == [255, 255, 255] + assert palette[381:384] == [204, 204, 12] + assert palette[765:] == [252, 0, 0] + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + + @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(static_test_file) im.load() @@ -41,14 +63,14 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(static_test_file) im.load() im.close() -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open(animated_test_file) im.seek(1) im.close() @@ -57,13 +79,13 @@ def test_seek_after_close(): im.seek(0) -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(static_test_file) as im: im.load() -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(static_test_file) as im: # Act @@ -73,20 +95,20 @@ def test_tell(): assert frame == 0 -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): FliImagePlugin.FliImageFile(invalid_file) -def test_palette_chunk_second(): +def test_palette_chunk_second() -> None: with Image.open("Tests/images/hopper_palette_chunk_second.fli") as im: with Image.open(static_test_file) as expected: assert_image_equal(im.convert("RGB"), expected.convert("RGB")) -def test_n_frames(): +def test_n_frames() -> None: with Image.open(static_test_file) as im: assert im.n_frames == 1 assert not im.is_animated @@ -96,7 +118,7 @@ def test_n_frames(): assert im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(animated_test_file) as im: n_frames = im.n_frames @@ -109,7 +131,7 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_seek_tell(): +def test_seek_tell() -> None: with Image.open(animated_test_file) as im: layer_number = im.tell() assert layer_number == 0 @@ -131,7 +153,7 @@ def test_seek_tell(): assert layer_number == 1 -def test_seek(): +def test_seek() -> None: with Image.open(animated_test_file) as im: im.seek(50) @@ -146,7 +168,7 @@ def test_seek(): ], ) @pytest.mark.timeout(timeout=3) -def test_timeouts(test_file): +def test_timeouts(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): @@ -159,7 +181,7 @@ def test_timeouts(test_file): "Tests/images/crash-5762152299364352.fli", ], ) -def test_crash(test_file): +def test_crash(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index af3b79815..e32f30a01 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image @@ -10,7 +11,7 @@ FpxImagePlugin = pytest.importorskip( ) -def test_sanity(): +def test_sanity() -> None: with Image.open("Tests/images/input_bw_one_band.fpx") as im: assert im.mode == "L" assert im.size == (70, 46) @@ -19,7 +20,7 @@ def test_sanity(): assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") -def test_close(): +def test_close() -> None: with Image.open("Tests/images/input_bw_one_band.fpx") as im: pass assert im.ole.fp.closed @@ -29,7 +30,7 @@ def test_close(): assert im.ole.fp.closed -def test_invalid_file(): +def test_invalid_file() -> None: # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): @@ -41,7 +42,7 @@ def test_invalid_file(): FpxImagePlugin.FpxImageFile(ole_file) -def test_fpx_invalid_number_of_bands(): +def test_fpx_invalid_number_of_bands() -> None: with pytest.raises(OSError, match="Invalid number of bands"): with Image.open("Tests/images/input_bw_five_bands.fpx"): pass diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index a494c8029..0c544245a 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import FtexImagePlugin, Image @@ -6,18 +7,18 @@ from PIL import FtexImagePlugin, Image from .helper import assert_image_equal_tofile, assert_image_similar -def test_load_raw(): +def test_load_raw() -> None: with Image.open("Tests/images/ftex_uncompressed.ftu") as im: assert_image_equal_tofile(im, "Tests/images/ftex_uncompressed.png") -def test_load_dxt1(): +def test_load_dxt1() -> None: 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(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index 7dfe05396..be98b08f2 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import GbrImagePlugin, Image @@ -6,12 +7,12 @@ from PIL import GbrImagePlugin, Image from .helper import assert_image_equal_tofile -def test_gbr_file(): +def test_gbr_file() -> None: with Image.open("Tests/images/gbr.gbr") as im: assert_image_equal_tofile(im, "Tests/images/gbr.png") -def test_load(): +def test_load() -> None: with Image.open("Tests/images/gbr.gbr") as im: assert im.load()[0, 0] == (0, 0, 0, 0) @@ -19,14 +20,14 @@ def test_load(): assert im.load()[0, 0] == (0, 0, 0, 0) -def test_multiple_load_operations(): +def test_multiple_load_operations() -> None: 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(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index ec80c54a1..d512df284 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import GdImageFile, UnidentifiedImageError @@ -6,18 +7,18 @@ from PIL import GdImageFile, UnidentifiedImageError TEST_GD_FILE = "Tests/images/hopper.gd" -def test_sanity(): +def test_sanity() -> None: with GdImageFile.open(TEST_GD_FILE) as im: assert im.size == (128, 128) assert im.format == "GD" -def test_bad_mode(): +def test_bad_mode() -> None: with pytest.raises(ValueError): GdImageFile.open(TEST_GD_FILE, "bad mode") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(UnidentifiedImageError): diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 78b77e974..db9d3586c 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,6 +1,9 @@ from __future__ import annotations + import warnings from io import BytesIO +from pathlib import Path +from typing import Generator import pytest @@ -22,7 +25,7 @@ with open(TEST_GIF, "rb") as f: data = f.read() -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_GIF) as im: im.load() assert im.mode == "P" @@ -32,8 +35,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_GIF) im.load() @@ -41,14 +44,14 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_GIF) im.load() im.close() -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open("Tests/images/iss634.gif") im.load() im.close() @@ -61,20 +64,20 @@ def test_seek_after_close(): im.seek(1) -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_GIF) as im: im.load() -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): GifImagePlugin.GifImageFile(invalid_file) -def test_l_mode_transparency(): +def test_l_mode_transparency() -> None: with Image.open("Tests/images/no_palette_with_transparency.gif") as im: assert im.mode == "L" assert im.load()[0, 0] == 128 @@ -85,7 +88,7 @@ def test_l_mode_transparency(): assert im.load()[0, 0] == 128 -def test_l_mode_after_rgb(): +def test_l_mode_after_rgb() -> None: with Image.open("Tests/images/no_palette_after_rgb.gif") as im: im.seek(1) assert im.mode == "RGB" @@ -94,13 +97,13 @@ def test_l_mode_after_rgb(): assert im.mode == "RGB" -def test_palette_not_needed_for_second_frame(): +def test_palette_not_needed_for_second_frame() -> None: with Image.open("Tests/images/palette_not_needed_for_second_frame.gif") as im: im.seek(1) assert_image_similar(im, hopper("L").convert("RGB"), 8) -def test_strategy(): +def test_strategy() -> None: with Image.open("Tests/images/iss634.gif") as im: expected_rgb_always = im.convert("RGB") @@ -141,14 +144,14 @@ def test_strategy(): GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST -def test_optimize(): - def test_grayscale(optimize): +def test_optimize() -> None: + def test_grayscale(optimize: int) -> int: im = Image.new("L", (1, 1), 0) filename = BytesIO() im.save(filename, "GIF", optimize=optimize) return len(filename.getvalue()) - def test_bilevel(optimize): + def test_bilevel(optimize: int) -> int: im = Image.new("1", (1, 1), 0) test_file = BytesIO() im.save(test_file, "GIF", optimize=optimize) @@ -176,7 +179,9 @@ def test_optimize(): (4, 513, 256), ), ) -def test_optimize_correctness(colors, size, expected_palette_length): +def test_optimize_correctness( + colors: int, size: int, expected_palette_length: int +) -> None: # 256 color Palette image, posterize to > 128 and < 128 levels. # Size bigger and smaller than 512x512. # Check the palette for number of colors allocated. @@ -198,14 +203,14 @@ def test_optimize_correctness(colors, size, expected_palette_length): assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) -def test_optimize_full_l(): +def test_optimize_full_l() -> None: im = Image.frombytes("L", (16, 16), bytes(range(256))) test_file = BytesIO() im.save(test_file, "GIF", optimize=True) assert im.mode == "L" -def test_optimize_if_palette_can_be_reduced_by_half(): +def test_optimize_if_palette_can_be_reduced_by_half() -> None: im = Image.new("P", (8, 1)) im.palette = ImagePalette.raw("RGB", bytes((0, 0, 0) * 150)) for i in range(8): @@ -218,7 +223,7 @@ def test_optimize_if_palette_can_be_reduced_by_half(): assert len(reloaded.palette.palette) // 3 == colors -def test_full_palette_second_frame(tmp_path): +def test_full_palette_second_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("P", (1, 256)) @@ -239,7 +244,7 @@ def test_full_palette_second_frame(tmp_path): reloaded.getpixel((0, i)) == i -def test_roundtrip(tmp_path): +def test_roundtrip(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = hopper() im.save(out) @@ -247,7 +252,7 @@ def test_roundtrip(tmp_path): assert_image_similar(reread.convert("RGB"), im, 50) -def test_roundtrip2(tmp_path): +def test_roundtrip2(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/403 out = str(tmp_path / "temp.gif") with Image.open(TEST_GIF) as im: @@ -257,7 +262,7 @@ def test_roundtrip2(tmp_path): assert_image_similar(reread.convert("RGB"), hopper(), 50) -def test_roundtrip_save_all(tmp_path): +def test_roundtrip_save_all(tmp_path: Path) -> None: # Single frame image out = str(tmp_path / "temp.gif") im = hopper() @@ -274,7 +279,7 @@ def test_roundtrip_save_all(tmp_path): assert reread.n_frames == 5 -def test_roundtrip_save_all_1(tmp_path): +def test_roundtrip_save_all_1(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("1", (1, 1)) im2 = Image.new("1", (1, 1), 1) @@ -295,7 +300,7 @@ def test_roundtrip_save_all_1(tmp_path): ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), ), ) -def test_loading_multiple_palettes(path, mode): +def test_loading_multiple_palettes(path: str, mode: str) -> None: with Image.open(path) as im: assert im.mode == "P" first_frame_colors = im.palette.colors.keys() @@ -313,7 +318,7 @@ def test_loading_multiple_palettes(path, mode): assert im.load()[24, 24] not in first_frame_colors -def test_headers_saving_for_animated_gifs(tmp_path): +def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: important_headers = ["background", "version", "duration", "loop"] # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: @@ -326,7 +331,7 @@ def test_headers_saving_for_animated_gifs(tmp_path): assert info[header] == reread.info[header] -def test_palette_handling(tmp_path): +def test_palette_handling(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/513 with Image.open(TEST_GIF) as im: @@ -342,12 +347,12 @@ def test_palette_handling(tmp_path): assert_image_similar(im, reloaded.convert("RGB"), 10) -def test_palette_434(tmp_path): +def test_palette_434(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/434 - def roundtrip(im, *args, **kwargs): + def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: out = str(tmp_path / "temp.gif") - im.copy().save(out, *args, **kwargs) + im.copy().save(out, **kwargs) reloaded = Image.open(out) return reloaded @@ -367,7 +372,7 @@ def test_palette_434(tmp_path): @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") -def test_save_netpbm_bmp_mode(tmp_path): +def test_save_netpbm_bmp_mode(tmp_path: Path) -> None: with Image.open(TEST_GIF) as img: img = img.convert("RGB") @@ -378,7 +383,7 @@ def test_save_netpbm_bmp_mode(tmp_path): @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") -def test_save_netpbm_l_mode(tmp_path): +def test_save_netpbm_l_mode(tmp_path: Path) -> None: with Image.open(TEST_GIF) as img: img = img.convert("L") @@ -388,7 +393,7 @@ def test_save_netpbm_l_mode(tmp_path): assert_image_similar(img, reloaded.convert("L"), 0) -def test_seek(): +def test_seek() -> None: with Image.open("Tests/images/dispose_none.gif") as img: frame_count = 0 try: @@ -399,7 +404,7 @@ def test_seek(): assert frame_count == 5 -def test_seek_info(): +def test_seek_info() -> None: with Image.open("Tests/images/iss634.gif") as im: info = im.info.copy() @@ -409,7 +414,7 @@ def test_seek_info(): assert im.info == info -def test_seek_rewind(): +def test_seek_rewind() -> None: with Image.open("Tests/images/iss634.gif") as im: im.seek(2) im.seek(1) @@ -427,7 +432,7 @@ def test_seek_rewind(): ("Tests/images/iss634.gif", 42), ), ) -def test_n_frames(path, n_frames): +def test_n_frames(path: str, n_frames: int) -> None: # Test is_animated before n_frames with Image.open(path) as im: assert im.is_animated == (n_frames != 1) @@ -438,7 +443,7 @@ def test_n_frames(path, n_frames): assert im.is_animated == (n_frames != 1) -def test_no_change(): +def test_no_change() -> None: # Test n_frames does not change the image with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(1) @@ -459,7 +464,7 @@ def test_no_change(): assert_image_equal(im, expected) -def test_eoferror(): +def test_eoferror() -> None: with Image.open(TEST_GIF) as im: n_frames = im.n_frames @@ -472,13 +477,13 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_first_frame_transparency(): +def test_first_frame_transparency() -> None: with Image.open("Tests/images/first_frame_transparency.gif") as im: px = im.load() assert px[0, 0] == im.info["transparency"] -def test_dispose_none(): +def test_dispose_none() -> None: with Image.open("Tests/images/dispose_none.gif") as img: try: while True: @@ -488,7 +493,7 @@ def test_dispose_none(): pass -def test_dispose_none_load_end(): +def test_dispose_none_load_end() -> None: # Test image created with: # # im = Image.open("transparent.gif") @@ -501,7 +506,7 @@ def test_dispose_none_load_end(): assert_image_equal_tofile(img, "Tests/images/dispose_none_load_end_second.png") -def test_dispose_background(): +def test_dispose_background() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as img: try: while True: @@ -511,7 +516,7 @@ def test_dispose_background(): pass -def test_dispose_background_transparency(): +def test_dispose_background_transparency() -> None: with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: img.seek(2) px = img.load() @@ -539,7 +544,10 @@ def test_dispose_background_transparency(): ), ), ) -def test_transparent_dispose(loading_strategy, expected_colors): +def test_transparent_dispose( + loading_strategy: GifImagePlugin.LoadingStrategy, + expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]], +) -> None: GifImagePlugin.LOADING_STRATEGY = loading_strategy try: with Image.open("Tests/images/transparent_dispose.gif") as img: @@ -552,7 +560,7 @@ def test_transparent_dispose(loading_strategy, expected_colors): GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST -def test_dispose_previous(): +def test_dispose_previous() -> None: with Image.open("Tests/images/dispose_prev.gif") as img: try: while True: @@ -562,7 +570,7 @@ def test_dispose_previous(): pass -def test_dispose_previous_first_frame(): +def test_dispose_previous_first_frame() -> None: with Image.open("Tests/images/dispose_prev_first_frame.gif") as im: im.seek(1) assert_image_equal_tofile( @@ -570,7 +578,7 @@ def test_dispose_previous_first_frame(): ) -def test_previous_frame_loaded(): +def test_previous_frame_loaded() -> None: with Image.open("Tests/images/dispose_none.gif") as img: img.load() img.seek(1) @@ -581,7 +589,7 @@ def test_previous_frame_loaded(): assert_image_equal(img_skipped, img) -def test_save_dispose(tmp_path): +def test_save_dispose(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [ Image.new("L", (100, 100), "#000"), @@ -609,7 +617,7 @@ def test_save_dispose(tmp_path): assert img.disposal_method == i + 1 -def test_dispose2_palette(tmp_path): +def test_dispose2_palette(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Four colors: white, gray, black, red @@ -640,7 +648,7 @@ def test_dispose2_palette(tmp_path): assert rgb_img.getpixel((50, 50)) == circle -def test_dispose2_diff(tmp_path): +def test_dispose2_diff(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # 4 frames: red/blue, red/red, blue/blue, red/blue @@ -682,7 +690,7 @@ def test_dispose2_diff(tmp_path): assert rgb_img.getpixel((1, 1)) == (255, 255, 255, 0) -def test_dispose2_background(tmp_path): +def test_dispose2_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [] @@ -708,7 +716,7 @@ def test_dispose2_background(tmp_path): assert im.getpixel((0, 0)) == (255, 0, 0) -def test_dispose2_background_frame(tmp_path): +def test_dispose2_background_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im_list = [Image.new("RGBA", (1, 20))] @@ -726,7 +734,7 @@ def test_dispose2_background_frame(tmp_path): assert im.n_frames == 3 -def test_transparency_in_second_frame(tmp_path): +def test_transparency_in_second_frame(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/different_transparency.gif") as im: assert im.info["transparency"] == 0 @@ -746,7 +754,7 @@ def test_transparency_in_second_frame(tmp_path): ) -def test_no_transparency_in_second_frame(): +def test_no_transparency_in_second_frame() -> None: with Image.open("Tests/images/iss634.gif") as img: # Seek to the second frame img.seek(img.tell() + 1) @@ -756,7 +764,7 @@ def test_no_transparency_in_second_frame(): assert img.histogram()[255] == 0 -def test_remapped_transparency(tmp_path): +def test_remapped_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("P", (1, 2)) @@ -772,7 +780,7 @@ def test_remapped_transparency(tmp_path): assert reloaded.info["transparency"] == reloaded.getpixel((0, 1)) -def test_duration(tmp_path): +def test_duration(tmp_path: Path) -> None: duration = 1000 out = str(tmp_path / "temp.gif") @@ -786,7 +794,7 @@ def test_duration(tmp_path): assert reread.info["duration"] == duration -def test_multiple_duration(tmp_path): +def test_multiple_duration(tmp_path: Path) -> None: duration_list = [1000, 2000, 3000] out = str(tmp_path / "temp.gif") @@ -821,7 +829,7 @@ def test_multiple_duration(tmp_path): pass -def test_roundtrip_info_duration(tmp_path): +def test_roundtrip_info_duration(tmp_path: Path) -> None: duration_list = [100, 500, 500] out = str(tmp_path / "temp.gif") @@ -838,7 +846,7 @@ def test_roundtrip_info_duration(tmp_path): ] == duration_list -def test_roundtrip_info_duration_combined(tmp_path): +def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/duplicate_frame.gif") as im: assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ @@ -854,7 +862,7 @@ def test_roundtrip_info_duration_combined(tmp_path): ] == [1000, 2000] -def test_identical_frames(tmp_path): +def test_identical_frames(tmp_path: Path) -> None: duration_list = [1000, 1500, 2000, 4000] out = str(tmp_path / "temp.gif") @@ -887,7 +895,9 @@ def test_identical_frames(tmp_path): 1500, ), ) -def test_identical_frames_to_single_frame(duration, tmp_path): +def test_identical_frames_to_single_frame( + duration: int | list[int], tmp_path: Path +) -> None: out = str(tmp_path / "temp.gif") im_list = [ Image.new("L", (100, 100), "#000"), @@ -904,7 +914,7 @@ def test_identical_frames_to_single_frame(duration, tmp_path): assert reread.info["duration"] == 4500 -def test_loop_none(tmp_path): +def test_loop_none(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("L", (100, 100), "#000") im.save(out, loop=None) @@ -912,7 +922,7 @@ def test_loop_none(tmp_path): assert "loop" not in reread.info -def test_number_of_loops(tmp_path): +def test_number_of_loops(tmp_path: Path) -> None: number_of_loops = 2 out = str(tmp_path / "temp.gif") @@ -930,7 +940,7 @@ def test_number_of_loops(tmp_path): assert im.info["loop"] == 2 -def test_background(tmp_path): +def test_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("L", (100, 100), "#000") im.info["background"] = 1 @@ -939,7 +949,7 @@ def test_background(tmp_path): assert reread.info["background"] == im.info["background"] -def test_webp_background(tmp_path): +def test_webp_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Test opaque WebP background @@ -954,7 +964,7 @@ def test_webp_background(tmp_path): im.save(out) -def test_comment(tmp_path): +def test_comment(tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" @@ -974,7 +984,7 @@ def test_comment(tmp_path): assert reread.info["version"] == b"GIF89a" -def test_comment_over_255(tmp_path): +def test_comment_over_255(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("L", (100, 100), "#000") comment = b"Test comment text" @@ -989,18 +999,18 @@ def test_comment_over_255(tmp_path): assert reread.info["version"] == b"GIF89a" -def test_zero_comment_subblocks(): +def test_zero_comment_subblocks() -> None: with Image.open("Tests/images/hopper_zero_comment_subblocks.gif") as im: assert_image_equal_tofile(im, TEST_GIF) -def test_read_multiple_comment_blocks(): +def test_read_multiple_comment_blocks() -> None: with Image.open("Tests/images/multiple_comments.gif") as im: # Multiple comment blocks in a frame are separated not concatenated assert im.info["comment"] == b"Test comment 1\nTest comment 2" -def test_empty_string_comment(tmp_path): +def test_empty_string_comment(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/chi.gif") as im: assert "comment" in im.info @@ -1013,7 +1023,7 @@ def test_empty_string_comment(tmp_path): assert "comment" not in frame.info -def test_retain_comment_in_subsequent_frames(tmp_path): +def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: # Test that a comment block at the beginning is kept with Image.open("Tests/images/chi.gif") as im: for frame in ImageSequence.Iterator(im): @@ -1044,10 +1054,10 @@ def test_retain_comment_in_subsequent_frames(tmp_path): assert frame.info["comment"] == b"Test" -def test_version(tmp_path): +def test_version(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") - def assert_version_after_save(im, version): + def assert_version_after_save(im: Image.Image, version: bytes) -> None: im.save(out) with Image.open(out) as reread: assert reread.info["version"] == version @@ -1074,7 +1084,7 @@ def test_version(tmp_path): assert_version_after_save(im, b"GIF87a") -def test_append_images(tmp_path): +def test_append_images(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Test appending single frame images @@ -1086,7 +1096,7 @@ def test_append_images(tmp_path): assert reread.n_frames == 3 # Tests appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims im.save(out, save_all=True, append_images=im_generator(ims)) @@ -1103,7 +1113,7 @@ def test_append_images(tmp_path): assert reread.n_frames == 10 -def test_transparent_optimize(tmp_path): +def test_transparent_optimize(tmp_path: Path) -> None: # From issue #2195, if the transparent color is incorrectly optimized out, GIF loses # transparency. # Need a palette that isn't using the 0 color, @@ -1123,7 +1133,7 @@ def test_transparent_optimize(tmp_path): assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) -def test_removed_transparency(tmp_path): +def test_removed_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("RGB", (256, 1)) @@ -1138,7 +1148,7 @@ def test_removed_transparency(tmp_path): assert "transparency" not in reloaded.info -def test_rgb_transparency(tmp_path): +def test_rgb_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Single frame @@ -1160,7 +1170,7 @@ def test_rgb_transparency(tmp_path): assert "transparency" not in reloaded.info -def test_rgba_transparency(tmp_path): +def test_rgba_transparency(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = hopper("P") @@ -1171,13 +1181,13 @@ def test_rgba_transparency(tmp_path): assert_image_equal(hopper("P").convert("RGB"), reloaded) -def test_background_outside_palettte(tmp_path): +def test_background_outside_palettte(tmp_path: Path) -> None: with Image.open("Tests/images/background_outside_palette.gif") as im: im.seek(1) assert im.info["background"] == 255 -def test_bbox(tmp_path): +def test_bbox(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("RGB", (100, 100), "#fff") @@ -1188,7 +1198,7 @@ def test_bbox(tmp_path): assert reread.n_frames == 2 -def test_bbox_alpha(tmp_path): +def test_bbox_alpha(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) @@ -1200,7 +1210,7 @@ def test_bbox_alpha(tmp_path): assert reread.n_frames == 2 -def test_palette_save_L(tmp_path): +def test_palette_save_L(tmp_path: Path) -> None: # Generate an L mode image with a separate palette im = hopper("P") @@ -1214,7 +1224,7 @@ def test_palette_save_L(tmp_path): assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) -def test_palette_save_P(tmp_path): +def test_palette_save_P(tmp_path: Path) -> None: im = Image.new("P", (1, 2)) im.putpixel((0, 1), 1) @@ -1228,7 +1238,7 @@ def test_palette_save_P(tmp_path): assert reloaded_rgb.getpixel((0, 1)) == (4, 5, 6) -def test_palette_save_duplicate_entries(tmp_path): +def test_palette_save_duplicate_entries(tmp_path: Path) -> None: im = Image.new("P", (1, 2)) im.putpixel((0, 1), 1) @@ -1241,7 +1251,7 @@ def test_palette_save_duplicate_entries(tmp_path): assert reloaded.convert("RGB").getpixel((0, 1)) == (0, 0, 0) -def test_palette_save_all_P(tmp_path): +def test_palette_save_all_P(tmp_path: Path) -> None: frames = [] colors = ((255, 0, 0), (0, 255, 0)) for color in colors: @@ -1264,7 +1274,7 @@ def test_palette_save_all_P(tmp_path): assert im.palette.palette == im.global_palette.palette -def test_palette_save_ImagePalette(tmp_path): +def test_palette_save_ImagePalette(tmp_path: Path) -> None: # Pass in a different palette, as an ImagePalette.ImagePalette # effectively the same as test_palette_save_P @@ -1279,7 +1289,7 @@ def test_palette_save_ImagePalette(tmp_path): assert_image_equal(reloaded.convert("RGB"), im.convert("RGB")) -def test_save_I(tmp_path): +def test_save_I(tmp_path: Path) -> None: # Test saving something that would trigger the auto-convert to 'L' im = hopper("I") @@ -1291,7 +1301,7 @@ def test_save_I(tmp_path): assert_image_equal(reloaded.convert("L"), im.convert("L")) -def test_getdata(): +def test_getdata() -> None: # Test getheader/getdata against legacy values. # Create a 'P' image with holes in the palette. im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST) @@ -1319,7 +1329,7 @@ def test_getdata(): GifImagePlugin._FORCE_OPTIMIZE = False -def test_lzw_bits(): +def test_lzw_bits() -> None: # see https://github.com/python-pillow/Pillow/issues/2811 with Image.open("Tests/images/issue_2811.gif") as im: assert im.tile[0][3][0] == 11 # LZW bits @@ -1327,7 +1337,7 @@ def test_lzw_bits(): im.load() -def test_extents(): +def test_extents() -> None: with Image.open("Tests/images/test_extents.gif") as im: assert im.size == (100, 100) @@ -1339,7 +1349,7 @@ def test_extents(): assert im.size == (150, 150) -def test_missing_background(): +def test_missing_background() -> None: # The Global Color Table Flag isn't set, so there is no background color index, # but the disposal method is "Restore to background color" with Image.open("Tests/images/missing_background.gif") as im: @@ -1347,7 +1357,7 @@ def test_missing_background(): assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.png") -def test_saving_rgba(tmp_path): +def test_saving_rgba(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") with Image.open("Tests/images/transparent.png") as im: im.save(out) diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index d5be46dc3..006ee952d 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -1,8 +1,9 @@ from __future__ import annotations + from PIL import GimpGradientFile, ImagePalette -def test_linear_pos_le_middle(): +def test_linear_pos_le_middle() -> None: # Arrange middle = 0.5 pos = 0.25 @@ -14,7 +15,7 @@ def test_linear_pos_le_middle(): assert ret == 0.25 -def test_linear_pos_le_small_middle(): +def test_linear_pos_le_small_middle() -> None: # Arrange middle = 1e-11 pos = 1e-12 @@ -26,7 +27,7 @@ def test_linear_pos_le_small_middle(): assert ret == 0.0 -def test_linear_pos_gt_middle(): +def test_linear_pos_gt_middle() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -38,7 +39,7 @@ def test_linear_pos_gt_middle(): assert ret == 0.75 -def test_linear_pos_gt_small_middle(): +def test_linear_pos_gt_small_middle() -> None: # Arrange middle = 1 - 1e-11 pos = 1 - 1e-12 @@ -50,7 +51,7 @@ def test_linear_pos_gt_small_middle(): assert ret == 1.0 -def test_curved(): +def test_curved() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -62,7 +63,7 @@ def test_curved(): assert ret == 0.75 -def test_sine(): +def test_sine() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -74,7 +75,7 @@ def test_sine(): assert ret == 0.8535533905932737 -def test_sphere_increasing(): +def test_sphere_increasing() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -86,7 +87,7 @@ def test_sphere_increasing(): assert round(abs(ret - 0.9682458365518543), 7) == 0 -def test_sphere_decreasing(): +def test_sphere_decreasing() -> None: # Arrange middle = 0.5 pos = 0.75 @@ -98,7 +99,7 @@ def test_sphere_decreasing(): assert ret == 0.3385621722338523 -def test_load_via_imagepalette(): +def test_load_via_imagepalette() -> None: # Arrange test_file = "Tests/images/gimp_gradient.ggr" @@ -111,7 +112,7 @@ def test_load_via_imagepalette(): assert palette[1] == "RGBA" -def test_load_1_3_via_imagepalette(): +def test_load_1_3_via_imagepalette() -> None: # Arrange # GIMP 1.3 gradient files contain a name field test_file = "Tests/images/gimp_gradient_with_name.ggr" diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py index 775d3b7cd..e8d5f1705 100644 --- a/Tests/test_file_gimppalette.py +++ b/Tests/test_file_gimppalette.py @@ -1,10 +1,11 @@ from __future__ import annotations + import pytest from PIL.GimpPaletteFile import GimpPaletteFile -def test_sanity(): +def test_sanity() -> None: with open("Tests/images/test.gpl", "rb") as fp: GimpPaletteFile(fp) @@ -21,7 +22,7 @@ def test_sanity(): GimpPaletteFile(fp) -def test_get_palette(): +def test_get_palette() -> None: # Arrange with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: palette_file = GimpPaletteFile(fp) diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index d962e85a4..096a5b88b 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,4 +1,8 @@ from __future__ import annotations + +from pathlib import Path +from typing import IO + import pytest from PIL import GribStubImagePlugin, Image @@ -8,7 +12,7 @@ from .helper import hopper TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -19,7 +23,7 @@ def test_open(): assert im.size == (1, 1) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -28,7 +32,7 @@ def test_invalid_file(): GribStubImagePlugin.GribStubImageFile(invalid_file) -def test_load(): +def test_load() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler @@ -36,7 +40,7 @@ def test_load(): im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: # Arrange im = hopper() tmpfile = str(tmp_path / "temp.grib") @@ -46,21 +50,21 @@ def test_save(tmp_path): im.save(tmpfile) -def test_handler(tmp_path): +def test_handler(tmp_path: Path) -> None: class TestHandler: opened = False loaded = False saved = False - def open(self, im): + def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im): + def load(self, im: Image.Image) -> Image.Image: self.loaded = True im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename): + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 9c776b712..f871e2eff 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,4 +1,8 @@ from __future__ import annotations + +from pathlib import Path +from typing import IO + import pytest from PIL import Hdf5StubImagePlugin, Image @@ -6,7 +10,7 @@ from PIL import Hdf5StubImagePlugin, Image TEST_FILE = "Tests/images/hdf5.h5" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -17,7 +21,7 @@ def test_open(): assert im.size == (1, 1) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" @@ -26,7 +30,7 @@ def test_invalid_file(): Hdf5StubImagePlugin.HDF5StubImageFile(invalid_file) -def test_load(): +def test_load() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act / Assert: stub cannot load without an implemented handler @@ -34,7 +38,7 @@ def test_load(): im.load() -def test_save(): +def test_save() -> None: # Arrange with Image.open(TEST_FILE) as im: dummy_fp = None @@ -47,21 +51,21 @@ def test_save(): Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename) -def test_handler(tmp_path): +def test_handler(tmp_path: Path) -> None: class TestHandler: opened = False loaded = False saved = False - def open(self, im): + def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im): + def load(self, im: Image.Image) -> Image.Image: self.loaded = True im.fp.close() return Image.new("RGB", (1, 1)) - def save(self, im, fp, filename): + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: self.saved = True handler = TestHandler() diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index c62fffc5b..488984aef 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,7 +1,9 @@ from __future__ import annotations + import io import os import warnings +from pathlib import Path import pytest @@ -13,7 +15,7 @@ from .helper import assert_image_equal, assert_image_similar_tofile, skip_unless TEST_FILE = "Tests/images/pillow.icns" -def test_sanity(): +def test_sanity() -> None: # Loading this icon by default should result in the largest size # (512x512@2x) being loaded with Image.open(TEST_FILE) as im: @@ -26,7 +28,7 @@ def test_sanity(): assert im.format == "ICNS" -def test_load(): +def test_load() -> None: with Image.open(TEST_FILE) as im: assert im.load()[0, 0] == (0, 0, 0, 0) @@ -34,7 +36,7 @@ def test_load(): assert im.load()[0, 0] == (0, 0, 0, 0) -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.icns") with Image.open(TEST_FILE) as im: @@ -51,7 +53,7 @@ def test_save(tmp_path): assert _binary.i32be(fp.read(4)) == file_length -def test_save_append_images(tmp_path): +def test_save_append_images(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.icns") provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) @@ -66,7 +68,7 @@ def test_save_append_images(tmp_path): assert_image_equal(reread, provided_im) -def test_save_fp(): +def test_save_fp() -> None: fp = io.BytesIO() with Image.open(TEST_FILE) as im: @@ -78,7 +80,7 @@ def test_save_fp(): assert reread.format == "ICNS" -def test_sizes(): +def test_sizes() -> None: # Check that we can load all of the sizes, and that the final pixel # dimensions are as expected with Image.open(TEST_FILE) as im: @@ -95,7 +97,7 @@ def test_sizes(): im.size = (1, 1) -def test_older_icon(): +def test_older_icon() -> None: # This icon was made with Icon Composer rather than iconutil; it still # uses PNG rather than JP2, however (since it was made on 10.9). with Image.open("Tests/images/pillow2.icns") as im: @@ -110,7 +112,7 @@ def test_older_icon(): @skip_unless_feature("jpg_2000") -def test_jp2_icon(): +def test_jp2_icon() -> None: # 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. @@ -126,7 +128,7 @@ def test_jp2_icon(): assert im2.size == (wr, hr) -def test_getimage(): +def test_getimage() -> None: with open(TEST_FILE, "rb") as fp: icns_file = IcnsImagePlugin.IcnsFile(fp) @@ -139,14 +141,14 @@ def test_getimage(): assert im.size == (512, 512) -def test_not_an_icns_file(): +def test_not_an_icns_file() -> None: with io.BytesIO(b"invalid\n") as fp: with pytest.raises(SyntaxError): IcnsImagePlugin.IcnsFile(fp) @skip_unless_feature("jpg_2000") -def test_icns_decompression_bomb(): +def test_icns_decompression_bomb() -> None: with Image.open( "Tests/images/oom-8ed3316a4109213ca96fb8a256a0bfefdece1461.icns" ) as im: diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index de9fa353a..f69a290fa 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,6 +1,8 @@ from __future__ import annotations + import io import os +from pathlib import Path import pytest @@ -11,7 +13,7 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper TEST_ICO_FILE = "Tests/images/hopper.ico" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_ICO_FILE) as im: im.load() assert im.mode == "RGBA" @@ -20,29 +22,29 @@ def test_sanity(): assert im.get_format_mimetype() == "image/x-icon" -def test_load(): +def test_load() -> None: with Image.open(TEST_ICO_FILE) as im: assert im.load()[0, 0] == (1, 1, 9, 255) -def test_mask(): +def test_mask() -> None: 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(): +def test_black_and_white() -> None: with Image.open("Tests/images/black_and_white.ico") as im: assert im.mode == "RGBA" assert im.size == (16, 16) -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): IcoImagePlugin.IcoImageFile(fp) -def test_save_to_bytes(): +def test_save_to_bytes() -> None: output = io.BytesIO() im = hopper() im.save(output, "ico", sizes=[(32, 32), (64, 64)]) @@ -72,7 +74,7 @@ def test_save_to_bytes(): ) -def test_getpixel(tmp_path): +def test_getpixel(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.ico") im = hopper() @@ -85,7 +87,7 @@ def test_getpixel(tmp_path): assert reloaded.getpixel((0, 0)) == (18, 20, 62) -def test_no_duplicates(tmp_path): +def test_no_duplicates(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.ico") temp_file2 = str(tmp_path / "temp2.ico") @@ -99,7 +101,7 @@ def test_no_duplicates(tmp_path): assert os.path.getsize(temp_file) == os.path.getsize(temp_file2) -def test_different_bit_depths(tmp_path): +def test_different_bit_depths(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.ico") temp_file2 = str(tmp_path / "temp2.ico") @@ -133,7 +135,7 @@ def test_different_bit_depths(tmp_path): @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) -def test_save_to_bytes_bmp(mode): +def test_save_to_bytes_bmp(mode: str) -> None: output = io.BytesIO() im = hopper(mode) im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) @@ -161,13 +163,13 @@ def test_save_to_bytes_bmp(mode): assert_image_equal(reloaded, im) -def test_incorrect_size(): +def test_incorrect_size() -> None: with Image.open(TEST_ICO_FILE) as im: with pytest.raises(ValueError): im.size = (1, 1) -def test_save_256x256(tmp_path): +def test_save_256x256(tmp_path: Path) -> None: """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" # Arrange with Image.open("Tests/images/hopper_256x256.ico") as im: @@ -180,7 +182,7 @@ def test_save_256x256(tmp_path): assert im_saved.size == (256, 256) -def test_only_save_relevant_sizes(tmp_path): +def test_only_save_relevant_sizes(tmp_path: Path) -> None: """Issue #2266 https://github.com/python-pillow/Pillow/issues/2266 Should save in 16x16, 24x24, 32x32, 48x48 sizes and not in 16x16, 24x24, 32x32, 48x48, 48x48, 48x48, 48x48 sizes @@ -196,7 +198,7 @@ def test_only_save_relevant_sizes(tmp_path): assert im_saved.info["sizes"] == {(16, 16), (24, 24), (32, 32), (48, 48)} -def test_save_append_images(tmp_path): +def test_save_append_images(tmp_path: Path) -> None: # append_images should be used for scaled down versions of the image im = hopper("RGBA") provided_im = Image.new("RGBA", (32, 32), (255, 0, 0)) @@ -210,7 +212,7 @@ def test_save_append_images(tmp_path): assert_image_equal(reread, provided_im) -def test_unexpected_size(): +def test_unexpected_size() -> None: # This image has been manually hexedited to state that it is 16x32 # while the image within is still 16x16 with pytest.warns(UserWarning): @@ -218,7 +220,7 @@ def test_unexpected_size(): assert im.size == (16, 16) -def test_draw_reloaded(tmp_path): +def test_draw_reloaded(tmp_path: Path) -> None: with Image.open(TEST_ICO_FILE) as im: outfile = str(tmp_path / "temp_saved_hopper_draw.ico") diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 0cb26d06a..036965bf5 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,6 +1,8 @@ from __future__ import annotations + import filecmp import warnings +from pathlib import Path import pytest @@ -12,7 +14,7 @@ from .helper import assert_image_equal_tofile, hopper, is_pypy TEST_IM = "Tests/images/hopper.im" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_IM) as im: im.load() assert im.mode == "RGB" @@ -20,7 +22,7 @@ def test_sanity(): assert im.format == "IM" -def test_name_limit(tmp_path): +def test_name_limit(tmp_path: Path) -> None: out = str(tmp_path / ("name_limit_test" * 7 + ".im")) with Image.open(TEST_IM) as im: im.save(out) @@ -28,8 +30,8 @@ def test_name_limit(tmp_path): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_IM) im.load() @@ -37,20 +39,20 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_IM) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_IM) as im: im.load() -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(TEST_IM) as im: # Act @@ -60,13 +62,13 @@ def test_tell(): assert frame == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_IM) as im: assert im.n_frames == 1 assert not im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(TEST_IM) as im: n_frames = im.n_frames @@ -80,14 +82,14 @@ def test_eoferror(): @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) -def test_roundtrip(mode, tmp_path): +def test_roundtrip(mode: str, tmp_path: Path) -> None: out = str(tmp_path / "temp.im") im = hopper(mode) im.save(out) assert_image_equal_tofile(im, out) -def test_small_palette(tmp_path): +def test_small_palette(tmp_path: Path) -> None: im = Image.new("P", (1, 1)) colors = [0, 1, 2] im.putpalette(colors) @@ -99,19 +101,19 @@ def test_small_palette(tmp_path): assert reloaded.getpalette() == colors + [0] * 765 -def test_save_unsupported_mode(tmp_path): +def test_save_unsupported_mode(tmp_path: Path) -> None: out = str(tmp_path / "temp.im") im = hopper("HSV") with pytest.raises(ValueError): im.save(out) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): ImImagePlugin.ImImageFile(invalid_file) -def test_number(): +def test_number() -> None: assert ImImagePlugin.number("1.2") == 1.2 diff --git a/Tests/test_file_imt.py b/Tests/test_file_imt.py index 3db488558..6957dfa0a 100644 --- a/Tests/test_file_imt.py +++ b/Tests/test_file_imt.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import pytest @@ -8,13 +9,13 @@ from PIL import Image, ImtImagePlugin from .helper import assert_image_equal_tofile -def test_sanity(): +def test_sanity() -> None: with Image.open("Tests/images/bw_gradient.imt") as im: assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") @pytest.mark.parametrize("data", (b"\n", b"\n-", b"width 1\n")) -def test_invalid_file(data): +def test_invalid_file(data: bytes) -> None: with io.BytesIO(data) as fp: with pytest.raises(SyntaxError): ImtImagePlugin.ImtImageFile(fp) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index d0ecde393..88c30d468 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from io import BytesIO, StringIO @@ -6,12 +7,24 @@ import pytest from PIL import Image, IptcImagePlugin -from .helper import hopper +from .helper import assert_image_equal, hopper TEST_FILE = "Tests/images/iptc.jpg" -def test_getiptcinfo_jpg_none(): +def test_open() -> None: + expected = Image.new("L", (1, 1)) + + f = BytesIO( + b"\x1c\x03<\x00\x02\x01\x00\x1c\x03x\x00\x01\x01\x1c\x03\x14\x00\x01\x01" + b"\x1c\x03\x1e\x00\x01\x01\x1c\x08\n\x00\x01\x00" + ) + with Image.open(f) as im: + assert im.tile == [("iptc", (0, 0, 1, 1), 25, "raw")] + assert_image_equal(im, expected) + + +def test_getiptcinfo_jpg_none() -> None: # Arrange with hopper() as im: # Act @@ -21,7 +34,7 @@ def test_getiptcinfo_jpg_none(): assert iptc is None -def test_getiptcinfo_jpg_found(): +def test_getiptcinfo_jpg_found() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act @@ -33,7 +46,7 @@ def test_getiptcinfo_jpg_found(): assert iptc[(2, 101)] == b"Hungary" -def test_getiptcinfo_fotostation(): +def test_getiptcinfo_fotostation() -> None: # Arrange with open(TEST_FILE, "rb") as fp: data = bytearray(fp.read()) @@ -50,7 +63,7 @@ def test_getiptcinfo_fotostation(): pytest.fail("FotoStation tag not found") -def test_getiptcinfo_zero_padding(): +def test_getiptcinfo_zero_padding() -> None: # Arrange with Image.open(TEST_FILE) as im: im.info["photoshop"][0x0404] += b"\x00\x00\x00" @@ -63,7 +76,7 @@ def test_getiptcinfo_zero_padding(): assert len(iptc) == 3 -def test_getiptcinfo_tiff_none(): +def test_getiptcinfo_tiff_none() -> None: # Arrange with Image.open("Tests/images/hopper.tif") as im: # Act @@ -73,29 +86,33 @@ def test_getiptcinfo_tiff_none(): assert iptc is None -def test_i(): +def test_i() -> None: # Arrange c = b"a" # Act - ret = IptcImagePlugin.i(c) + with pytest.warns(DeprecationWarning): + ret = IptcImagePlugin.i(c) # Assert assert ret == 97 -def test_dump(): +def test_dump(monkeypatch: pytest.MonkeyPatch) -> None: # Arrange c = b"abc" # Temporarily redirect stdout - old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() + mystdout = StringIO() + monkeypatch.setattr(sys, "stdout", mystdout) # Act - IptcImagePlugin.dump(c) - - # Reset stdout - sys.stdout = old_stdout + with pytest.warns(DeprecationWarning): + IptcImagePlugin.dump(c) # Assert assert mystdout.getvalue() == "61 62 63 \n" + + +def test_pad_deprecation() -> None: + with pytest.warns(DeprecationWarning): + assert IptcImagePlugin.PAD == b"\0\0\0\0" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index ffaea6296..654242148 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,8 +1,12 @@ from __future__ import annotations + import os import re import warnings from io import BytesIO +from pathlib import Path +from types import ModuleType +from typing import Any import pytest @@ -30,6 +34,7 @@ from .helper import ( skip_unless_feature, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: @@ -40,7 +45,7 @@ TEST_FILE = "Tests/images/hopper.jpg" @skip_unless_feature("jpg") class TestFileJpeg: - def roundtrip(self, im, **options): + def roundtrip(self, im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "JPEG", **options) test_bytes = out.tell() @@ -49,7 +54,7 @@ class TestFileJpeg: im.bytes = test_bytes # for testing only return im - def gen_random_image(self, size, mode="RGB"): + def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image: """Generates a very hard to compress file :param size: tuple :param mode: optional image mode @@ -57,7 +62,7 @@ class TestFileJpeg: """ return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) - def test_sanity(self): + def test_sanity(self) -> None: # internal version number assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) @@ -69,13 +74,13 @@ class TestFileJpeg: assert im.get_format_mimetype() == "image/jpeg" @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero(self, size, tmp_path): + def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") im = Image.new("RGB", size) with pytest.raises(ValueError): im.save(f) - def test_app(self): + def test_app(self) -> None: # Test APP/COM reader (@PIL135) with Image.open(TEST_FILE) as im: assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") @@ -88,7 +93,7 @@ class TestFileJpeg: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" assert im.app["COM"] == im.info["comment"] - def test_comment_write(self): + def test_comment_write(self) -> None: with Image.open(TEST_FILE) as im: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0\x00" @@ -106,15 +111,13 @@ class TestFileJpeg: assert "comment" not in reloaded.info # Test that a comment argument overrides the default comment - for comment in ("Test comment text", b"Text comment text"): + for comment in ("Test comment text", b"Test comment text"): out = BytesIO() im.save(out, format="JPEG", comment=comment) with Image.open(out) as reloaded: - if not isinstance(comment, bytes): - comment = comment.encode() - assert reloaded.info["comment"] == comment + assert reloaded.info["comment"] == b"Test comment text" - def test_cmyk(self): + def test_cmyk(self) -> None: # Test CMYK handling. Thanks to Tim and Charlie for test data, # Michael for getting me to look one more time. f = "Tests/images/pil_sample_cmyk.jpg" @@ -142,12 +145,25 @@ class TestFileJpeg: ) assert k > 0.9 + def test_rgb(self) -> None: + def getchannels(im: Image.Image) -> tuple[int, int, int]: + return tuple(v[0] for v in im.layer) + + im = hopper() + im_ycbcr = self.roundtrip(im) + assert getchannels(im_ycbcr) == (1, 2, 3) + assert_image_similar(im, im_ycbcr, 17) + + im_rgb = self.roundtrip(im, keep_rgb=True) + assert getchannels(im_rgb) == (ord("R"), ord("G"), ord("B")) + assert_image_similar(im, im_rgb, 12) + @pytest.mark.parametrize( "test_image_path", [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], ) - def test_dpi(self, test_image_path): - def test(xdpi, ydpi=None): + def test_dpi(self, test_image_path: str) -> None: + def test(xdpi: int, ydpi: int | None = None): with Image.open(test_image_path) as im: im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) return im.info.get("dpi") @@ -160,7 +176,7 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_icc(self, tmp_path): + def test_icc(self, tmp_path: Path) -> None: # Test ICC support with Image.open("Tests/images/rgb.jpg") as im1: icc_profile = im1.info["icc_profile"] @@ -192,7 +208,7 @@ class TestFileJpeg: ImageFile.MAXBLOCK * 4 + 3, # large block ), ) - def test_icc_big(self, n): + def test_icc_big(self, n: int) -> None: # Make sure that the "extra" support handles large blocks # The ICC APP marker can store 65519 bytes per marker, so # using a 4-byte test code should allow us to detect out of @@ -205,7 +221,7 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_large_icc_meta(self, tmp_path): + def test_large_icc_meta(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 # Sometimes the meta data on the icc_profile block is bigger than # Image.MAXBLOCK or the image size. @@ -229,7 +245,7 @@ class TestFileJpeg: f = str(tmp_path / "temp3.jpg") im.save(f, progressive=True, quality=94, exif=b" " * 43668) - def test_optimize(self): + def test_optimize(self) -> None: im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), optimize=0) im3 = self.roundtrip(hopper(), optimize=1) @@ -238,14 +254,14 @@ class TestFileJpeg: assert im1.bytes >= im2.bytes assert im1.bytes >= im3.bytes - def test_optimize_large_buffer(self, tmp_path): + def test_optimize_large_buffer(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", optimize=True) - def test_progressive(self): + def test_progressive(self) -> None: im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), progressive=False) im3 = self.roundtrip(hopper(), progressive=True) @@ -256,25 +272,25 @@ class TestFileJpeg: assert_image_equal(im1, im3) assert im1.bytes >= im3.bytes - def test_progressive_large_buffer(self, tmp_path): + def test_progressive_large_buffer(self, tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") # this requires ~ 1.5x Image.MAXBLOCK im = Image.new("RGB", (4096, 4096), 0xFF3333) im.save(f, format="JPEG", progressive=True) - def test_progressive_large_buffer_highest_quality(self, tmp_path): + def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") im = self.gen_random_image((255, 255)) # this requires more bytes than pixels in the image im.save(f, format="JPEG", progressive=True, quality=100) - def test_progressive_cmyk_buffer(self): + def test_progressive_cmyk_buffer(self) -> None: # Issue 2272, quality 90 cmyk image is tripping the large buffer bug. f = BytesIO() im = self.gen_random_image((256, 256), "CMYK") im.save(f, format="JPEG", progressive=True, quality=94) - def test_large_exif(self, tmp_path): + def test_large_exif(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") im = hopper() @@ -283,12 +299,12 @@ class TestFileJpeg: with pytest.raises(ValueError): im.save(f, "JPEG", quality=90, exif=b"1" * 65534) - def test_exif_typeerror(self): + def test_exif_typeerror(self) -> None: with Image.open("Tests/images/exif_typeerror.jpg") as im: # Should not raise a TypeError im._getexif() - def test_exif_gps(self, tmp_path): + def test_exif_gps(self, tmp_path: Path) -> None: expected_exif_gps = { 0: b"\x00\x00\x00\x01", 2: 4294967295, @@ -313,7 +329,7 @@ class TestFileJpeg: exif = reloaded._getexif() assert exif[gps_index] == expected_exif_gps - def test_empty_exif_gps(self): + def test_empty_exif_gps(self) -> None: with Image.open("Tests/images/empty_gps_ifd.jpg") as im: exif = im.getexif() del exif[0x8769] @@ -331,7 +347,7 @@ class TestFileJpeg: # Assert that it was transposed assert 0x0112 not in exif - def test_exif_equality(self): + def test_exif_equality(self) -> None: # In 7.2.0, Exif rationals were changed to be read as # TiffImagePlugin.IFDRational. This class had a bug in __eq__, # breaking the self-equality of Exif data @@ -341,7 +357,7 @@ class TestFileJpeg: exifs.append(im._getexif()) assert exifs[0] == exifs[1] - def test_exif_rollback(self): + def test_exif_rollback(self) -> None: # rolling back exif support in 3.1 to pre-3.0 formatting. # expected from 2.9, with b/u qualifiers switched for 3.2 compatibility # this test passes on 2.9 and 3.1, but not 3.0 @@ -376,12 +392,12 @@ class TestFileJpeg: for tag, value in expected_exif.items(): assert value == exif[tag] - def test_exif_gps_typeerror(self): + def test_exif_gps_typeerror(self) -> None: with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: # Should not raise a TypeError im._getexif() - def test_progressive_compat(self): + def test_progressive_compat(self) -> None: im1 = self.roundtrip(hopper()) assert not im1.info.get("progressive") assert not im1.info.get("progression") @@ -402,7 +418,7 @@ class TestFileJpeg: assert im3.info.get("progressive") assert im3.info.get("progression") - def test_quality(self): + def test_quality(self) -> None: im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), quality=50) assert_image(im1, im2.mode, im2.size) @@ -412,57 +428,60 @@ class TestFileJpeg: assert_image(im1, im3.mode, im3.size) assert im2.bytes > im3.bytes - def test_smooth(self): + def test_smooth(self) -> None: im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), smooth=100) assert_image(im1, im2.mode, im2.size) - def test_subsampling(self): - def getsampling(im): + def test_subsampling(self) -> None: + def getsampling(im: Image.Image): layer = im.layer return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] # experimental API - im = self.roundtrip(hopper(), subsampling=-1) # default - assert getsampling(im) == (2, 2, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling=0) # 4:4:4 - assert getsampling(im) == (1, 1, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling=1) # 4:2:2 - assert getsampling(im) == (2, 1, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling=2) # 4:2:0 - assert getsampling(im) == (2, 2, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling=3) # default (undefined) - assert getsampling(im) == (2, 2, 1, 1, 1, 1) + for subsampling in (-1, 3): # (default, invalid) + im = self.roundtrip(hopper(), subsampling=subsampling) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) + for subsampling1 in (0, "4:4:4"): + im = self.roundtrip(hopper(), subsampling=subsampling1) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) + for subsampling1 in (1, "4:2:2"): + im = self.roundtrip(hopper(), subsampling=subsampling1) + assert getsampling(im) == (2, 1, 1, 1, 1, 1) + for subsampling1 in (2, "4:2:0", "4:1:1"): + im = self.roundtrip(hopper(), subsampling=subsampling1) + assert getsampling(im) == (2, 2, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling="4:4:4") - assert getsampling(im) == (1, 1, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling="4:2:2") - assert getsampling(im) == (2, 1, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling="4:2:0") - assert getsampling(im) == (2, 2, 1, 1, 1, 1) - im = self.roundtrip(hopper(), subsampling="4:1:1") - assert getsampling(im) == (2, 2, 1, 1, 1, 1) + # RGB colorspace + for subsampling1 in (-1, 0, "4:4:4"): + # "4:4:4" doesn't really make sense for RGB, but the conversion + # to an integer happens at a higher level + im = self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1) + assert getsampling(im) == (1, 1, 1, 1, 1, 1) + for subsampling1 in (1, "4:2:2", 2, "4:2:0", 3): + with pytest.raises(OSError): + self.roundtrip(hopper(), keep_rgb=True, subsampling=subsampling1) with pytest.raises(TypeError): self.roundtrip(hopper(), subsampling="1:1:1") - def test_exif(self): + def test_exif(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: info = im._getexif() assert info[305] == "Adobe Photoshop CS Macintosh" - def test_get_child_images(self): + def test_get_child_images(self) -> None: with Image.open("Tests/images/flower.jpg") as im: ims = im.get_child_images() assert len(ims) == 1 assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1) - def test_mp(self): + def test_mp(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: assert im._getmp() is None - def test_quality_keep(self, tmp_path): + def test_quality_keep(self, tmp_path: Path) -> None: # RGB with Image.open("Tests/images/hopper.jpg") as im: f = str(tmp_path / "temp.jpg") @@ -476,13 +495,13 @@ class TestFileJpeg: f = str(tmp_path / "temp.jpg") im.save(f, quality="keep") - def test_junk_jpeg_header(self): + def test_junk_jpeg_header(self) -> None: # https://github.com/python-pillow/Pillow/issues/630 filename = "Tests/images/junk_jpeg_header.jpg" with Image.open(filename): pass - def test_ff00_jpeg_header(self): + def test_ff00_jpeg_header(self) -> None: filename = "Tests/images/jpeg_ff00_header.jpg" with Image.open(filename): pass @@ -490,7 +509,7 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_truncated_jpeg_should_read_all_the_data(self): + def test_truncated_jpeg_should_read_all_the_data(self) -> None: filename = "Tests/images/truncated_jpeg.jpg" ImageFile.LOAD_TRUNCATED_IMAGES = True with Image.open(filename) as im: @@ -498,7 +517,7 @@ class TestFileJpeg: ImageFile.LOAD_TRUNCATED_IMAGES = False assert im.getbbox() is not None - def test_truncated_jpeg_throws_oserror(self): + def test_truncated_jpeg_throws_oserror(self) -> None: filename = "Tests/images/truncated_jpeg.jpg" with Image.open(filename) as im: with pytest.raises(OSError): @@ -511,8 +530,8 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_qtables(self, tmp_path): - def _n_qtables_helper(n, test_file): + def test_qtables(self, tmp_path: Path) -> None: + def _n_qtables_helper(n: int, test_file: str) -> None: with Image.open(test_file) as im: f = str(tmp_path / "temp.jpg") im.save(f, qtables=[[n] * 64] * n) @@ -620,24 +639,24 @@ class TestFileJpeg: with pytest.raises(ValueError): self.roundtrip(im, qtables=[[1, 2, 3, 4]]) - def test_load_16bit_qtables(self): + def test_load_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: assert len(im.quantization) == 2 assert len(im.quantization[0]) == 64 assert max(im.quantization[0]) > 255 - def test_save_multiple_16bit_qtables(self): + def test_save_multiple_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: im2 = self.roundtrip(im, qtables="keep") assert im.quantization == im2.quantization - def test_save_single_16bit_qtable(self): + def test_save_single_16bit_qtable(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: im2 = self.roundtrip(im, qtables={0: im.quantization[0]}) assert len(im2.quantization) == 1 assert im2.quantization[0] == im.quantization[0] - def test_save_low_quality_baseline_qtables(self): + def test_save_low_quality_baseline_qtables(self) -> None: with Image.open(TEST_FILE) as im: im2 = self.roundtrip(im, quality=10) assert len(im2.quantization) == 2 @@ -648,7 +667,7 @@ class TestFileJpeg: "blocks, rows, markers", ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)), ) - def test_restart_markers(self, blocks, rows, markers): + def test_restart_markers(self, blocks: int, rows: int, markers: int) -> None: im = Image.new("RGB", (32, 32)) # 16 MCUs out = BytesIO() im.save( @@ -662,20 +681,20 @@ class TestFileJpeg: assert len(re.findall(b"\xff[\xd0-\xd7]", out.getvalue())) == markers @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") - def test_load_djpeg(self): + def test_load_djpeg(self) -> None: with Image.open(TEST_FILE) as img: img.load_djpeg() assert_image_similar_tofile(img, TEST_FILE, 5) @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") - def test_save_cjpeg(self, tmp_path): + def test_save_cjpeg(self, tmp_path: Path) -> None: with Image.open(TEST_FILE) as img: tempfile = str(tmp_path / "temp.jpg") JpegImagePlugin._save_cjpeg(img, 0, tempfile) # Default save quality is 75%, so a tiny bit of difference is alright assert_image_similar_tofile(img, tempfile, 17) - def test_no_duplicate_0x1001_tag(self): + def test_no_duplicate_0x1001_tag(self) -> None: # Arrange tag_ids = {v: k for k, v in ExifTags.TAGS.items()} @@ -683,7 +702,7 @@ class TestFileJpeg: assert tag_ids["RelatedImageWidth"] == 0x1001 assert tag_ids["RelatedImageLength"] == 0x1002 - def test_MAXBLOCK_scaling(self, tmp_path): + def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None: im = self.gen_random_image((512, 512)) f = str(tmp_path / "temp.jpeg") im.save(f, quality=100, optimize=True) @@ -694,7 +713,7 @@ class TestFileJpeg: reloaded.save(f, quality="keep", progressive=True) reloaded.save(f, quality="keep", optimize=True) - def test_bad_mpo_header(self): + def test_bad_mpo_header(self) -> None: """Treat unknown MPO as JPEG""" # Arrange @@ -706,20 +725,20 @@ class TestFileJpeg: assert im.format == "JPEG" @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) - def test_save_correct_modes(self, mode): + def test_save_correct_modes(self, mode: str) -> None: out = BytesIO() img = Image.new(mode, (20, 20)) img.save(out, "JPEG") @pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P")) - def test_save_wrong_modes(self, mode): + def test_save_wrong_modes(self, mode: str) -> None: # ref https://github.com/python-pillow/Pillow/issues/2005 out = BytesIO() img = Image.new(mode, (20, 20)) with pytest.raises(OSError): img.save(out, "JPEG") - def test_save_tiff_with_dpi(self, tmp_path): + def test_save_tiff_with_dpi(self, tmp_path: Path) -> None: # Arrange outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: @@ -731,7 +750,7 @@ class TestFileJpeg: reloaded.load() assert im.info["dpi"] == reloaded.info["dpi"] - def test_save_dpi_rounding(self, tmp_path): + def test_save_dpi_rounding(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: im.save(outfile, dpi=(72.2, 72.2)) @@ -744,7 +763,7 @@ class TestFileJpeg: with Image.open(outfile) as reloaded: assert reloaded.info["dpi"] == (73, 73) - def test_dpi_tuple_from_exif(self): + def test_dpi_tuple_from_exif(self) -> None: # Arrange # This Photoshop CC 2017 image has DPI in EXIF not metadata # EXIF XResolution is (2000000, 10000) @@ -752,7 +771,7 @@ class TestFileJpeg: # Act / Assert assert im.info.get("dpi") == (200, 200) - def test_dpi_int_from_exif(self): + def test_dpi_int_from_exif(self) -> None: # Arrange # This image has DPI in EXIF not metadata # EXIF XResolution is 72 @@ -760,7 +779,7 @@ class TestFileJpeg: # Act / Assert assert im.info.get("dpi") == (72, 72) - def test_dpi_from_dpcm_exif(self): + def test_dpi_from_dpcm_exif(self) -> None: # Arrange # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg @@ -768,7 +787,7 @@ class TestFileJpeg: # Act / Assert assert im.info.get("dpi") == (508, 508) - def test_dpi_exif_zero_division(self): + def test_dpi_exif_zero_division(self) -> None: # Arrange # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg @@ -777,7 +796,7 @@ class TestFileJpeg: # This should return the default, and not raise a ZeroDivisionError assert im.info.get("dpi") == (72, 72) - def test_dpi_exif_string(self): + def test_dpi_exif_string(self) -> None: # Arrange # 0x011A tag in this exif contains string '300300\x02' with Image.open("Tests/images/broken_exif_dpi.jpg") as im: @@ -785,14 +804,14 @@ class TestFileJpeg: # This should return the default assert im.info.get("dpi") == (72, 72) - def test_dpi_exif_truncated(self): + def test_dpi_exif_truncated(self) -> None: # Arrange with Image.open("Tests/images/truncated_exif_dpi.jpg") as im: # Act / Assert # This should return the default assert im.info.get("dpi") == (72, 72) - def test_no_dpi_in_exif(self): + def test_no_dpi_in_exif(self) -> None: # Arrange # This is photoshop-200dpi.jpg with resolution removed from EXIF: # exiftool "-*resolution*"= photoshop-200dpi.jpg @@ -802,7 +821,7 @@ class TestFileJpeg: # https://exiv2.org/tags.html assert im.info.get("dpi") == (72, 72) - def test_invalid_exif(self): + def test_invalid_exif(self) -> None: # This is no-dpi-in-exif with the tiff header of the exif block # hexedited from MM * to FF FF FF FF with Image.open("Tests/images/invalid-exif.jpg") as im: @@ -813,7 +832,7 @@ class TestFileJpeg: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_exif_x_resolution(self, tmp_path): + def test_exif_x_resolution(self, tmp_path: Path) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() assert exif[282] == 180 @@ -825,14 +844,14 @@ class TestFileJpeg: with Image.open(out) as reloaded: assert reloaded.getexif()[282] == 180 - def test_invalid_exif_x_resolution(self): + def test_invalid_exif_x_resolution(self) -> None: # When no x or y resolution is defined in EXIF with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: # This should return the default, and not a ValueError or # OSError for an unidentified image. assert im.info.get("dpi") == (72, 72) - def test_ifd_offset_exif(self): + def test_ifd_offset_exif(self) -> None: # Arrange # This image has been manually hexedited to have an IFD offset of 10, # in contrast to normal 8 @@ -840,10 +859,14 @@ class TestFileJpeg: # Act / Assert assert im._getexif()[306] == "2017:03:13 23:03:09" + def test_multiple_exif(self) -> None: + with Image.open("Tests/images/multiple_exif.jpg") as im: + assert im.info["exif"] == b"Exif\x00\x00firstsecond" + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_photoshop(self): + def test_photoshop(self) -> None: with Image.open("Tests/images/photoshop-200dpi.jpg") as im: assert im.info["photoshop"][0x03ED] == { "XResolution": 200.0, @@ -860,14 +883,14 @@ class TestFileJpeg: with Image.open("Tests/images/app13.jpg") as im: assert "photoshop" not in im.info - def test_photoshop_malformed_and_multiple(self): + def test_photoshop_malformed_and_multiple(self) -> None: with Image.open("Tests/images/app13-multiple.jpg") as im: assert "photoshop" in im.info assert 24 == len(im.info["photoshop"]) apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] assert [65504, 24] == apps_13_lengths - def test_adobe_transform(self): + def test_adobe_transform(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: assert im.info["adobe_transform"] == 1 @@ -881,11 +904,11 @@ class TestFileJpeg: assert "adobe" in im.info assert "adobe_transform" not in im.info - def test_icc_after_SOF(self): + def test_icc_after_SOF(self) -> None: with Image.open("Tests/images/icc-after-SOF.jpg") as im: assert im.info["icc_profile"] == b"profile" - def test_jpeg_magic_number(self): + def test_jpeg_magic_number(self) -> None: size = 4097 buffer = BytesIO(b"\xFF" * size) # Many xFF bytes buffer.max_pos = 0 @@ -904,7 +927,7 @@ class TestFileJpeg: # Assert the entire file has not been read assert 0 < buffer.max_pos < size - def test_getxmp(self): + def test_getxmp(self) -> None: with Image.open("Tests/images/xmp_test.jpg") as im: if ElementTree is None: with pytest.warns( @@ -933,7 +956,7 @@ class TestFileJpeg: with Image.open("Tests/images/hopper.jpg") as im: assert im.getxmp() == {} - def test_getxmp_no_prefix(self): + def test_getxmp_no_prefix(self) -> None: with Image.open("Tests/images/xmp_no_prefix.jpg") as im: if ElementTree is None: with pytest.warns( @@ -944,7 +967,7 @@ class TestFileJpeg: else: assert im.getxmp() == {"xmpmeta": {"key": "value"}} - def test_getxmp_padded(self): + def test_getxmp_padded(self) -> None: with Image.open("Tests/images/xmp_padded.jpg") as im: if ElementTree is None: with pytest.warns( @@ -956,16 +979,16 @@ class TestFileJpeg: assert im.getxmp() == {"xmpmeta": None} @pytest.mark.timeout(timeout=1) - def test_eof(self): + def test_eof(self) -> None: # 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): + def decode(self, buffer: bytes) -> tuple[int, int]: return 0, 0 decoder = InfiniteMockPyDecoder(None) - def closure(mode, *args): + def closure(mode: str, *args) -> InfiniteMockPyDecoder: decoder.__init__(mode, *args) return decoder @@ -979,7 +1002,7 @@ class TestFileJpeg: im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_separate_tables(self): + def test_separate_tables(self) -> None: im = hopper() data = [] # [interchange, tables-only, image-only] for streamtype in range(3): @@ -1001,14 +1024,14 @@ class TestFileJpeg: with Image.open(BytesIO(data[1] + data[2])) as combined_im: assert_image_equal(interchange_im, combined_im) - def test_repr_jpeg(self): + def test_repr_jpeg(self) -> None: im = hopper() with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: assert repr_jpeg.format == "JPEG" assert_image_similar(im, repr_jpeg, 17) - def test_repr_jpeg_error_returns_none(self): + def test_repr_jpeg_error_returns_none(self) -> None: im = hopper("F") assert im._repr_jpeg_() is None @@ -1017,7 +1040,7 @@ class TestFileJpeg: @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") class TestFileCloseW32: - def test_fd_leak(self, tmp_path): + def test_fd_leak(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index aaa4104e5..fab19e2ea 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,7 +1,10 @@ from __future__ import annotations + import os import re from io import BytesIO +from pathlib import Path +from typing import Any import pytest @@ -34,7 +37,7 @@ test_card.load() # 'Not enough memory to handle tile data' -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "JPEG2000", **options) test_bytes = out.tell() @@ -45,7 +48,7 @@ def roundtrip(im, **options): return im -def test_sanity(): +def test_sanity() -> None: # Internal version number assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000")) @@ -58,20 +61,20 @@ def test_sanity(): assert im.get_format_mimetype() == "image/jp2" -def test_jpf(): +def test_jpf() -> None: with Image.open("Tests/images/balloon.jpf") as im: assert im.format == "JPEG2000" assert im.get_format_mimetype() == "image/jpx" -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file) -def test_bytesio(): +def test_bytesio() -> None: with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = BytesIO(f.read()) assert_image_similar_tofile(test_card, data, 1.0e-3) @@ -81,7 +84,7 @@ def test_bytesio(): # PIL (they were made using Adobe Photoshop) -def test_lossless(tmp_path): +def test_lossless(tmp_path: Path) -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: im.load() outfile = str(tmp_path / "temp_test-card.png") @@ -89,54 +92,54 @@ def test_lossless(tmp_path): assert_image_similar(im, test_card, 1.0e-3) -def test_lossy_tiled(): +def test_lossy_tiled() -> None: assert_image_similar_tofile( test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0 ) -def test_lossless_rt(): +def test_lossless_rt() -> None: im = roundtrip(test_card) assert_image_equal(im, test_card) -def test_lossy_rt(): +def test_lossy_rt() -> None: im = roundtrip(test_card, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_tiled_rt(): +def test_tiled_rt() -> None: im = roundtrip(test_card, tile_size=(128, 128)) assert_image_equal(im, test_card) -def test_tiled_offset_rt(): +def test_tiled_offset_rt() -> None: im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32)) assert_image_equal(im, test_card) -def test_tiled_offset_too_small(): +def test_tiled_offset_too_small() -> None: with pytest.raises(ValueError): roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)) -def test_irreversible_rt(): +def test_irreversible_rt() -> None: im = roundtrip(test_card, irreversible=True, quality_layers=[20]) assert_image_similar(im, test_card, 2.0) -def test_prog_qual_rt(): +def test_prog_qual_rt() -> None: im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP") assert_image_similar(im, test_card, 2.0) -def test_prog_res_rt(): +def test_prog_res_rt() -> None: im = roundtrip(test_card, num_resolutions=8, progression="RLCP") assert_image_equal(im, test_card) @pytest.mark.parametrize("num_resolutions", range(2, 6)) -def test_default_num_resolutions(num_resolutions): +def test_default_num_resolutions(num_resolutions: int) -> None: d = 1 << (num_resolutions - 1) im = test_card.resize((d - 1, d - 1)) with pytest.raises(OSError): @@ -145,7 +148,7 @@ def test_default_num_resolutions(num_resolutions): assert_image_equal(im, reloaded) -def test_reduce(): +def test_reduce() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert callable(im.reduce) @@ -159,7 +162,7 @@ def test_reduce(): assert im.size == (40, 30) -def test_load_dpi(): +def test_load_dpi() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert im.info["dpi"] == (71.9836, 71.9836) @@ -167,7 +170,7 @@ def test_load_dpi(): assert "dpi" not in im.info -def test_restricted_icc_profile(): +def test_restricted_icc_profile() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = True try: # JPEG2000 image with a restricted ICC profile and a known colorspace @@ -177,7 +180,7 @@ def test_restricted_icc_profile(): ImageFile.LOAD_TRUNCATED_IMAGES = False -def test_header_errors(): +def test_header_errors() -> None: for path in ( "Tests/images/invalid_header_length.jp2", "Tests/images/not_enough_data.jp2", @@ -191,17 +194,17 @@ def test_header_errors(): pass -def test_layers_type(tmp_path): +def test_layers_type(tmp_path: Path) -> None: outfile = str(tmp_path / "temp_layers.jp2") for quality_layers in [[100, 50, 10], (100, 50, 10), None]: test_card.save(outfile, quality_layers=quality_layers) - for quality_layers in ["quality_layers", ("100", "50", "10")]: + for quality_layers_str in ["quality_layers", ("100", "50", "10")]: with pytest.raises(ValueError): - test_card.save(outfile, quality_layers=quality_layers) + test_card.save(outfile, quality_layers=quality_layers_str) -def test_layers(): +def test_layers() -> None: out = BytesIO() test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP") out.seek(0) @@ -231,7 +234,7 @@ def test_layers(): ("foo.jp2", {"no_jp2": False}, 4, b"jP"), ), ) -def test_no_jp2(name, args, offset, data): +def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: out = BytesIO() if name: out.name = name @@ -240,7 +243,7 @@ def test_no_jp2(name, args, offset, data): assert out.read(2) == data -def test_mct(): +def test_mct() -> None: # Three component for val in (0, 1): out = BytesIO() @@ -261,7 +264,7 @@ def test_mct(): assert_image_similar(im, jp2, 1.0e-3) -def test_sgnd(tmp_path): +def test_sgnd(tmp_path: Path) -> None: outfile = str(tmp_path / "temp.jp2") im = Image.new("L", (1, 1)) @@ -276,7 +279,7 @@ def test_sgnd(tmp_path): @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_rgba(ext): +def test_rgba(ext: str) -> None: # Arrange with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: # Act @@ -287,47 +290,47 @@ def test_rgba(ext): @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_16bit_monochrome_has_correct_mode(ext): +def test_16bit_monochrome_has_correct_mode(ext: str) -> None: with Image.open("Tests/images/16bit.cropped" + ext) as im: im.load() assert im.mode == "I;16" -def test_16bit_monochrome_jp2_like_tiff(): +def test_16bit_monochrome_jp2_like_tiff() -> None: with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.jp2", 1e-3) -def test_16bit_monochrome_j2k_like_tiff(): +def test_16bit_monochrome_j2k_like_tiff() -> None: with Image.open("Tests/images/16bit.cropped.tif") as tiff_16bit: assert_image_similar_tofile(tiff_16bit, "Tests/images/16bit.cropped.j2k", 1e-3) -def test_16bit_j2k_roundtrips(): +def test_16bit_j2k_roundtrips() -> None: with Image.open("Tests/images/16bit.cropped.j2k") as j2k: im = roundtrip(j2k) assert_image_equal(im, j2k) -def test_16bit_jp2_roundtrips(): +def test_16bit_jp2_roundtrips() -> None: with Image.open("Tests/images/16bit.cropped.jp2") as jp2: im = roundtrip(jp2) assert_image_equal(im, jp2) -def test_issue_6194(): +def test_issue_6194() -> None: with Image.open("Tests/images/issue_6194.j2k") as im: assert im.getpixel((5, 5)) == 31 -def test_unbound_local(): +def test_unbound_local() -> None: # prepatch, a malformed jp2 file could cause an UnboundLocalError exception. with pytest.raises(OSError): with Image.open("Tests/images/unbound_variable.jp2"): pass -def test_parser_feed(): +def test_parser_feed() -> None: # Arrange with open("Tests/images/test-card-lossless.jp2", "rb") as f: data = f.read() @@ -344,12 +347,12 @@ def test_parser_feed(): not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) @pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) -def test_subsampling_decode(name): +def test_subsampling_decode(name: str) -> None: test = f"{EXTRA_DIR}/{name}.jp2" reference = f"{EXTRA_DIR}/{name}.ppm" with Image.open(test) as im: - epsilon = 3 # for YCbCr images + epsilon = 3.0 # for YCbCr images with Image.open(reference) as im2: width, height = im2.size if name[-1] == "2": @@ -360,7 +363,7 @@ def test_subsampling_decode(name): assert_image_similar(im, expected, epsilon) -def test_comment(): +def test_comment() -> None: with Image.open("Tests/images/comment.jp2") as im: assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" @@ -371,7 +374,7 @@ def test_comment(): pass -def test_save_comment(): +def test_save_comment() -> None: for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() test_card.save(out, "JPEG2000", comment=comment) @@ -398,7 +401,7 @@ def test_save_comment(): "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", ], ) -def test_crashes(test_file): +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: # Valgrind should not complain here @@ -409,7 +412,7 @@ def test_crashes(test_file): @skip_unless_feature_version("jpg_2000", "2.4.0") -def test_plt_marker(): +def test_plt_marker() -> None: # Search the start of the codesteam for PLT out = BytesIO() test_card.save(out, "JPEG2000", no_jp2=True, plt=True) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 65adf449d..0994d9904 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,4 +1,5 @@ from __future__ import annotations + import base64 import io import itertools @@ -6,6 +7,7 @@ import os import re import sys from collections import namedtuple +from pathlib import Path import pytest @@ -25,7 +27,7 @@ from .helper import ( @skip_unless_feature("libtiff") class LibTiffTestCase: - def _assert_noerr(self, tmp_path, im): + def _assert_noerr(self, tmp_path: Path, im: Image.Image) -> None: """Helper tests that assert basic sanity about the g4 tiff reading""" # 1 bit assert im.mode == "1" @@ -49,10 +51,10 @@ class LibTiffTestCase: class TestFileLibTiff(LibTiffTestCase): - def test_version(self): + def test_version(self) -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff")) - def test_g4_tiff(self, tmp_path): + def test_g4_tiff(self, tmp_path: Path) -> None: """Test the ordinary file path load path""" test_file = "Tests/images/hopper_g4_500.tif" @@ -60,12 +62,12 @@ class TestFileLibTiff(LibTiffTestCase): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_large(self, tmp_path): + def test_g4_large(self, tmp_path: Path) -> None: test_file = "Tests/images/pport_g4.tif" with Image.open(test_file) as im: self._assert_noerr(tmp_path, im) - def test_g4_tiff_file(self, tmp_path): + def test_g4_tiff_file(self, tmp_path: Path) -> None: """Testing the string load path""" test_file = "Tests/images/hopper_g4_500.tif" @@ -74,7 +76,7 @@ class TestFileLibTiff(LibTiffTestCase): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_tiff_bytesio(self, tmp_path): + def test_g4_tiff_bytesio(self, tmp_path: Path) -> None: """Testing the stringio loading code path""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() @@ -85,7 +87,7 @@ class TestFileLibTiff(LibTiffTestCase): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_non_disk_file_object(self, tmp_path): + def test_g4_non_disk_file_object(self, tmp_path: Path) -> None: """Testing loading from non-disk non-BytesIO file object""" test_file = "Tests/images/hopper_g4_500.tif" s = io.BytesIO() @@ -97,18 +99,18 @@ class TestFileLibTiff(LibTiffTestCase): assert im.size == (500, 500) self._assert_noerr(tmp_path, im) - def test_g4_eq_png(self): + def test_g4_eq_png(self) -> None: """Checking that we're actually getting the data that we expect""" with Image.open("Tests/images/hopper_bw_500.png") as png: assert_image_equal_tofile(png, "Tests/images/hopper_g4_500.tif") # see https://github.com/python-pillow/Pillow/issues/279 - def test_g4_fillorder_eq_png(self): + def test_g4_fillorder_eq_png(self) -> None: """Checking that we're actually getting the data that we expect""" with Image.open("Tests/images/g4-fillorder-test.tif") as g4: assert_image_equal_tofile(g4, "Tests/images/g4-fillorder-test.png") - def test_g4_write(self, tmp_path): + def test_g4_write(self, tmp_path: Path) -> None: """Checking to see that the saved image is the same as what we wrote""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: @@ -127,7 +129,7 @@ class TestFileLibTiff(LibTiffTestCase): assert orig.tobytes() != reread.tobytes() - def test_adobe_deflate_tiff(self): + def test_adobe_deflate_tiff(self) -> None: test_file = "Tests/images/tiff_adobe_deflate.tif" with Image.open(test_file) as im: assert im.mode == "RGB" @@ -138,7 +140,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") @pytest.mark.parametrize("legacy_api", (False, True)) - def test_write_metadata(self, legacy_api, tmp_path): + def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: """Test metadata writing through libtiff""" f = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper_g4.tif") as img: @@ -183,7 +185,7 @@ class TestFileLibTiff(LibTiffTestCase): assert field in reloaded, f"{field} not in metadata" @pytest.mark.valgrind_known_error(reason="Known invalid metadata") - def test_additional_metadata(self, tmp_path): + def test_additional_metadata(self, tmp_path: Path) -> None: # these should not crash. Seriously dummy data, most of it doesn't make # any sense, so we're running up against limits where we're asking # libtiff to do stupid things. @@ -240,8 +242,8 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.WRITE_LIBTIFF = False - def test_custom_metadata(self, tmp_path): - tc = namedtuple("test_case", "value,type,supported_by_default") + def test_custom_metadata(self, tmp_path: Path) -> None: + tc = namedtuple("tc", "value,type,supported_by_default") custom = { 37000 + k: v for k, v in enumerate( @@ -282,7 +284,9 @@ class TestFileLibTiff(LibTiffTestCase): for libtiff in libtiffs: TiffImagePlugin.WRITE_LIBTIFF = libtiff - def check_tags(tiffinfo): + def check_tags( + tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] + ) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -321,7 +325,7 @@ class TestFileLibTiff(LibTiffTestCase): ) TiffImagePlugin.WRITE_LIBTIFF = False - def test_subifd(self, tmp_path): + def test_subifd(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/g4_orientation_6.tif") as im: im.tag_v2[SUBIFD] = 10000 @@ -329,7 +333,7 @@ class TestFileLibTiff(LibTiffTestCase): # Should not segfault im.save(outfile) - def test_xmlpacket_tag(self, tmp_path): + def test_xmlpacket_tag(self, tmp_path: Path) -> None: TiffImagePlugin.WRITE_LIBTIFF = True out = str(tmp_path / "temp.tif") @@ -340,7 +344,7 @@ class TestFileLibTiff(LibTiffTestCase): if 700 in reloaded.tag_v2: assert reloaded.tag_v2[700] == b"xmlpacket tag" - def test_int_dpi(self, tmp_path): + def test_int_dpi(self, tmp_path: Path) -> None: # issue #1765 im = hopper("RGB") out = str(tmp_path / "temp.tif") @@ -350,7 +354,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as reloaded: assert reloaded.info["dpi"] == (72.0, 72.0) - def test_g3_compression(self, tmp_path): + def test_g3_compression(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper_g4_500.tif") as i: out = str(tmp_path / "temp.tif") i.save(out, compression="group3") @@ -359,7 +363,7 @@ class TestFileLibTiff(LibTiffTestCase): assert reread.info["compression"] == "group3" assert_image_equal(reread, i) - def test_little_endian(self, tmp_path): + def test_little_endian(self, tmp_path: Path) -> None: with Image.open("Tests/images/16bit.deflate.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16" @@ -378,7 +382,7 @@ class TestFileLibTiff(LibTiffTestCase): # UNDONE - libtiff defaults to writing in native endian, so # on big endian, we'll get back mode = 'I;16B' here. - def test_big_endian(self, tmp_path): + def test_big_endian(self, tmp_path: Path) -> None: with Image.open("Tests/images/16bit.MM.deflate.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16B" @@ -395,7 +399,7 @@ class TestFileLibTiff(LibTiffTestCase): assert reread.info["compression"] == im.info["compression"] assert reread.getpixel((0, 0)) == 480 - def test_g4_string_info(self, tmp_path): + def test_g4_string_info(self, tmp_path: Path) -> None: """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: @@ -408,7 +412,7 @@ class TestFileLibTiff(LibTiffTestCase): assert "temp.tif" == reread.tag_v2[269] assert "temp.tif" == reread.tag[269][0] - def test_12bit_rawmode(self): + def test_12bit_rawmode(self) -> None: """Are we generating the same interpretation of the image as Imagemagick is?""" TiffImagePlugin.READ_LIBTIFF = True @@ -423,7 +427,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") - def test_blur(self, tmp_path): + def test_blur(self, tmp_path: Path) -> None: # test case from irc, how to do blur on b/w image # and save to compressed tif. out = str(tmp_path / "temp.tif") @@ -435,7 +439,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, out) - def test_compressions(self, tmp_path): + def test_compressions(self, tmp_path: Path) -> None: # Test various tiff compressions and assert similar image content but reduced # file sizes. im = hopper("RGB") @@ -461,7 +465,7 @@ class TestFileLibTiff(LibTiffTestCase): assert size_compressed > size_jpeg assert size_jpeg > size_jpeg_30 - def test_tiff_jpeg_compression(self, tmp_path): + def test_tiff_jpeg_compression(self, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") im.save(out, compression="tiff_jpeg") @@ -469,7 +473,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as reloaded: assert reloaded.info["compression"] == "jpeg" - def test_tiff_deflate_compression(self, tmp_path): + def test_tiff_deflate_compression(self, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") im.save(out, compression="tiff_deflate") @@ -477,7 +481,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as reloaded: assert reloaded.info["compression"] == "tiff_adobe_deflate" - def test_quality(self, tmp_path): + def test_quality(self, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") @@ -492,7 +496,7 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, compression="jpeg", quality=0) im.save(out, compression="jpeg", quality=100) - def test_cmyk_save(self, tmp_path): + def test_cmyk_save(self, tmp_path: Path) -> None: im = hopper("CMYK") out = str(tmp_path / "temp.tif") @@ -500,7 +504,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, out) @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) - def test_palette_save(self, im, tmp_path): + def test_palette_save(self, im: Image.Image, tmp_path: Path) -> None: out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True @@ -512,14 +516,14 @@ class TestFileLibTiff(LibTiffTestCase): assert len(reloaded.tag_v2[320]) == 768 @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) - def test_bw_compression_w_rgb(self, compression, tmp_path): + def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") with pytest.raises(OSError): im.save(out, compression=compression) - def test_fp_leak(self): + def test_fp_leak(self) -> None: im = Image.open("Tests/images/hopper_g4_500.tif") fn = im.fp.fileno() @@ -533,7 +537,7 @@ class TestFileLibTiff(LibTiffTestCase): with pytest.raises(OSError): os.close(fn) - def test_multipage(self): + def test_multipage(self) -> None: # issue #862 TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/multipage.tiff") as im: @@ -556,7 +560,7 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False - def test_multipage_nframes(self): + def test_multipage_nframes(self) -> None: # issue #862 TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/multipage.tiff") as im: @@ -569,7 +573,7 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False - def test_multipage_seek_backwards(self): + def test_multipage_seek_backwards(self) -> None: TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/multipage.tiff") as im: im.seek(1) @@ -580,14 +584,14 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False - def test__next(self): + def test__next(self) -> None: TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/hopper.tif") as im: assert not im.tag.next im.load() assert not im.tag.next - def test_4bit(self): + def test_4bit(self) -> None: # Arrange test_file = "Tests/images/hopper_gray_4bpp.tif" original = hopper("L") @@ -602,7 +606,7 @@ class TestFileLibTiff(LibTiffTestCase): assert im.mode == "L" assert_image_similar(im, original, 7.3) - def test_gray_semibyte_per_pixel(self): + def test_gray_semibyte_per_pixel(self) -> None: test_files = ( ( 24.8, # epsilon @@ -635,7 +639,7 @@ class TestFileLibTiff(LibTiffTestCase): assert im2.mode == "L" assert_image_equal(im, im2) - def test_save_bytesio(self): + def test_save_bytesio(self) -> None: # PR 1011 # Test TIFF saving to io.BytesIO() object. @@ -645,7 +649,7 @@ class TestFileLibTiff(LibTiffTestCase): # Generate test image pilim = hopper() - def save_bytesio(compression=None): + def save_bytesio(compression: str | None = None) -> None: buffer_io = io.BytesIO() pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) @@ -660,7 +664,7 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False - def test_save_ycbcr(self, tmp_path): + def test_save_ycbcr(self, tmp_path: Path) -> None: im = hopper("YCbCr") outfile = str(tmp_path / "temp.tif") im.save(outfile, compression="jpeg") @@ -669,7 +673,7 @@ class TestFileLibTiff(LibTiffTestCase): assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) - def test_exif_ifd(self, tmp_path): + def test_exif_ifd(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: assert im.tag_v2[34665] == 125456 @@ -679,7 +683,7 @@ class TestFileLibTiff(LibTiffTestCase): if Image.core.libtiff_support_custom_tags: assert reloaded.tag_v2[34665] == 125456 - def test_crashing_metadata(self, tmp_path): + def test_crashing_metadata(self, tmp_path: Path) -> None: # issue 1597 with Image.open("Tests/images/rdf.tif") as im: out = str(tmp_path / "temp.tif") @@ -689,7 +693,7 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, format="TIFF") TiffImagePlugin.WRITE_LIBTIFF = False - def test_page_number_x_0(self, tmp_path): + def test_page_number_x_0(self, tmp_path: Path) -> None: # Issue 973 # Test TIFF with tag 297 (Page Number) having value of 0 0. # The first number is the current page number. @@ -703,7 +707,7 @@ class TestFileLibTiff(LibTiffTestCase): # Should not divide by zero im.save(outfile) - def test_fd_duplication(self, tmp_path): + def test_fd_duplication(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1651 tmpfile = str(tmp_path / "temp.tif") @@ -717,7 +721,7 @@ class TestFileLibTiff(LibTiffTestCase): # Should not raise PermissionError. os.remove(tmpfile) - def test_read_icc(self): + def test_read_icc(self) -> None: with Image.open("Tests/images/hopper.iccprofile.tif") as img: icc = img.info.get("icc_profile") assert icc is not None @@ -728,8 +732,8 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False assert icc == icc_libtiff - def test_write_icc(self, tmp_path): - def check_write(libtiff): + def test_write_icc(self, tmp_path: Path) -> None: + def check_write(libtiff: bool) -> None: TiffImagePlugin.WRITE_LIBTIFF = libtiff with Image.open("Tests/images/hopper.iccprofile.tif") as img: @@ -748,7 +752,7 @@ class TestFileLibTiff(LibTiffTestCase): for libtiff in libtiffs: check_write(libtiff) - def test_multipage_compression(self): + def test_multipage_compression(self) -> None: with Image.open("Tests/images/compression.tif") as im: im.seek(0) assert im._compression == "tiff_ccitt" @@ -764,7 +768,7 @@ class TestFileLibTiff(LibTiffTestCase): assert im.size == (10, 10) im.load() - def test_save_tiff_with_jpegtables(self, tmp_path): + def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None: # Arrange outfile = str(tmp_path / "temp.tif") @@ -776,7 +780,7 @@ class TestFileLibTiff(LibTiffTestCase): # Should not raise UnicodeDecodeError or anything else im.save(outfile) - def test_16bit_RGB_tiff(self): + def test_16bit_RGB_tiff(self) -> None: with Image.open("Tests/images/tiff_16bit_RGB.tiff") as im: assert im.mode == "RGB" assert im.size == (100, 40) @@ -792,7 +796,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - def test_16bit_RGBa_tiff(self): + def test_16bit_RGBa_tiff(self) -> None: with Image.open("Tests/images/tiff_16bit_RGBa.tiff") as im: assert im.mode == "RGBA" assert im.size == (100, 40) @@ -804,7 +808,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") @skip_unless_feature("jpg") - def test_gimp_tiff(self): + def test_gimp_tiff(self) -> None: # Read TIFF JPEG images from GIMP [@PIL168] filename = "Tests/images/pil168.tif" with Image.open(filename) as im: @@ -817,14 +821,14 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/pil168.png") - def test_sampleformat(self): + def test_sampleformat(self) -> None: # https://github.com/python-pillow/Pillow/issues/1466 with Image.open("Tests/images/copyleft.tiff") as im: assert im.mode == "RGB" assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode="RGB") - def test_sampleformat_write(self, tmp_path): + def test_sampleformat_write(self, tmp_path: Path) -> None: im = Image.new("F", (1, 1)) out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True @@ -835,7 +839,7 @@ class TestFileLibTiff(LibTiffTestCase): assert reloaded.mode == "F" assert reloaded.getexif()[SAMPLEFORMAT] == 3 - def test_lzma(self, capfd): + def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None: try: with Image.open("Tests/images/hopper_lzma.tif") as im: assert im.mode == "RGB" @@ -851,7 +855,7 @@ class TestFileLibTiff(LibTiffTestCase): sys.stderr.write(captured.err) raise - def test_webp(self, capfd): + def test_webp(self, capfd: pytest.CaptureFixture[str]) -> None: try: with Image.open("Tests/images/hopper_webp.tif") as im: assert im.mode == "RGB" @@ -873,7 +877,7 @@ class TestFileLibTiff(LibTiffTestCase): sys.stderr.write(captured.err) raise - def test_lzw(self): + def test_lzw(self) -> None: with Image.open("Tests/images/hopper_lzw.tif") as im: assert im.mode == "RGB" assert im.size == (128, 128) @@ -881,12 +885,12 @@ class TestFileLibTiff(LibTiffTestCase): im2 = hopper() assert_image_similar(im, im2, 5) - def test_strip_cmyk_jpeg(self): + def test_strip_cmyk_jpeg(self) -> None: infile = "Tests/images/tiff_strip_cmyk_jpeg.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - def test_strip_cmyk_16l_jpeg(self): + def test_strip_cmyk_16l_jpeg(self) -> None: infile = "Tests/images/tiff_strip_cmyk_16l_jpeg.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) @@ -894,7 +898,7 @@ class TestFileLibTiff(LibTiffTestCase): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_strip_ycbcr_jpeg_2x2_sampling(self): + def test_strip_ycbcr_jpeg_2x2_sampling(self) -> None: infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.2) @@ -902,12 +906,12 @@ class TestFileLibTiff(LibTiffTestCase): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_strip_ycbcr_jpeg_1x1_sampling(self): + def test_strip_ycbcr_jpeg_1x1_sampling(self) -> None: infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) - def test_tiled_cmyk_jpeg(self): + def test_tiled_cmyk_jpeg(self) -> None: infile = "Tests/images/tiff_tiled_cmyk_jpeg.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) @@ -915,7 +919,7 @@ class TestFileLibTiff(LibTiffTestCase): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_tiled_ycbcr_jpeg_1x1_sampling(self): + def test_tiled_ycbcr_jpeg_1x1_sampling(self) -> None: infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower2.jpg", 0.01) @@ -923,45 +927,45 @@ class TestFileLibTiff(LibTiffTestCase): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_tiled_ycbcr_jpeg_2x2_sampling(self): + def test_tiled_ycbcr_jpeg_2x2_sampling(self) -> None: infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower.jpg", 1.5) - def test_strip_planar_rgb(self): + def test_strip_planar_rgb(self) -> None: # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_strip_raw.tif tiff_strip_planar_lzw.tiff infile = "Tests/images/tiff_strip_planar_lzw.tiff" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_tiled_planar_rgb(self): + def test_tiled_planar_rgb(self) -> None: # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_tiled_raw.tif tiff_tiled_planar_lzw.tiff infile = "Tests/images/tiff_tiled_planar_lzw.tiff" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_tiled_planar_16bit_RGB(self): + def test_tiled_planar_16bit_RGB(self) -> None: # gdal_translate -co TILED=yes -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_16bit_RGB.tiff tiff_tiled_planar_16bit_RGB.tiff with Image.open("Tests/images/tiff_tiled_planar_16bit_RGB.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - def test_strip_planar_16bit_RGB(self): + def test_strip_planar_16bit_RGB(self) -> None: # gdal_translate -co TILED=no -co INTERLEAVE=BAND -co COMPRESS=LZW \ # tiff_16bit_RGB.tiff tiff_strip_planar_16bit_RGB.tiff with Image.open("Tests/images/tiff_strip_planar_16bit_RGB.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGB_target.png") - def test_tiled_planar_16bit_RGBa(self): + def test_tiled_planar_16bit_RGBa(self) -> None: # gdal_translate -co TILED=yes \ # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ # tiff_16bit_RGBa.tiff tiff_tiled_planar_16bit_RGBa.tiff with Image.open("Tests/images/tiff_tiled_planar_16bit_RGBa.tiff") as im: assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") - def test_strip_planar_16bit_RGBa(self): + def test_strip_planar_16bit_RGBa(self) -> None: # gdal_translate -co TILED=no \ # -co INTERLEAVE=BAND -co COMPRESS=LZW -co ALPHA=PREMULTIPLIED \ # tiff_16bit_RGBa.tiff tiff_strip_planar_16bit_RGBa.tiff @@ -969,7 +973,7 @@ class TestFileLibTiff(LibTiffTestCase): 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): + def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -985,11 +989,11 @@ class TestFileLibTiff(LibTiffTestCase): for tag in tags: assert tag not in reloaded.getexif() - def test_old_style_jpeg(self): + def test_old_style_jpeg(self) -> None: with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") - def test_open_missing_samplesperpixel(self): + def test_open_missing_samplesperpixel(self) -> None: with Image.open( "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" ) as im: @@ -1018,21 +1022,23 @@ class TestFileLibTiff(LibTiffTestCase): ), ], ) - def test_wrong_bits_per_sample(self, file_name, mode, size, tile): + def test_wrong_bits_per_sample( + self, file_name: str, mode: str, size: tuple[int, int], tile + ) -> None: with Image.open("Tests/images/" + file_name) as im: assert im.mode == mode assert im.size == size assert im.tile == tile im.load() - def test_no_rows_per_strip(self): + def test_no_rows_per_strip(self) -> None: # This image does not have a RowsPerStrip TIFF tag infile = "Tests/images/no_rows_per_strip.tif" with Image.open(infile) as im: im.load() assert im.size == (950, 975) - def test_orientation(self): + def test_orientation(self) -> None: with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: @@ -1043,7 +1049,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_similar(base_im, im, 0.7) - def test_exif_transpose(self): + def test_exif_transpose(self) -> None: with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: @@ -1052,7 +1058,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_similar(base_im, im, 0.7) @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") - def test_sampleformat_not_corrupted(self): + def test_sampleformat_not_corrupted(self) -> None: # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted # when saving to a new file. # Pillow 6.0 fails with "OSError: cannot identify image file". @@ -1073,7 +1079,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as im: im.load() - def test_realloc_overflow(self): + def test_realloc_overflow(self) -> None: TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: with pytest.raises(OSError) as e: @@ -1084,7 +1090,7 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) - def test_save_multistrip(self, compression, tmp_path): + def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") im.save(out, compression=compression) @@ -1094,14 +1100,14 @@ class TestFileLibTiff(LibTiffTestCase): assert len(im.tag_v2[STRIPOFFSETS]) > 1 @pytest.mark.parametrize("argument", (True, False)) - def test_save_single_strip(self, argument, tmp_path): + def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") if not argument: TiffImagePlugin.STRIP_SIZE = 2**18 try: - arguments = {"compression": "tiff_adobe_deflate"} + arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} if argument: arguments["strip_size"] = 2**18 im.save(out, **arguments) @@ -1112,13 +1118,13 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.STRIP_SIZE = 65536 @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) - def test_save_zero(self, compression, tmp_path): + def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: im = Image.new("RGB", (0, 0)) out = str(tmp_path / "temp.tif") with pytest.raises(SystemError): im.save(out, compression=compression) - def test_save_many_compressed(self, tmp_path): + def test_save_many_compressed(self, tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.tif") for _ in range(10000): @@ -1132,7 +1138,7 @@ class TestFileLibTiff(LibTiffTestCase): ("Tests/images/child_ifd_jpeg.tiff", (20,)), ), ) - def test_get_child_images(self, path, sizes): + def test_get_child_images(self, path: str, sizes: tuple[int, ...]) -> None: with Image.open(path) as im: ims = im.get_child_images() diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 9501c55a6..617e1e89c 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,5 +1,7 @@ from __future__ import annotations + from io import BytesIO +from pathlib import Path from PIL import Image @@ -7,7 +9,6 @@ from .test_file_libtiff import LibTiffTestCase class TestFileLibTiffSmall(LibTiffTestCase): - """The small lena image was failing on open in the libtiff decoder because the file pointer was set to the wrong place by a spurious seek. It wasn't failing with the byteio method. @@ -16,7 +17,7 @@ class TestFileLibTiffSmall(LibTiffTestCase): file just before reading in libtiff. These tests remain to ensure that it stays fixed.""" - def test_g4_hopper_file(self, tmp_path): + def test_g4_hopper_file(self, tmp_path: Path) -> None: """Testing the open file load path""" test_file = "Tests/images/hopper_g4.tif" @@ -25,7 +26,7 @@ class TestFileLibTiffSmall(LibTiffTestCase): assert im.size == (128, 128) self._assert_noerr(tmp_path, im) - def test_g4_hopper_bytesio(self, tmp_path): + def test_g4_hopper_bytesio(self, tmp_path: Path) -> None: """Testing the bytesio loading code path""" test_file = "Tests/images/hopper_g4.tif" s = BytesIO() @@ -36,7 +37,7 @@ class TestFileLibTiffSmall(LibTiffTestCase): assert im.size == (128, 128) self._assert_noerr(tmp_path, im) - def test_g4_hopper(self, tmp_path): + def test_g4_hopper(self, tmp_path: Path) -> None: """The 128x128 lena image failed for some reason.""" test_file = "Tests/images/hopper_g4.tif" diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index 4b31aaa78..2c94fdc39 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, McIdasImagePlugin @@ -6,14 +7,14 @@ from PIL import Image, McIdasImagePlugin from .helper import assert_image_equal_tofile -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): McIdasImagePlugin.McIdasImageFile(invalid_file) -def test_valid_file(): +def test_valid_file() -> None: # Arrange # https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8 # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index e7ea39ea9..9a6f13ea3 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImagePalette @@ -12,7 +13,7 @@ pytestmark = skip_unless_feature("libtiff") TEST_FILE = "Tests/images/hopper.mic" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "RGBA" @@ -27,22 +28,22 @@ def test_sanity(): assert_image_similar(im, im2, 10) -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_FILE) as im: assert im.n_frames == 1 -def test_is_animated(): +def test_is_animated() -> None: with Image.open(TEST_FILE) as im: assert not im.is_animated -def test_tell(): +def test_tell() -> None: with Image.open(TEST_FILE) as im: assert im.tell() == 0 -def test_seek(): +def test_seek() -> None: with Image.open(TEST_FILE) as im: im.seek(0) assert im.tell() == 0 @@ -52,7 +53,7 @@ def test_seek(): assert im.tell() == 0 -def test_close(): +def test_close() -> None: with Image.open(TEST_FILE) as im: pass assert im.ole.fp.closed @@ -62,7 +63,7 @@ def test_close(): assert im.ole.fp.closed -def test_invalid_file(): +def test_invalid_file() -> None: # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index da62bc6d4..4fb00d699 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,6 +1,8 @@ from __future__ import annotations + import warnings from io import BytesIO +from typing import Any import pytest @@ -18,7 +20,7 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] pytestmark = skip_unless_feature("jpg") -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "MPO", **options) test_bytes = out.tell() @@ -29,7 +31,7 @@ def roundtrip(im, **options): @pytest.mark.parametrize("test_file", test_files) -def test_sanity(test_file): +def test_sanity(test_file: str) -> None: with Image.open(test_file) as im: im.load() assert im.mode == "RGB" @@ -38,8 +40,8 @@ def test_sanity(test_file): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(test_files[0]) im.load() @@ -47,14 +49,14 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(test_files[0]) im.load() im.close() -def test_seek_after_close(): +def test_seek_after_close() -> None: im = Image.open(test_files[0]) im.close() @@ -62,14 +64,14 @@ def test_seek_after_close(): im.seek(1) -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(test_files[0]) as im: im.load() @pytest.mark.parametrize("test_file", test_files) -def test_app(test_file): +def test_app(test_file: str) -> None: # Test APP/COM reader (@PIL135) with Image.open(test_file) as im: assert im.applist[0][0] == "APP1" @@ -81,7 +83,7 @@ def test_app(test_file): @pytest.mark.parametrize("test_file", test_files) -def test_exif(test_file): +def test_exif(test_file: str) -> None: with Image.open(test_file) as im_original: im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) @@ -92,7 +94,7 @@ def test_exif(test_file): assert info[34665] == 188 -def test_frame_size(): +def test_frame_size() -> None: # This image has been hexedited to contain a different size # in the EXIF data of the second frame with Image.open("Tests/images/sugarshack_frame_size.mpo") as im: @@ -105,7 +107,7 @@ def test_frame_size(): assert im.size == (640, 480) -def test_ignore_frame_size(): +def test_ignore_frame_size() -> None: # Ignore the different size of the second frame # since this is not a "Large Thumbnail" image with Image.open("Tests/images/ignore_frame_size.mpo") as im: @@ -119,7 +121,7 @@ def test_ignore_frame_size(): assert im.size == (64, 64) -def test_parallax(): +def test_parallax() -> None: # Nintendo with Image.open("Tests/images/sugarshack.mpo") as im: exif = im.getexif() @@ -132,7 +134,7 @@ def test_parallax(): assert exif.get_ifd(0x927C)[0xB211] == -3.125 -def test_reload_exif_after_seek(): +def test_reload_exif_after_seek() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: exif = im.getexif() del exif[296] @@ -142,14 +144,14 @@ def test_reload_exif_after_seek(): @pytest.mark.parametrize("test_file", test_files) -def test_mp(test_file): +def test_mp(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 -def test_mp_offset(): +def test_mp_offset() -> None: # This image has been manually hexedited to have an IFD offset of 10 # in APP2 data, in contrast to normal 8 with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: @@ -158,7 +160,7 @@ def test_mp_offset(): assert mpinfo[45057] == 2 -def test_mp_no_data(): +def test_mp_no_data() -> None: # This image has been manually hexedited to have the second frame # beyond the end of the file with Image.open("Tests/images/sugarshack_no_data.mpo") as im: @@ -167,7 +169,7 @@ def test_mp_no_data(): @pytest.mark.parametrize("test_file", test_files) -def test_mp_attribute(test_file): +def test_mp_attribute(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() for frame_number, mpentry in enumerate(mpinfo[0xB002]): @@ -184,7 +186,7 @@ def test_mp_attribute(test_file): @pytest.mark.parametrize("test_file", test_files) -def test_seek(test_file): +def test_seek(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 # prior to first image raises an error, both blatant and borderline @@ -208,13 +210,13 @@ def test_seek(test_file): assert im.tell() == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: assert im.n_frames == 2 assert im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: n_frames = im.n_frames @@ -228,7 +230,7 @@ def test_eoferror(): @pytest.mark.parametrize("test_file", test_files) -def test_image_grab(test_file): +def test_image_grab(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 im0 = im.tobytes() @@ -243,7 +245,7 @@ def test_image_grab(test_file): @pytest.mark.parametrize("test_file", test_files) -def test_save(test_file): +def test_save(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 jpg0 = roundtrip(im) @@ -254,7 +256,7 @@ def test_save(test_file): assert_image_similar(im, jpg1, 30) -def test_save_all(): +def test_save_all() -> None: for test_file in test_files: with Image.open(test_file) as im: im_reloaded = roundtrip(im, save_all=True) diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index f4e357ae0..b0964aabe 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,5 +1,7 @@ from __future__ import annotations + import os +from pathlib import Path import pytest @@ -12,7 +14,7 @@ EXTRA_DIR = "Tests/images/picins" YA_EXTRA_DIR = "Tests/images/msp" -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.msp") hopper("1").save(test_file) @@ -24,14 +26,14 @@ def test_sanity(tmp_path): assert im.format == "MSP" -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): MspImagePlugin.MspImageFile(invalid_file) -def test_bad_checksum(): +def test_bad_checksum() -> None: # Arrange # This was created by forcing Pillow to save with checksum=0 bad_checksum = "Tests/images/hopper_bad_checksum.msp" @@ -41,7 +43,7 @@ def test_bad_checksum(): MspImagePlugin.MspImageFile(bad_checksum) -def test_open_windows_v1(): +def test_open_windows_v1() -> None: # Arrange # Act with Image.open(TEST_FILE) as im: @@ -50,7 +52,7 @@ def test_open_windows_v1(): assert isinstance(im, MspImagePlugin.MspImageFile) -def _assert_file_image_equal(source_path, target_path): +def _assert_file_image_equal(source_path: str, target_path: str) -> None: with Image.open(source_path) as im: assert_image_equal_tofile(im, target_path) @@ -58,7 +60,7 @@ def _assert_file_image_equal(source_path, target_path): @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) -def test_open_windows_v2(): +def test_open_windows_v2() -> None: files = ( os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) @@ -71,7 +73,7 @@ def test_open_windows_v2(): @pytest.mark.skipif( not os.path.exists(YA_EXTRA_DIR), reason="Even More Extra image files not installed" ) -def test_msp_v2(): +def test_msp_v2() -> None: for f in os.listdir(YA_EXTRA_DIR): if ".MSP" not in f: continue @@ -79,7 +81,7 @@ def test_msp_v2(): _assert_file_image_equal(path, path.replace(".MSP", ".png")) -def test_cannot_save_wrong_mode(tmp_path): +def test_cannot_save_wrong_mode(tmp_path: Path) -> None: # Arrange im = hopper() filename = str(tmp_path / "temp.msp") diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 735840de4..194f39b30 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -1,6 +1,8 @@ from __future__ import annotations + import os.path import subprocess +from pathlib import Path import pytest @@ -9,7 +11,7 @@ from PIL import Image from .helper import assert_image_equal, hopper, magick_command -def helper_save_as_palm(tmp_path, mode): +def helper_save_as_palm(tmp_path: Path, mode: str) -> None: # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".palm")) @@ -22,7 +24,7 @@ def helper_save_as_palm(tmp_path, mode): assert os.path.getsize(outfile) > 0 -def open_with_magick(magick, tmp_path, f): +def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image: outfile = str(tmp_path / "temp.png") rc = subprocess.call( magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT @@ -31,7 +33,7 @@ def open_with_magick(magick, tmp_path, f): return Image.open(outfile) -def roundtrip(tmp_path, mode): +def roundtrip(tmp_path: Path, mode: str) -> None: magick = magick_command() if not magick: return @@ -44,7 +46,7 @@ def roundtrip(tmp_path, mode): assert_image_equal(converted, im) -def test_monochrome(tmp_path): +def test_monochrome(tmp_path: Path) -> None: # Arrange mode = "1" @@ -54,7 +56,7 @@ def test_monochrome(tmp_path): @pytest.mark.xfail(reason="Palm P image is wrong") -def test_p_mode(tmp_path): +def test_p_mode(tmp_path: Path) -> None: # Arrange mode = "P" @@ -64,6 +66,6 @@ def test_p_mode(tmp_path): @pytest.mark.parametrize("mode", ("L", "RGB")) -def test_oserror(tmp_path, mode): +def test_oserror(tmp_path: Path, mode: str) -> None: with pytest.raises(OSError): helper_save_as_palm(tmp_path, mode) diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py index 596a3414f..81a316fc1 100644 --- a/Tests/test_file_pcd.py +++ b/Tests/test_file_pcd.py @@ -1,8 +1,9 @@ from __future__ import annotations + from PIL import Image -def test_load_raw(): +def test_load_raw() -> None: with Image.open("Tests/images/hopper.pcd") as im: im.load() # should not segfault. diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index f42ec4a68..ab9f9663e 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image, ImageFile, PcxImagePlugin @@ -6,7 +9,7 @@ from PIL import Image, ImageFile, PcxImagePlugin from .helper import assert_image_equal, hopper -def _roundtrip(tmp_path, im): +def _roundtrip(tmp_path: Path, im: Image.Image) -> None: f = str(tmp_path / "temp.pcx") im.save(f) with Image.open(f) as im2: @@ -17,7 +20,7 @@ def _roundtrip(tmp_path, im): assert_image_equal(im2, im) -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: for mode in ("1", "L", "P", "RGB"): _roundtrip(tmp_path, hopper(mode)) @@ -33,7 +36,7 @@ def test_sanity(tmp_path): im.save(f) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): @@ -41,7 +44,7 @@ def test_invalid_file(): @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) -def test_odd(tmp_path, mode): +def test_odd(tmp_path: Path, mode: str) -> None: # See issue #523, odd sized images should have a stride that's even. # Not that ImageMagick or GIMP write PCX that way. # We were not handling properly. @@ -50,7 +53,7 @@ def test_odd(tmp_path, mode): _roundtrip(tmp_path, hopper(mode).resize((511, 511))) -def test_odd_read(): +def test_odd_read() -> None: # Reading an image with an odd stride, making it malformed with Image.open("Tests/images/odd_stride.pcx") as im: im.load() @@ -58,7 +61,7 @@ def test_odd_read(): assert im.size == (371, 150) -def test_pil184(): +def test_pil184() -> None: # Check reading of files where xmin/xmax is not zero. test_file = "Tests/images/pil184.pcx" @@ -70,7 +73,7 @@ def test_pil184(): assert im.histogram()[0] + im.histogram()[255] == 447 * 144 -def test_1px_width(tmp_path): +def test_1px_width(tmp_path: Path) -> None: im = Image.new("L", (1, 256)) px = im.load() for y in range(256): @@ -78,7 +81,7 @@ def test_1px_width(tmp_path): _roundtrip(tmp_path, im) -def test_large_count(tmp_path): +def test_large_count(tmp_path: Path) -> None: im = Image.new("L", (256, 1)) px = im.load() for x in range(256): @@ -86,7 +89,7 @@ def test_large_count(tmp_path): _roundtrip(tmp_path, im) -def _test_buffer_overflow(tmp_path, im, size=1024): +def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) -> None: _last = ImageFile.MAXBLOCK ImageFile.MAXBLOCK = size try: @@ -95,7 +98,7 @@ def _test_buffer_overflow(tmp_path, im, size=1024): ImageFile.MAXBLOCK = _last -def test_break_in_count_overflow(tmp_path): +def test_break_in_count_overflow(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(4): @@ -104,7 +107,7 @@ def test_break_in_count_overflow(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_one_in_loop(tmp_path): +def test_break_one_in_loop(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(5): @@ -113,7 +116,7 @@ def test_break_one_in_loop(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_many_in_loop(tmp_path): +def test_break_many_in_loop(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(4): @@ -124,7 +127,7 @@ def test_break_many_in_loop(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_one_at_end(tmp_path): +def test_break_one_at_end(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(5): @@ -134,7 +137,7 @@ def test_break_one_at_end(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_many_at_end(tmp_path): +def test_break_many_at_end(tmp_path: Path) -> None: im = Image.new("L", (256, 5)) px = im.load() for y in range(5): @@ -146,7 +149,7 @@ def test_break_many_at_end(tmp_path): _test_buffer_overflow(tmp_path, im) -def test_break_padding(tmp_path): +def test_break_padding(tmp_path: Path) -> None: im = Image.new("L", (257, 5)) px = im.load() for y in range(5): diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 9e07d9ed0..d39a86565 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -1,9 +1,12 @@ from __future__ import annotations + import io import os import os.path import tempfile import time +from pathlib import Path +from typing import Any, Generator import pytest @@ -12,7 +15,7 @@ from PIL import Image, PdfParser, features from .helper import hopper, mark_if_feature_version, skip_unless_feature -def helper_save_as_pdf(tmp_path, mode, **kwargs): +def helper_save_as_pdf(tmp_path: Path, mode: str, **kwargs: Any) -> str: # Arrange im = hopper(mode) outfile = str(tmp_path / ("temp_" + mode + ".pdf")) @@ -39,17 +42,17 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs): @pytest.mark.parametrize("mode", ("L", "P", "RGB", "CMYK")) -def test_save(tmp_path, mode): +def test_save(tmp_path: Path, mode: str) -> None: helper_save_as_pdf(tmp_path, mode) @skip_unless_feature("jpg_2000") @pytest.mark.parametrize("mode", ("LA", "RGBA")) -def test_save_alpha(tmp_path, mode): +def test_save_alpha(tmp_path: Path, mode: str) -> None: helper_save_as_pdf(tmp_path, mode) -def test_p_alpha(tmp_path): +def test_p_alpha(tmp_path: Path) -> None: # Arrange outfile = str(tmp_path / "temp.pdf") with Image.open("Tests/images/pil123p.png") as im: @@ -65,7 +68,7 @@ def test_p_alpha(tmp_path): assert b"\n/SMask " in contents -def test_monochrome(tmp_path): +def test_monochrome(tmp_path: Path) -> None: # Arrange mode = "1" @@ -74,7 +77,7 @@ def test_monochrome(tmp_path): assert os.path.getsize(outfile) < (5000 if features.check("libtiff") else 15000) -def test_unsupported_mode(tmp_path): +def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("PA") outfile = str(tmp_path / "temp_PA.pdf") @@ -82,7 +85,7 @@ def test_unsupported_mode(tmp_path): im.save(outfile) -def test_resolution(tmp_path): +def test_resolution(tmp_path: Path) -> None: im = hopper() outfile = str(tmp_path / "temp.pdf") @@ -110,7 +113,7 @@ def test_resolution(tmp_path): {"dpi": (75, 150), "resolution": 200}, ), ) -def test_dpi(params, tmp_path): +def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: im = hopper() outfile = str(tmp_path / "temp.pdf") @@ -134,7 +137,7 @@ def test_dpi(params, tmp_path): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_save_all(tmp_path): +def test_save_all(tmp_path: Path) -> None: # Single frame image helper_save_as_pdf(tmp_path, "RGB", save_all=True) @@ -154,7 +157,7 @@ def test_save_all(tmp_path): assert os.path.getsize(outfile) > 0 # Test appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims im.save(outfile, save_all=True, append_images=im_generator(ims)) @@ -170,7 +173,7 @@ def test_save_all(tmp_path): assert os.path.getsize(outfile) > 0 -def test_multiframe_normal_save(tmp_path): +def test_multiframe_normal_save(tmp_path: Path) -> None: # Test saving a multiframe image without save_all with Image.open("Tests/images/dispose_bgnd.gif") as im: outfile = str(tmp_path / "temp.pdf") @@ -180,7 +183,7 @@ def test_multiframe_normal_save(tmp_path): assert os.path.getsize(outfile) > 0 -def test_pdf_open(tmp_path): +def test_pdf_open(tmp_path: Path) -> None: # fail on a buffer full of null bytes with pytest.raises(PdfParser.PdfFormatError): PdfParser.PdfParser(buf=bytearray(65536)) @@ -217,14 +220,14 @@ def test_pdf_open(tmp_path): assert not hopper_pdf.should_close_file -def test_pdf_append_fails_on_nonexistent_file(): +def test_pdf_append_fails_on_nonexistent_file() -> None: im = hopper("RGB") with tempfile.TemporaryDirectory() as temp_dir: with pytest.raises(OSError): im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) -def check_pdf_pages_consistency(pdf): +def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None: pages_info = pdf.read_indirect(pdf.pages_ref) assert b"Parent" not in pages_info assert b"Kids" in pages_info @@ -242,7 +245,7 @@ def check_pdf_pages_consistency(pdf): assert kids_not_used == [] -def test_pdf_append(tmp_path): +def test_pdf_append(tmp_path: Path) -> None: # make a PDF file pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser") @@ -293,7 +296,7 @@ def test_pdf_append(tmp_path): check_pdf_pages_consistency(pdf) -def test_pdf_info(tmp_path): +def test_pdf_info(tmp_path: Path) -> None: # make a PDF file pdf_filename = helper_save_as_pdf( tmp_path, @@ -322,7 +325,7 @@ def test_pdf_info(tmp_path): check_pdf_pages_consistency(pdf) -def test_pdf_append_to_bytesio(): +def test_pdf_append_to_bytesio() -> None: im = hopper("RGB") f = io.BytesIO() im.save(f, format="PDF") @@ -337,7 +340,7 @@ def test_pdf_append_to_bytesio(): @pytest.mark.timeout(1) @pytest.mark.skipif("PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower") @pytest.mark.parametrize("newline", (b"\r", b"\n")) -def test_redos(newline): +def test_redos(newline: bytes) -> None: malicious = b" trailer<<>>" + newline * 3456 # This particular exception isn't relevant here. diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index 63779f202..8f208cfbf 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, PixarImagePlugin @@ -8,7 +9,7 @@ from .helper import assert_image_similar, hopper TEST_FILE = "Tests/images/hopper.pxr" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "RGB" @@ -20,7 +21,7 @@ def test_sanity(): assert_image_similar(im, im2, 4.8) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index ff3862110..c51f56ce7 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,9 +1,13 @@ from __future__ import annotations + import re import sys import warnings import zlib from io import BytesIO +from pathlib import Path +from types import ModuleType +from typing import Any import pytest @@ -20,6 +24,7 @@ from .helper import ( skip_unless_feature, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: @@ -34,7 +39,7 @@ TEST_PNG_FILE = "Tests/images/hopper.png" MAGIC = PngImagePlugin._MAGIC -def chunk(cid, *data): +def chunk(cid: bytes, *data: bytes) -> bytes: test_file = BytesIO() PngImagePlugin.putchunk(*(test_file, cid) + data) return test_file.getvalue() @@ -50,11 +55,11 @@ HEAD = MAGIC + IHDR TAIL = IDAT + IEND -def load(data): +def load(data: bytes) -> Image.Image: return Image.open(BytesIO(data)) -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "PNG", **options) out.seek(0) @@ -63,7 +68,7 @@ def roundtrip(im, **options): @skip_unless_feature("zlib") class TestFilePng: - def get_chunks(self, filename): + def get_chunks(self, filename: str) -> list[bytes]: chunks = [] with open(filename, "rb") as fp: fp.read(8) @@ -78,7 +83,7 @@ class TestFilePng: png.crc(cid, s) return chunks - def test_sanity(self, tmp_path): + def test_sanity(self, tmp_path: Path) -> None: # internal version number assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib")) @@ -101,13 +106,13 @@ class TestFilePng: reloaded = reloaded.convert(mode) assert_image_equal(reloaded, im) - def test_invalid_file(self): + def test_invalid_file(self) -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): PngImagePlugin.PngImageFile(invalid_file) - def test_broken(self): + def test_broken(self) -> None: # Check reading of totally broken files. In this case, the test # file was checked into Subversion as a text file. @@ -116,7 +121,7 @@ class TestFilePng: with Image.open(test_file): pass - def test_bad_text(self): + def test_bad_text(self) -> None: # Make sure PIL can read malformed tEXt chunks (@PIL152) im = load(HEAD + chunk(b"tEXt") + TAIL) @@ -134,7 +139,7 @@ class TestFilePng: im = load(HEAD + chunk(b"tEXt", b"spam\0egg\0") + TAIL) assert im.info == {"spam": "egg\x00"} - def test_bad_ztxt(self): + def test_bad_ztxt(self) -> None: # Test reading malformed zTXt chunks (python-pillow/Pillow#318) im = load(HEAD + chunk(b"zTXt") + TAIL) @@ -155,7 +160,7 @@ class TestFilePng: im = load(HEAD + chunk(b"zTXt", b"spam\0\0" + zlib.compress(b"egg")) + TAIL) assert im.info == {"spam": "egg"} - def test_bad_itxt(self): + def test_bad_itxt(self) -> None: im = load(HEAD + chunk(b"iTXt") + TAIL) assert im.info == {} @@ -199,7 +204,7 @@ class TestFilePng: assert im.info["spam"].lang == "en" assert im.info["spam"].tkey == "Spam" - def test_interlace(self): + def test_interlace(self) -> None: test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) @@ -214,7 +219,7 @@ class TestFilePng: im.load() - def test_load_transparent_p(self): + def test_load_transparent_p(self) -> None: test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) @@ -224,7 +229,7 @@ class TestFilePng: # image has 124 unique alpha values assert len(im.getchannel("A").getcolors()) == 124 - def test_load_transparent_rgb(self): + def test_load_transparent_rgb(self) -> None: test_file = "Tests/images/rgb_trns.png" with Image.open(test_file) as im: assert im.info["transparency"] == (0, 255, 52) @@ -236,7 +241,7 @@ class TestFilePng: # image has 876 transparent pixels assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_palette(self, tmp_path): + def test_save_p_transparent_palette(self, tmp_path: Path) -> None: in_file = "Tests/images/pil123p.png" with Image.open(in_file) as im: # 'transparency' contains a byte string with the opacity for @@ -257,7 +262,7 @@ class TestFilePng: # image has 124 unique alpha values assert len(im.getchannel("A").getcolors()) == 124 - def test_save_p_single_transparency(self, tmp_path): + def test_save_p_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/p_trns_single.png" with Image.open(in_file) as im: # pixel value 164 is full transparent @@ -280,7 +285,7 @@ class TestFilePng: # image has 876 transparent pixels assert im.getchannel("A").getcolors()[0][0] == 876 - def test_save_p_transparent_black(self, tmp_path): + def test_save_p_transparent_black(self, tmp_path: Path) -> None: # check if solid black image with full transparency # is supported (check for #1838) im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) @@ -298,7 +303,7 @@ class TestFilePng: assert_image(im, "RGBA", (10, 10)) assert im.getcolors() == [(100, (0, 0, 0, 0))] - def test_save_grayscale_transparency(self, tmp_path): + def test_save_grayscale_transparency(self, tmp_path: Path) -> None: for mode, num_transparent in {"1": 1994, "L": 559, "I": 559}.items(): in_file = "Tests/images/" + mode.lower() + "_trns.png" with Image.open(in_file) as im: @@ -319,13 +324,13 @@ class TestFilePng: test_im_rgba = test_im.convert("RGBA") assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent - def test_save_rgb_single_transparency(self, tmp_path): + def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/caption_6_33_22.png" with Image.open(in_file) as im: test_file = str(tmp_path / "temp.png") im.save(test_file) - def test_load_verify(self): + def test_load_verify(self) -> None: # Check open/load/verify exception (@PIL150) with Image.open(TEST_PNG_FILE) as im: @@ -338,7 +343,7 @@ class TestFilePng: with pytest.raises(RuntimeError): im.verify() - def test_verify_struct_error(self): + def test_verify_struct_error(self) -> None: # Check open/load/verify exception (#1755) # offsets to test, -10: breaks in i32() in read. (OSError) @@ -354,7 +359,7 @@ class TestFilePng: with pytest.raises((OSError, SyntaxError)): im.verify() - def test_verify_ignores_crc_error(self): + def test_verify_ignores_crc_error(self) -> None: # check ignores crc errors in ancillary chunks chunk_data = chunk(b"tEXt", b"spam") @@ -371,7 +376,7 @@ class TestFilePng: finally: ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_verify_not_ignores_crc_error_in_required_chunk(self): + def test_verify_not_ignores_crc_error_in_required_chunk(self) -> None: # check does not ignore crc errors in required chunks image_data = MAGIC + IHDR[:-1] + b"q" + TAIL @@ -383,18 +388,18 @@ class TestFilePng: finally: ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_roundtrip_dpi(self): + def test_roundtrip_dpi(self) -> None: # Check dpi roundtripping with Image.open(TEST_PNG_FILE) as im: im = roundtrip(im, dpi=(100.33, 100.33)) assert im.info["dpi"] == (100.33, 100.33) - def test_load_float_dpi(self): + def test_load_float_dpi(self) -> None: with Image.open(TEST_PNG_FILE) as im: assert im.info["dpi"] == (95.9866, 95.9866) - def test_roundtrip_text(self): + def test_roundtrip_text(self) -> None: # Check text roundtripping with Image.open(TEST_PNG_FILE) as im: @@ -406,7 +411,7 @@ class TestFilePng: assert im.info == {"TXT": "VALUE", "ZIP": "VALUE"} assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} - def test_roundtrip_itxt(self): + def test_roundtrip_itxt(self) -> None: # Check iTXt roundtripping im = Image.new("RGB", (32, 32)) @@ -422,7 +427,7 @@ class TestFilePng: assert im.text["eggs"].lang == "en" assert im.text["eggs"].tkey == "Eggs" - def test_nonunicode_text(self): + def test_nonunicode_text(self) -> None: # Check so that non-Unicode text is saved as a tEXt rather than iTXt im = Image.new("RGB", (32, 32)) @@ -431,10 +436,10 @@ class TestFilePng: im = roundtrip(im, pnginfo=info) assert isinstance(im.info["Text"], str) - def test_unicode_text(self): + def test_unicode_text(self) -> None: # Check preservation of non-ASCII characters - def rt_text(value): + def rt_text(value: str) -> None: im = Image.new("RGB", (32, 32)) info = PngImagePlugin.PngInfo() info.add_text("Text", value) @@ -447,7 +452,7 @@ class TestFilePng: rt_text(chr(0x4E00) + chr(0x66F0) + chr(0x9FBA) + chr(0x3042) + chr(0xAC00)) rt_text("A" + chr(0xC4) + chr(0x472) + chr(0x3042)) # Combined - def test_scary(self): + def test_scary(self) -> None: # Check reading of evil PNG file. For information, see: # http://scary.beasts.org/security/CESA-2004-001.txt # The first byte is removed from pngtest_bad.png @@ -461,7 +466,7 @@ class TestFilePng: with Image.open(pngfile): pass - def test_trns_rgb(self): + def test_trns_rgb(self) -> None: # Check writing and reading of tRNS chunks for RGB images. # Independent file sample provided by Sebastian Spaeth. @@ -476,7 +481,7 @@ class TestFilePng: im = roundtrip(im, transparency=(0, 1, 2)) assert im.info["transparency"] == (0, 1, 2) - def test_trns_p(self, tmp_path): + def test_trns_p(self, tmp_path: Path) -> None: # Check writing a transparency of 0, issue #528 im = hopper("P") im.info["transparency"] = 0 @@ -489,13 +494,13 @@ class TestFilePng: assert_image_equal(im2.convert("RGBA"), im.convert("RGBA")) - def test_trns_null(self): + def test_trns_null(self) -> None: # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" with Image.open(test_file) as im: assert im.info["transparency"] == 0 - def test_save_icc_profile(self): + def test_save_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile_none.png") as im: assert im.info["icc_profile"] is None @@ -505,40 +510,40 @@ class TestFilePng: im = roundtrip(im, icc_profile=expected_icc) assert im.info["icc_profile"] == expected_icc - def test_discard_icc_profile(self): + def test_discard_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile.png") as im: assert "icc_profile" in im.info im = roundtrip(im, icc_profile=None) assert "icc_profile" not in im.info - def test_roundtrip_icc_profile(self): + def test_roundtrip_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile.png") as im: expected_icc = im.info["icc_profile"] im = roundtrip(im) assert im.info["icc_profile"] == expected_icc - def test_roundtrip_no_icc_profile(self): + def test_roundtrip_no_icc_profile(self) -> None: with Image.open("Tests/images/icc_profile_none.png") as im: assert im.info["icc_profile"] is None im = roundtrip(im) assert "icc_profile" not in im.info - def test_repr_png(self): + def test_repr_png(self) -> None: im = hopper() with Image.open(BytesIO(im._repr_png_())) as repr_png: assert repr_png.format == "PNG" assert_image_equal(im, repr_png) - def test_repr_png_error_returns_none(self): + def test_repr_png_error_returns_none(self) -> None: im = hopper("F") assert im._repr_png_() is None - def test_chunk_order(self, tmp_path): + def test_chunk_order(self, tmp_path: Path) -> None: with Image.open("Tests/images/icc_profile.png") as im: test_file = str(tmp_path / "temp.png") im.convert("P").save(test_file, dpi=(100, 100)) @@ -559,17 +564,17 @@ class TestFilePng: # pHYs - before IDAT assert chunks.index(b"pHYs") < chunks.index(b"IDAT") - def test_getchunks(self): + def test_getchunks(self) -> None: im = hopper() chunks = PngImagePlugin.getchunks(im) assert len(chunks) == 3 - def test_read_private_chunks(self): + def test_read_private_chunks(self) -> None: with Image.open("Tests/images/exif.png") as im: assert im.private_chunks == [(b"orNT", b"\x01")] - def test_roundtrip_private_chunk(self): + def test_roundtrip_private_chunk(self) -> None: # Check private chunk roundtripping with Image.open(TEST_PNG_FILE) as im: @@ -587,7 +592,7 @@ class TestFilePng: (b"prIV", b"VALUE3", True), ] - def test_textual_chunks_after_idat(self): + def test_textual_chunks_after_idat(self) -> None: with Image.open("Tests/images/hopper.png") as im: assert "comment" in im.text for k, v in { @@ -614,7 +619,7 @@ class TestFilePng: with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} - def test_padded_idat(self): + def test_padded_idat(self) -> None: # This image has been manually hexedited # so that the IDAT chunk has padding at the end # Set MAXBLOCK to the length of the actual data @@ -634,7 +639,7 @@ class TestFilePng: @pytest.mark.parametrize( "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") ) - def test_truncated_chunks(self, cid): + def test_truncated_chunks(self, cid: bytes) -> None: fp = BytesIO() with PngImagePlugin.PngStream(fp) as png: with pytest.raises(ValueError): @@ -644,7 +649,7 @@ class TestFilePng: png.call(cid, 0, 0) ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_specify_bits(self, tmp_path): + def test_specify_bits(self, tmp_path: Path) -> None: im = hopper("P") out = str(tmp_path / "temp.png") @@ -653,7 +658,7 @@ class TestFilePng: with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 48 - def test_plte_length(self, tmp_path): + def test_plte_length(self, tmp_path: Path) -> None: im = Image.new("P", (1, 1)) im.putpalette((1, 1, 1)) @@ -663,7 +668,7 @@ class TestFilePng: with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 3 - def test_getxmp(self): + def test_getxmp(self) -> None: with Image.open("Tests/images/color_snakes.png") as im: if ElementTree is None: with pytest.warns( @@ -678,7 +683,7 @@ class TestFilePng: assert description["PixelXDimension"] == "10" assert description["subject"]["Seq"] is None - def test_exif(self): + def test_exif(self) -> None: # With an EXIF chunk with Image.open("Tests/images/exif.png") as im: exif = im._getexif() @@ -704,7 +709,7 @@ class TestFilePng: exif = im.getexif() assert exif[274] == 3 - def test_exif_save(self, tmp_path): + def test_exif_save(self, tmp_path: Path) -> None: # Test exif is not saved from info test_file = str(tmp_path / "temp.png") with Image.open("Tests/images/exif.png") as im: @@ -724,7 +729,7 @@ class TestFilePng: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_exif_from_jpg(self, tmp_path): + def test_exif_from_jpg(self, tmp_path: Path) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: test_file = str(tmp_path / "temp.png") im.save(test_file, exif=im.getexif()) @@ -733,7 +738,7 @@ class TestFilePng: exif = reloaded._getexif() assert exif[305] == "Adobe Photoshop CS Macintosh" - def test_exif_argument(self, tmp_path): + def test_exif_argument(self, tmp_path: Path) -> None: with Image.open(TEST_PNG_FILE) as im: test_file = str(tmp_path / "temp.png") im.save(test_file, exif=b"exifstring") @@ -741,11 +746,11 @@ class TestFilePng: with Image.open(test_file) as reloaded: assert reloaded.info["exif"] == b"Exif\x00\x00exifstring" - def test_tell(self): + def test_tell(self) -> None: with Image.open(TEST_PNG_FILE) as im: assert im.tell() == 0 - def test_seek(self): + def test_seek(self) -> None: with Image.open(TEST_PNG_FILE) as im: im.seek(0) @@ -753,7 +758,7 @@ class TestFilePng: im.seek(1) @pytest.mark.parametrize("buffer", (True, False)) - def test_save_stdout(self, buffer): + def test_save_stdout(self, buffer: bool) -> None: old_stdout = sys.stdout if buffer: @@ -785,7 +790,7 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): mem_limit = 2 * 1024 # max increase in K iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs - def test_leak_load(self): + def test_leak_load(self) -> None: with open("Tests/images/hopper.png", "rb") as f: DATA = BytesIO(f.read(16 * 1024)) @@ -793,7 +798,7 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase): with Image.open(DATA) as im: im.load() - def core(): + def core() -> None: with Image.open(DATA) as im: im.load() diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index bb49a46d3..6e0fa32e4 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,18 +1,25 @@ from __future__ import annotations + import sys from io import BytesIO +from pathlib import Path import pytest from PIL import Image, PpmImagePlugin -from .helper import assert_image_equal_tofile, assert_image_similar, hopper +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, +) # sample ppm stream TEST_FILE = "Tests/images/hopper.ppm" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: assert im.mode == "RGB" assert im.size == (128, 128) @@ -63,7 +70,9 @@ def test_sanity(): ), ), ) -def test_arbitrary_maxval(data, mode, pixels): +def test_arbitrary_maxval( + data: bytes, mode: str, pixels: tuple[int | tuple[int, int, int], ...] +) -> None: fp = BytesIO(data) with Image.open(fp) as im: assert im.size == (3, 1) @@ -73,7 +82,7 @@ def test_arbitrary_maxval(data, mode, pixels): assert tuple(px[x, 0] for x in range(3)) == pixels -def test_16bit_pgm(): +def test_16bit_pgm() -> None: with Image.open("Tests/images/16_bit_binary.pgm") as im: assert im.mode == "I" assert im.size == (20, 100) @@ -82,22 +91,60 @@ def test_16bit_pgm(): assert_image_equal_tofile(im, "Tests/images/16_bit_binary_pgm.png") -def test_16bit_pgm_write(tmp_path): +def test_16bit_pgm_write(tmp_path: Path) -> None: with Image.open("Tests/images/16_bit_binary.pgm") as im: - f = str(tmp_path / "temp.pgm") - im.save(f, "PPM") + filename = str(tmp_path / "temp.pgm") + im.save(filename, "PPM") - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) -def test_pnm(tmp_path): +def test_pnm(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.pnm") as im: assert_image_similar(im, hopper(), 0.0001) - f = str(tmp_path / "temp.pnm") - im.save(f) + filename = str(tmp_path / "temp.pnm") + im.save(filename) - assert_image_equal_tofile(im, f) + assert_image_equal_tofile(im, filename) + + +def test_pfm(tmp_path: Path) -> None: + with Image.open("Tests/images/hopper.pfm") as im: + assert im.info["scale"] == 1.0 + assert_image_equal(im, hopper("F")) + + filename = str(tmp_path / "tmp.pfm") + im.save(filename) + + assert_image_equal_tofile(im, filename) + + +def test_pfm_big_endian(tmp_path: Path) -> None: + with Image.open("Tests/images/hopper_be.pfm") as im: + assert im.info["scale"] == 2.5 + assert_image_equal(im, hopper("F")) + + filename = str(tmp_path / "tmp.pfm") + im.save(filename) + + assert_image_equal_tofile(im, filename) + + +@pytest.mark.parametrize( + "data", + [ + b"Pf 1 1 NaN \0\0\0\0", + b"Pf 1 1 inf \0\0\0\0", + b"Pf 1 1 -inf \0\0\0\0", + b"Pf 1 1 0.0 \0\0\0\0", + b"Pf 1 1 -0.0 \0\0\0\0", + ], +) +def test_pfm_invalid(data: bytes) -> None: + with pytest.raises(ValueError): + with Image.open(BytesIO(data)): + pass @pytest.mark.parametrize( @@ -117,12 +164,12 @@ def test_pnm(tmp_path): ), ), ) -def test_plain(plain_path, raw_path): +def test_plain(plain_path: str, raw_path: str) -> None: with Image.open(plain_path) as im: assert_image_equal_tofile(im, raw_path) -def test_16bit_plain_pgm(): +def test_16bit_plain_pgm() -> None: # P2 with maxval 2 ** 16 - 1 with Image.open("Tests/images/hopper_16bit_plain.pgm") as im: assert im.mode == "I" @@ -141,7 +188,9 @@ def test_16bit_plain_pgm(): (b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6), ), ) -def test_plain_data_with_comment(tmp_path, header, data, comment_count): +def test_plain_data_with_comment( + tmp_path: Path, header: bytes, data: bytes, comment_count: int +) -> None: path1 = str(tmp_path / "temp1.ppm") path2 = str(tmp_path / "temp2.ppm") comment = b"# comment" * comment_count @@ -154,7 +203,7 @@ def test_plain_data_with_comment(tmp_path, header, data, comment_count): @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) -def test_plain_truncated_data(tmp_path, data): +def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -165,7 +214,7 @@ def test_plain_truncated_data(tmp_path, data): @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) -def test_plain_invalid_data(tmp_path, data): +def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -182,7 +231,7 @@ def test_plain_invalid_data(tmp_path, data): b"P3\n128 128\n255\n012345678910 0", # token too long ), ) -def test_plain_ppm_token_too_long(tmp_path, data): +def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -192,7 +241,7 @@ def test_plain_ppm_token_too_long(tmp_path, data): im.load() -def test_plain_ppm_value_too_large(tmp_path): +def test_plain_ppm_value_too_large(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P3\n128 128\n255\n256") @@ -202,12 +251,12 @@ def test_plain_ppm_value_too_large(tmp_path): im.load() -def test_magic(): +def test_magic() -> None: with pytest.raises(SyntaxError): PpmImagePlugin.PpmImageFile(fp=BytesIO(b"PyInvalid")) -def test_header_with_comments(tmp_path): +def test_header_with_comments(tmp_path: Path) -> None: 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") @@ -216,7 +265,7 @@ def test_header_with_comments(tmp_path): assert im.size == (128, 128) -def test_non_integer_token(tmp_path): +def test_non_integer_token(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\nTEST") @@ -226,7 +275,7 @@ def test_non_integer_token(tmp_path): pass -def test_header_token_too_long(tmp_path): +def test_header_token_too_long(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n 01234567890") @@ -238,7 +287,7 @@ def test_header_token_too_long(tmp_path): assert str(e.value) == "Token too long in file header: 01234567890" -def test_truncated_file(tmp_path): +def test_truncated_file(tmp_path: Path) -> None: # Test EOF in header path = str(tmp_path / "temp.pgm") with open(path, "wb") as f: @@ -257,7 +306,7 @@ def test_truncated_file(tmp_path): im.load() -def test_not_enough_image_data(tmp_path): +def test_not_enough_image_data(tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P2 1 2 255 255") @@ -268,7 +317,7 @@ def test_not_enough_image_data(tmp_path): @pytest.mark.parametrize("maxval", (b"0", b"65536")) -def test_invalid_maxval(maxval, tmp_path): +def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n3 1 " + maxval) @@ -280,7 +329,7 @@ def test_invalid_maxval(maxval, tmp_path): assert str(e.value) == "maxval must be greater than 0 and less than 65536" -def test_neg_ppm(): +def test_neg_ppm() -> None: # Storage.c accepted negative values for xsize, ysize. the # internal open_ppm function didn't check for sanity but it # has been removed. The default opener doesn't accept negative @@ -291,7 +340,7 @@ def test_neg_ppm(): pass -def test_mimetypes(tmp_path): +def test_mimetypes(tmp_path: Path) -> None: path = str(tmp_path / "temp.pgm") with open(path, "wb") as f: @@ -306,7 +355,7 @@ def test_mimetypes(tmp_path): @pytest.mark.parametrize("buffer", (True, False)) -def test_save_stdout(buffer): +def test_save_stdout(buffer: bool) -> None: old_stdout = sys.stdout if buffer: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 8b06ce2b1..e60638b22 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest @@ -10,7 +11,7 @@ from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_ test_file = "Tests/images/hopper.psd" -def test_sanity(): +def test_sanity() -> None: with Image.open(test_file) as im: im.load() assert im.mode == "RGB" @@ -23,8 +24,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(test_file) im.load() @@ -32,27 +33,27 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(test_file) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(test_file) as im: im.load() -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): PsdImagePlugin.PsdImageFile(invalid_file) -def test_n_frames(): +def test_n_frames() -> None: with Image.open("Tests/images/hopper_merged.psd") as im: assert im.n_frames == 1 assert not im.is_animated @@ -63,7 +64,7 @@ def test_n_frames(): assert im.is_animated -def test_eoferror(): +def test_eoferror() -> None: with Image.open(test_file) as im: # PSD seek index starts at 1 rather than 0 n_frames = im.n_frames + 1 @@ -77,7 +78,7 @@ def test_eoferror(): im.seek(n_frames - 1) -def test_seek_tell(): +def test_seek_tell() -> None: with Image.open(test_file) as im: layer_number = im.tell() assert layer_number == 1 @@ -94,30 +95,30 @@ def test_seek_tell(): assert layer_number == 2 -def test_seek_eoferror(): +def test_seek_eoferror() -> None: with Image.open(test_file) as im: with pytest.raises(EOFError): im.seek(-1) -def test_open_after_exclusive_load(): +def test_open_after_exclusive_load() -> None: with Image.open(test_file) as im: im.load() im.seek(im.tell() + 1) im.load() -def test_rgba(): +def test_rgba() -> None: with Image.open("Tests/images/rgba.psd") as im: assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png") -def test_layer_skip(): +def test_layer_skip() -> None: with Image.open("Tests/images/five_channels.psd") as im: assert im.n_frames == 1 -def test_icc_profile(): +def test_icc_profile() -> None: with Image.open(test_file) as im: assert "icc_profile" in im.info @@ -125,12 +126,12 @@ def test_icc_profile(): assert len(icc_profile) == 3144 -def test_no_icc_profile(): +def test_no_icc_profile() -> None: with Image.open("Tests/images/hopper_merged.psd") as im: assert "icc_profile" not in im.info -def test_combined_larger_than_size(): +def test_combined_larger_than_size() -> None: # The combined size of the individual parts is larger than the # declared 'size' of the extra data field, resulting in a backwards seek. @@ -156,7 +157,7 @@ def test_combined_larger_than_size(): ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), ], ) -def test_crashes(test_file, raises): +def test_crashes(test_file: str, raises) -> None: with open(test_file, "rb") as f: with pytest.raises(raises): with Image.open(f): diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index b7c945729..fd4b981ce 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, QoiImagePlugin @@ -6,7 +7,7 @@ from PIL import Image, QoiImagePlugin from .helper import assert_image_equal_tofile -def test_sanity(): +def test_sanity() -> None: with Image.open("Tests/images/hopper.qoi") as im: assert im.mode == "RGB" assert im.size == (128, 128) @@ -22,7 +23,7 @@ def test_sanity(): assert_image_equal_tofile(im, "Tests/images/pil123rgba.png") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 13698276b..e13a8019e 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image, SgiImagePlugin @@ -11,7 +14,7 @@ from .helper import ( ) -def test_rgb(): +def test_rgb() -> None: # Created with ImageMagick then renamed: # convert hopper.ppm -compress None sgi:hopper.rgb test_file = "Tests/images/hopper.rgb" @@ -21,11 +24,11 @@ def test_rgb(): assert im.get_format_mimetype() == "image/rgb" -def test_rgb16(): +def test_rgb16() -> None: assert_image_equal_tofile(hopper(), "Tests/images/hopper16.rgb") -def test_l(): +def test_l() -> None: # Created with ImageMagick # convert hopper.ppm -monochrome -compress None sgi:hopper.bw test_file = "Tests/images/hopper.bw" @@ -35,7 +38,7 @@ def test_l(): assert im.get_format_mimetype() == "image/sgi" -def test_rgba(): +def test_rgba() -> None: # Created with ImageMagick: # convert transparent.png -compress None transparent.sgi test_file = "Tests/images/transparent.sgi" @@ -45,7 +48,7 @@ def test_rgba(): assert im.get_format_mimetype() == "image/sgi" -def test_rle(): +def test_rle() -> None: # Created with ImageMagick: # convert hopper.ppm hopper.sgi test_file = "Tests/images/hopper.sgi" @@ -54,22 +57,22 @@ def test_rle(): assert_image_equal_tofile(im, "Tests/images/hopper.rgb") -def test_rle16(): +def test_rle16() -> None: test_file = "Tests/images/tv16.sgi" with Image.open(test_file) as im: assert_image_equal_tofile(im, "Tests/images/tv.rgb") -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(ValueError): SgiImagePlugin.SgiImageFile(invalid_file) -def test_write(tmp_path): - def roundtrip(img): +def test_write(tmp_path: Path) -> None: + def roundtrip(img: Image.Image) -> None: out = str(tmp_path / "temp.sgi") img.save(out, format="sgi") assert_image_equal_tofile(img, out) @@ -88,7 +91,7 @@ def test_write(tmp_path): roundtrip(Image.new("L", (10, 1))) -def test_write16(tmp_path): +def test_write16(tmp_path: Path) -> None: test_file = "Tests/images/hopper16.rgb" with Image.open(test_file) as im: @@ -98,7 +101,7 @@ def test_write16(tmp_path): assert_image_equal_tofile(im, out) -def test_unsupported_mode(tmp_path): +def test_unsupported_mode(tmp_path: Path) -> None: im = hopper("LA") out = str(tmp_path / "temp.sgi") diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index f21098754..75fef1dc6 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,7 +1,9 @@ from __future__ import annotations + import tempfile import warnings from io import BytesIO +from pathlib import Path import pytest @@ -12,7 +14,7 @@ from .helper import assert_image_equal_tofile, hopper, is_pypy TEST_FILE = "Tests/images/hopper.spider" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "F" @@ -21,8 +23,8 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): - def open(): +def test_unclosed_file() -> None: + def open() -> None: im = Image.open(TEST_FILE) im.load() @@ -30,20 +32,20 @@ def test_unclosed_file(): open() -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() -def test_context_manager(): +def test_context_manager() -> None: with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: # Arrange temp = str(tmp_path / "temp.spider") im = hopper() @@ -58,7 +60,7 @@ def test_save(tmp_path): assert im2.format == "SPIDER" -def test_tempfile(): +def test_tempfile() -> None: # Arrange im = hopper() @@ -74,11 +76,11 @@ def test_tempfile(): assert reloaded.format == "SPIDER" -def test_is_spider_image(): +def test_is_spider_image() -> None: assert SpiderImagePlugin.isSpiderImage(TEST_FILE) -def test_tell(): +def test_tell() -> None: # Arrange with Image.open(TEST_FILE) as im: # Act @@ -88,13 +90,13 @@ def test_tell(): assert index == 0 -def test_n_frames(): +def test_n_frames() -> None: with Image.open(TEST_FILE) as im: assert im.n_frames == 1 assert not im.is_animated -def test_load_image_series(): +def test_load_image_series() -> None: # Arrange not_spider_file = "Tests/images/hopper.ppm" file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] @@ -108,7 +110,7 @@ def test_load_image_series(): assert img_list[0].size == (128, 128) -def test_load_image_series_no_input(): +def test_load_image_series_no_input() -> None: # Arrange file_list = None @@ -119,7 +121,7 @@ def test_load_image_series_no_input(): assert img_list is None -def test_is_int_not_a_number(): +def test_is_int_not_a_number() -> None: # Arrange not_a_number = "a" @@ -130,7 +132,7 @@ def test_is_int_not_a_number(): assert ret == 0 -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/invalid.spider" with pytest.raises(OSError): @@ -138,20 +140,20 @@ def test_invalid_file(): pass -def test_nonstack_file(): +def test_nonstack_file() -> None: with Image.open(TEST_FILE) as im: with pytest.raises(EOFError): im.seek(0) -def test_nonstack_dos(): +def test_nonstack_dos() -> None: with Image.open(TEST_FILE) as im: for i, frame in enumerate(ImageSequence.Iterator(im)): assert i <= 1, "Non-stack DOS file test failed" # for issue #4093 -def test_odd_size(): +def test_odd_size() -> None: data = BytesIO() width = 100 im = Image.new("F", (width, 64)) diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 874b37b52..6cfff8730 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import pytest @@ -10,7 +11,7 @@ from .helper import assert_image_equal_tofile, assert_image_similar, hopper EXTRA_DIR = "Tests/images/sunraster" -def test_sanity(): +def test_sanity() -> None: # Arrange # Created with ImageMagick: convert hopper.jpg hopper.ras test_file = "Tests/images/hopper.ras" @@ -27,7 +28,7 @@ def test_sanity(): SunImagePlugin.SunImageFile(invalid_file) -def test_im1(): +def test_im1() -> None: with Image.open("Tests/images/sunraster.im1") as im: assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png") @@ -35,7 +36,7 @@ def test_im1(): @pytest.mark.skipif( not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) -def test_others(): +def test_others() -> None: files = ( os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 4470823cd..6217ebedd 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest @@ -18,7 +19,7 @@ TEST_TAR_FILE = "Tests/images/hopper.tar" ("jpg", "hopper.jpg", "JPEG"), ), ) -def test_sanity(codec, test_path, format): +def test_sanity(codec: str, test_path: str, format: str) -> None: if features.check(codec): with TarIO.TarIO(TEST_TAR_FILE, test_path) as tar: with Image.open(tar) as im: @@ -29,18 +30,18 @@ def test_sanity(codec, test_path, format): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") -def test_unclosed_file(): +def test_unclosed_file() -> None: with pytest.warns(ResourceWarning): TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") -def test_close(): +def test_close() -> None: with warnings.catch_warnings(): tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar.close() -def test_contextmanager(): +def test_contextmanager() -> None: with warnings.catch_warnings(): with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): pass diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index d0f228573..3c6da50c5 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,7 +1,9 @@ from __future__ import annotations + import os from glob import glob from itertools import product +from pathlib import Path import pytest @@ -20,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} @pytest.mark.parametrize("mode", _MODES) -def test_sanity(mode, tmp_path): - def roundtrip(original_im): +def test_sanity(mode: str, tmp_path: Path) -> None: + def roundtrip(original_im: Image.Image) -> None: out = str(tmp_path / "temp.tga") original_im.save(out, rle=rle) @@ -63,7 +65,7 @@ def test_sanity(mode, tmp_path): roundtrip(original_im) -def test_palette_depth_16(tmp_path): +def test_palette_depth_16(tmp_path: Path) -> None: with Image.open("Tests/images/p_16.tga") as im: assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png") @@ -73,7 +75,7 @@ def test_palette_depth_16(tmp_path): assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png") -def test_id_field(): +def test_id_field() -> None: # tga file with id field test_file = "Tests/images/tga_id_field.tga" @@ -83,7 +85,7 @@ def test_id_field(): assert im.size == (100, 100) -def test_id_field_rle(): +def test_id_field_rle() -> None: # tga file with id field test_file = "Tests/images/rgb32rle.tga" @@ -93,7 +95,7 @@ def test_id_field_rle(): assert im.size == (199, 199) -def test_cross_scan_line(): +def test_cross_scan_line() -> None: with Image.open("Tests/images/cross_scan_line.tga") as im: assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png") @@ -102,7 +104,7 @@ def test_cross_scan_line(): im.load() -def test_save(tmp_path): +def test_save(tmp_path: Path) -> None: test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: out = str(tmp_path / "temp.tga") @@ -119,7 +121,7 @@ def test_save(tmp_path): assert test_im.size == (100, 100) -def test_small_palette(tmp_path): +def test_small_palette(tmp_path: Path) -> None: im = Image.new("P", (1, 1)) colors = [0, 0, 0] im.putpalette(colors) @@ -131,7 +133,7 @@ def test_small_palette(tmp_path): assert reloaded.getpalette() == colors -def test_save_wrong_mode(tmp_path): +def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper("PA") out = str(tmp_path / "temp.tga") @@ -139,7 +141,7 @@ def test_save_wrong_mode(tmp_path): im.save(out) -def test_save_mapdepth(): +def test_save_mapdepth() -> None: # This image has been manually hexedited from 200x32_p_bl_raw.tga # to include an origin test_file = "Tests/images/200x32_p_bl_raw_origin.tga" @@ -147,7 +149,7 @@ def test_save_mapdepth(): assert_image_equal_tofile(im, "Tests/images/tga/common/200x32_p.png") -def test_save_id_section(tmp_path): +def test_save_id_section(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: out = str(tmp_path / "temp.tga") @@ -178,7 +180,7 @@ def test_save_id_section(tmp_path): assert "id_section" not in test_im.info -def test_save_orientation(tmp_path): +def test_save_orientation(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" out = str(tmp_path / "temp.tga") with Image.open(test_file) as im: @@ -189,7 +191,7 @@ def test_save_orientation(tmp_path): assert test_im.info["orientation"] == 1 -def test_horizontal_orientations(): +def test_horizontal_orientations() -> None: # 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) @@ -198,7 +200,7 @@ def test_horizontal_orientations(): assert im.load()[90, 90][:3] == (0, 255, 0) -def test_save_rle(tmp_path): +def test_save_rle(tmp_path: Path) -> None: test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: assert im.info["compression"] == "tga_rle" @@ -231,7 +233,7 @@ def test_save_rle(tmp_path): assert test_im.info["compression"] == "tga_rle" -def test_save_l_transparency(tmp_path): +def test_save_l_transparency(tmp_path: Path) -> None: # There are 559 transparent pixels in la.tga. num_transparent = 559 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 0851796d0..0110948ae 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,7 +1,11 @@ from __future__ import annotations + import os import warnings from io import BytesIO +from pathlib import Path +from types import ModuleType +from typing import Generator import pytest @@ -18,6 +22,7 @@ from .helper import ( is_win32, ) +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: @@ -25,7 +30,7 @@ except ImportError: class TestFileTiff: - def test_sanity(self, tmp_path): + def test_sanity(self, tmp_path: Path) -> None: filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename) @@ -57,21 +62,21 @@ class TestFileTiff: pass @pytest.mark.skipif(is_pypy(), reason="Requires CPython") - def test_unclosed_file(self): - def open(): + def test_unclosed_file(self) -> None: + def open() -> None: im = Image.open("Tests/images/multipage.tiff") im.load() with pytest.warns(ResourceWarning): open() - def test_closed_file(self): + def test_closed_file(self) -> None: with warnings.catch_warnings(): im = Image.open("Tests/images/multipage.tiff") im.load() im.close() - def test_seek_after_close(self): + def test_seek_after_close(self) -> None: im = Image.open("Tests/images/multipage.tiff") im.close() @@ -80,12 +85,12 @@ class TestFileTiff: with pytest.raises(ValueError): im.seek(1) - def test_context_manager(self): + def test_context_manager(self) -> None: with warnings.catch_warnings(): with Image.open("Tests/images/multipage.tiff") as im: im.load() - def test_mac_tiff(self): + def test_mac_tiff(self) -> None: # Read RGBa images from macOS [@PIL136] filename = "Tests/images/pil136.tiff" @@ -97,7 +102,7 @@ class TestFileTiff: assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - def test_bigtiff(self, tmp_path): + def test_bigtiff(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert_image_equal_tofile(im, "Tests/images/hopper.tif") @@ -108,13 +113,13 @@ class TestFileTiff: outfile = str(tmp_path / "temp.tif") im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) - def test_set_legacy_api(self): + def test_set_legacy_api(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() with pytest.raises(Exception) as e: ifd.legacy_api = None assert str(e.value) == "Not allowing setting of legacy api" - def test_xyres_tiff(self): + def test_xyres_tiff(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: # legacy api @@ -127,7 +132,7 @@ class TestFileTiff: assert im.info["dpi"] == (72.0, 72.0) - def test_xyres_fallback_tiff(self): + def test_xyres_fallback_tiff(self) -> None: filename = "Tests/images/compression.tif" with Image.open(filename) as im: # v2 api @@ -141,7 +146,7 @@ class TestFileTiff: # Fallback "inch". assert im.info["dpi"] == (100.0, 100.0) - def test_int_resolution(self): + def test_int_resolution(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: # Try to read a file where X,Y_RESOLUTION are ints @@ -154,14 +159,14 @@ class TestFileTiff: "resolution_unit, dpi", [(None, 72.8), (2, 72.8), (3, 184.912)], ) - def test_load_float_dpi(self, resolution_unit, dpi): + def test_load_float_dpi(self, resolution_unit: int | None, dpi: float) -> None: with Image.open( "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit assert im.info["dpi"] == (dpi, dpi) - def test_save_float_dpi(self, tmp_path): + def test_save_float_dpi(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: dpi = (72.2, 72.2) @@ -170,7 +175,7 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert reloaded.info["dpi"] == dpi - def test_save_setting_missing_resolution(self): + def test_save_setting_missing_resolution(self) -> None: b = BytesIO() with Image.open("Tests/images/10ct_32bit_128.tiff") as im: im.save(b, format="tiff", resolution=123.45) @@ -178,7 +183,7 @@ class TestFileTiff: assert im.tag_v2[X_RESOLUTION] == 123.45 assert im.tag_v2[Y_RESOLUTION] == 123.45 - def test_invalid_file(self): + def test_invalid_file(self) -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): @@ -189,30 +194,30 @@ class TestFileTiff: TiffImagePlugin.TiffImageFile(invalid_file) TiffImagePlugin.PREFIXES.pop() - def test_bad_exif(self): + def test_bad_exif(self) -> None: with Image.open("Tests/images/hopper_bad_exif.jpg") as i: # Should not raise struct.error. with pytest.warns(UserWarning): i._getexif() - def test_save_rgba(self, tmp_path): + def test_save_rgba(self, tmp_path: Path) -> None: im = hopper("RGBA") outfile = str(tmp_path / "temp.tif") im.save(outfile) - def test_save_unsupported_mode(self, tmp_path): + def test_save_unsupported_mode(self, tmp_path: Path) -> None: im = hopper("HSV") outfile = str(tmp_path / "temp.tif") with pytest.raises(OSError): im.save(outfile) - def test_8bit_s(self): + def test_8bit_s(self) -> None: with Image.open("Tests/images/8bit.s.tif") as im: im.load() assert im.mode == "L" assert im.getpixel((50, 50)) == 184 - def test_little_endian(self): + def test_little_endian(self) -> None: with Image.open("Tests/images/16bit.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16" @@ -222,7 +227,7 @@ class TestFileTiff: assert b[0] == ord(b"\xe0") assert b[1] == ord(b"\x01") - def test_big_endian(self): + def test_big_endian(self) -> None: with Image.open("Tests/images/16bit.MM.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16B" @@ -232,7 +237,7 @@ class TestFileTiff: assert b[0] == ord(b"\x01") assert b[1] == ord(b"\xe0") - def test_16bit_r(self): + def test_16bit_r(self) -> None: with Image.open("Tests/images/16bit.r.tif") as im: assert im.getpixel((0, 0)) == 480 assert im.mode == "I;16" @@ -241,14 +246,14 @@ class TestFileTiff: assert b[0] == ord(b"\xe0") assert b[1] == ord(b"\x01") - def test_16bit_s(self): + def test_16bit_s(self) -> None: with Image.open("Tests/images/16bit.s.tif") as im: im.load() assert im.mode == "I" assert im.getpixel((0, 0)) == 32767 assert im.getpixel((0, 1)) == 0 - def test_12bit_rawmode(self): + def test_12bit_rawmode(self) -> None: """Are we generating the same interpretation of the image as Imagemagick is?""" @@ -261,7 +266,7 @@ class TestFileTiff: assert_image_equal_tofile(im, "Tests/images/12in16bit.tif") - def test_32bit_float(self): + def test_32bit_float(self) -> None: # Issue 614, specific 32-bit float format path = "Tests/images/10ct_32bit_128.tiff" with Image.open(path) as im: @@ -270,7 +275,7 @@ class TestFileTiff: assert im.getpixel((0, 0)) == -0.4526388943195343 assert im.getextrema() == (-3.140936851501465, 3.140684127807617) - def test_unknown_pixel_mode(self): + def test_unknown_pixel_mode(self) -> None: with pytest.raises(OSError): with Image.open("Tests/images/hopper_unknown_pixel_mode.tif"): pass @@ -282,12 +287,12 @@ class TestFileTiff: ("Tests/images/multipage.tiff", 3), ), ) - def test_n_frames(self, path, n_frames): + def test_n_frames(self, path: str, n_frames: int) -> None: with Image.open(path) as im: assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) - def test_eoferror(self): + def test_eoferror(self) -> None: with Image.open("Tests/images/multipage-lastframe.tif") as im: n_frames = im.n_frames @@ -299,7 +304,7 @@ class TestFileTiff: # Test that seeking to the last frame does not raise an error im.seek(n_frames - 1) - def test_multipage(self): + def test_multipage(self) -> None: # issue #862 with Image.open("Tests/images/multipage.tiff") as im: # file is a multipage tiff: 10x10 green, 10x10 red, 20x20 blue @@ -323,13 +328,13 @@ class TestFileTiff: assert im.size == (20, 20) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) - def test_multipage_last_frame(self): + def test_multipage_last_frame(self) -> None: with Image.open("Tests/images/multipage-lastframe.tif") as im: im.load() assert im.size == (20, 20) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) - def test_frame_order(self): + def test_frame_order(self) -> None: # A frame can't progress to itself after reading with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: assert im.n_frames == 1 @@ -342,7 +347,7 @@ class TestFileTiff: with Image.open("Tests/images/multipage_out_of_order.tiff") as im: assert im.n_frames == 3 - def test___str__(self): + def test___str__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: # Act @@ -351,7 +356,7 @@ class TestFileTiff: # Assert assert isinstance(ret, str) - def test_dict(self): + def test_dict(self) -> None: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: @@ -391,7 +396,7 @@ class TestFileTiff: } assert dict(im.tag) == legacy_tags - def test__delitem__(self): + def test__delitem__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: len_before = len(dict(im.ifd)) @@ -400,36 +405,36 @@ class TestFileTiff: assert len_before == len_after + 1 @pytest.mark.parametrize("legacy_api", (False, True)) - def test_load_byte(self, legacy_api): + def test_load_byte(self, legacy_api: bool) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc" ret = ifd.load_byte(data, legacy_api) assert ret == b"abc" - def test_load_string(self): + def test_load_string(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc\0" ret = ifd.load_string(data, False) assert ret == "abc" - def test_load_float(self): + def test_load_float(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdabcd" ret = ifd.load_float(data, False) assert ret == (1.6777999408082104e22, 1.6777999408082104e22) - def test_load_double(self): + def test_load_double(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdefghabcdefgh" ret = ifd.load_double(data, False) assert ret == (8.540883223036124e194, 8.540883223036124e194) - def test_ifd_tag_type(self): + def test_ifd_tag_type(self) -> None: with Image.open("Tests/images/ifd_tag_type.tiff") as im: assert 0x8825 in im.tag_v2 - def test_exif(self, tmp_path): - def check_exif(exif): + def test_exif(self, tmp_path: Path) -> None: + def check_exif(exif: Image.Exif) -> None: assert sorted(exif.keys()) == [ 256, 257, @@ -480,19 +485,19 @@ class TestFileTiff: exif = im.getexif() check_exif(exif) - def test_modify_exif(self, tmp_path): + def test_modify_exif(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/ifd_tag_type.tiff") as im: exif = im.getexif() - exif[256] = 100 + exif[264] = 100 im.save(outfile, exif=exif) with Image.open(outfile) as im: exif = im.getexif() - assert exif[256] == 100 + assert exif[264] == 100 - def test_reload_exif_after_seek(self): + def test_reload_exif_after_seek(self) -> None: with Image.open("Tests/images/multipage.tiff") as im: exif = im.getexif() del exif[256] @@ -500,7 +505,7 @@ class TestFileTiff: assert 256 in exif - def test_exif_frames(self): + def test_exif_frames(self) -> None: # Test that EXIF data can change across frames with Image.open("Tests/images/g4-multi.tiff") as im: assert im.getexif()[273] == (328, 815) @@ -509,7 +514,7 @@ class TestFileTiff: assert im.getexif()[273] == (1408, 1907) @pytest.mark.parametrize("mode", ("1", "L")) - def test_photometric(self, mode, tmp_path): + def test_photometric(self, mode: str, tmp_path: Path) -> None: filename = str(tmp_path / "temp.tif") im = hopper(mode) im.save(filename, tiffinfo={262: 0}) @@ -517,13 +522,13 @@ class TestFileTiff: assert reloaded.tag_v2[262] == 0 assert_image_equal(im, reloaded) - def test_seek(self): + def test_seek(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: im.seek(0) assert im.tell() == 0 - def test_seek_eof(self): + def test_seek_eof(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: assert im.tell() == 0 @@ -532,21 +537,21 @@ class TestFileTiff: with pytest.raises(EOFError): im.seek(1) - def test__limit_rational_int(self): + def test__limit_rational_int(self) -> None: from PIL.TiffImagePlugin import _limit_rational value = 34 ret = _limit_rational(value, 65536) assert ret == (34, 1) - def test__limit_rational_float(self): + def test__limit_rational_float(self) -> None: from PIL.TiffImagePlugin import _limit_rational value = 22.3 ret = _limit_rational(value, 65536) assert ret == (223, 10) - def test_4bit(self): + def test_4bit(self) -> None: test_file = "Tests/images/hopper_gray_4bpp.tif" original = hopper("L") with Image.open(test_file) as im: @@ -554,7 +559,7 @@ class TestFileTiff: assert im.mode == "L" assert_image_similar(im, original, 7.3) - def test_gray_semibyte_per_pixel(self): + def test_gray_semibyte_per_pixel(self) -> None: test_files = ( ( 24.8, # epsilon @@ -587,7 +592,7 @@ class TestFileTiff: assert im2.mode == "L" assert_image_equal(im, im2) - def test_with_underscores(self, tmp_path): + def test_with_underscores(self, tmp_path: Path) -> None: kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename, **kwargs) @@ -600,7 +605,7 @@ class TestFileTiff: assert im.tag_v2[X_RESOLUTION] == 72 assert im.tag_v2[Y_RESOLUTION] == 36 - def test_roundtrip_tiff_uint16(self, tmp_path): + def test_roundtrip_tiff_uint16(self, tmp_path: Path) -> None: # Test an image of all '0' values pixel_value = 0x1234 infile = "Tests/images/uint16_1_4660.tif" @@ -612,25 +617,33 @@ class TestFileTiff: assert_image_equal_tofile(im, tmpfile) - def test_strip_raw(self): + def test_rowsperstrip(self, tmp_path: Path) -> None: + outfile = str(tmp_path / "temp.tif") + im = hopper() + im.save(outfile, tiffinfo={278: 256}) + + with Image.open(outfile) as im: + assert im.tag_v2[278] == 256 + + def test_strip_raw(self) -> None: infile = "Tests/images/tiff_strip_raw.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_strip_planar_raw(self): + def test_strip_planar_raw(self) -> None: # gdal_translate -of GTiff -co INTERLEAVE=BAND \ # tiff_strip_raw.tif tiff_strip_planar_raw.tiff infile = "Tests/images/tiff_strip_planar_raw.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_strip_planar_raw_with_overviews(self): + def test_strip_planar_raw_with_overviews(self) -> None: # gdaladdo tiff_strip_planar_raw2.tif 2 4 8 16 infile = "Tests/images/tiff_strip_planar_raw_with_overviews.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") - def test_tiled_planar_raw(self): + def test_tiled_planar_raw(self) -> None: # gdal_translate -of GTiff -co TILED=YES -co BLOCKXSIZE=32 \ # -co BLOCKYSIZE=32 -co INTERLEAVE=BAND \ # tiff_tiled_raw.tif tiff_tiled_planar_raw.tiff @@ -638,7 +651,7 @@ 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): + def test_planar_configuration_save(self, tmp_path: Path) -> None: infile = "Tests/images/tiff_tiled_planar_raw.tif" with Image.open(infile) as im: assert im._planar_configuration == 2 @@ -650,7 +663,7 @@ class TestFileTiff: assert_image_equal_tofile(reloaded, infile) @pytest.mark.parametrize("mode", ("P", "PA")) - def test_palette(self, mode, tmp_path): + def test_palette(self, mode: str, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") im = hopper(mode) @@ -659,7 +672,7 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) - def test_tiff_save_all(self): + def test_tiff_save_all(self) -> None: mp = BytesIO() with Image.open("Tests/images/multipage.tiff") as im: im.save(mp, format="tiff", save_all=True) @@ -679,7 +692,7 @@ class TestFileTiff: assert reread.n_frames == 3 # Test appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims mp = BytesIO() @@ -689,7 +702,7 @@ class TestFileTiff: with Image.open(mp) as reread: assert reread.n_frames == 3 - def test_saving_icc_profile(self, tmp_path): + def test_saving_icc_profile(self, tmp_path: Path) -> None: # Tests saving TIFF with icc_profile set. # At the time of writing this will only work for non-compressed tiffs # as libtiff does not support embedded ICC profiles, @@ -703,7 +716,7 @@ class TestFileTiff: with Image.open(tmpfile) as reloaded: assert b"Dummy value" == reloaded.info["icc_profile"] - def test_save_icc_profile(self, tmp_path): + def test_save_icc_profile(self, tmp_path: Path) -> None: im = hopper() assert "icc_profile" not in im.info @@ -714,14 +727,14 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert reloaded.info["icc_profile"] == icc_profile - def test_save_bmp_compression(self, tmp_path): + def test_save_bmp_compression(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.bmp") as im: assert im.info["compression"] == 0 outfile = str(tmp_path / "temp.tif") im.save(outfile) - def test_discard_icc_profile(self, tmp_path): + def test_discard_icc_profile(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/icc_profile.png") as im: @@ -732,7 +745,7 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert "icc_profile" not in reloaded.info - def test_getxmp(self): + def test_getxmp(self) -> None: with Image.open("Tests/images/lab.tif") as im: if ElementTree is None: with pytest.warns( @@ -747,7 +760,7 @@ class TestFileTiff: assert description[0]["format"] == "image/tiff" assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] - def test_get_photoshop_blocks(self): + def test_get_photoshop_blocks(self) -> None: with Image.open("Tests/images/lab.tif") as im: assert list(im.get_photoshop_blocks().keys()) == [ 1061, @@ -773,7 +786,28 @@ class TestFileTiff: 4001, ] - def test_close_on_load_exclusive(self, tmp_path): + def test_tiff_chunks(self, tmp_path: Path) -> None: + tmpfile = str(tmp_path / "temp.tif") + + im = hopper() + with open(tmpfile, "wb") as fp: + for y in range(0, 128, 32): + chunk = im.crop((0, y, 128, y + 32)) + if y == 0: + chunk.save( + fp, + "TIFF", + tiffinfo={ + TiffImagePlugin.IMAGEWIDTH: 128, + TiffImagePlugin.IMAGELENGTH: 128, + }, + ) + else: + fp.write(chunk.tobytes()) + + assert_image_equal_tofile(im, tmpfile) + + def test_close_on_load_exclusive(self, tmp_path: Path) -> None: # similar to test_fd_leak, but runs on unixlike os tmpfile = str(tmp_path / "temp.tif") @@ -786,7 +820,7 @@ class TestFileTiff: im.load() assert fp.closed - def test_close_on_load_nonexclusive(self, tmp_path): + def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/uint16_1_4660.tif") as im: @@ -808,7 +842,7 @@ class TestFileTiff: not os.path.exists("Tests/images/string_dimension.tiff"), reason="Extra image files not installed", ) - def test_string_dimension(self): + def test_string_dimension(self) -> None: # Assert that an error is raised if one of the dimensions is a string with Image.open("Tests/images/string_dimension.tiff") as im: with pytest.raises(OSError): @@ -816,7 +850,7 @@ class TestFileTiff: @pytest.mark.timeout(6) @pytest.mark.filterwarnings("ignore:Truncated File Read") - def test_timeout(self): + def test_timeout(self) -> None: with Image.open("Tests/images/timeout-6646305047838720") as im: ImageFile.LOAD_TRUNCATED_IMAGES = True im.load() @@ -829,7 +863,7 @@ class TestFileTiff: ], ) @pytest.mark.timeout(2) - def test_oom(self, test_file): + def test_oom(self, test_file: str) -> None: with pytest.raises(UnidentifiedImageError): with pytest.warns(UserWarning): with Image.open(test_file): @@ -838,7 +872,7 @@ class TestFileTiff: @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestFileTiffW32: - def test_fd_leak(self, tmp_path): + def test_fd_leak(self, tmp_path: Path) -> None: tmpfile = str(tmp_path / "temp.tif") # this is an mmaped file. diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index edd57e6b5..d7a18c725 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,6 +1,8 @@ from __future__ import annotations + import io import struct +from pathlib import Path import pytest @@ -12,7 +14,7 @@ from .helper import assert_deep_equal, hopper TAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()} -def test_rt_metadata(tmp_path): +def test_rt_metadata(tmp_path: Path) -> None: """Test writing arbitrary metadata into the tiff image directory Use case is ImageJ private tags, one numeric, one arbitrary data. https://github.com/python-pillow/Pillow/issues/291 @@ -78,7 +80,7 @@ def test_rt_metadata(tmp_path): assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) -def test_read_metadata(): +def test_read_metadata() -> None: with Image.open("Tests/images/hopper_g4.tif") as img: assert { "YResolution": IFDRational(4294967295, 113653537), @@ -119,10 +121,11 @@ def test_read_metadata(): } == img.tag.named() -def test_write_metadata(tmp_path): +def test_write_metadata(tmp_path: Path) -> None: """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: f = str(tmp_path / "temp.tiff") + del img.tag[278] img.save(f, tiffinfo=img.tag) original = img.tag_v2.named() @@ -155,13 +158,15 @@ def test_write_metadata(tmp_path): assert value == reloaded[tag], f"{tag} didn't roundtrip" -def test_change_stripbytecounts_tag_type(tmp_path): +def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.tif") as im: info = im.tag_v2 + del info[278] # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT im = im.resize((500, 500)) + info[TiffImagePlugin.IMAGEWIDTH] = im.width # STRIPBYTECOUNTS can be a SHORT or a LONG info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT @@ -172,19 +177,21 @@ def test_change_stripbytecounts_tag_type(tmp_path): assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG -def test_no_duplicate_50741_tag(): +def test_no_duplicate_50741_tag() -> None: assert TAG_IDS["MakerNoteSafety"] == 50741 assert TAG_IDS["BestQualityScale"] == 50780 -def test_iptc(tmp_path): +def test_iptc(tmp_path: Path) -> None: out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.Lab.tif") as im: im.save(out) @pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) -def test_writing_other_types_to_ascii(value, expected, tmp_path): +def test_writing_other_types_to_ascii( + value: bytes | int, expected: str, tmp_path: Path +) -> None: info = TiffImagePlugin.ImageFileDirectory_v2() tag = TiffTags.TAGS_V2[271] @@ -201,7 +208,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path): @pytest.mark.parametrize("value", (1, IFDRational(1))) -def test_writing_other_types_to_bytes(value, tmp_path): +def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -217,7 +224,7 @@ def test_writing_other_types_to_bytes(value, tmp_path): assert reloaded.tag_v2[700] == b"\x01" -def test_writing_other_types_to_undefined(tmp_path): +def test_writing_other_types_to_undefined(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -233,7 +240,7 @@ def test_writing_other_types_to_undefined(tmp_path): assert reloaded.tag_v2[33723] == b"1" -def test_undefined_zero(tmp_path): +def test_undefined_zero(tmp_path: Path) -> None: # Check that the tag has not been changed since this test was created tag = TiffTags.TAGS_V2[45059] assert tag.type == TiffTags.UNDEFINED @@ -248,7 +255,7 @@ def test_undefined_zero(tmp_path): assert info[45059] == original -def test_empty_metadata(): +def test_empty_metadata() -> None: f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) @@ -257,7 +264,7 @@ def test_empty_metadata(): info.load(f) -def test_iccprofile(tmp_path): +def test_iccprofile(tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/1462 out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.iccprofile.tif") as im: @@ -268,7 +275,7 @@ def test_iccprofile(tmp_path): assert im.info["icc_profile"] == reloaded.info["icc_profile"] -def test_iccprofile_binary(): +def test_iccprofile_binary() -> None: # https://github.com/python-pillow/Pillow/issues/1526 # We should be able to load this, # but probably won't be able to save it. @@ -278,19 +285,19 @@ def test_iccprofile_binary(): assert im.info["icc_profile"] -def test_iccprofile_save_png(tmp_path): +def test_iccprofile_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile.tif") as im: outfile = str(tmp_path / "temp.png") im.save(outfile) -def test_iccprofile_binary_save_png(tmp_path): +def test_iccprofile_binary_save_png(tmp_path: Path) -> None: with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: outfile = str(tmp_path / "temp.png") im.save(outfile) -def test_exif_div_zero(tmp_path): +def test_exif_div_zero(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() info[41988] = TiffImagePlugin.IFDRational(0, 0) @@ -303,7 +310,7 @@ def test_exif_div_zero(tmp_path): assert 0 == reloaded.tag_v2[41988].denominator -def test_ifd_unsigned_rational(tmp_path): +def test_ifd_unsigned_rational(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -334,7 +341,7 @@ def test_ifd_unsigned_rational(tmp_path): assert 1 == reloaded.tag_v2[41493].denominator -def test_ifd_signed_rational(tmp_path): +def test_ifd_signed_rational(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -377,7 +384,7 @@ def test_ifd_signed_rational(tmp_path): assert -1 == reloaded.tag_v2[37380].denominator -def test_ifd_signed_long(tmp_path): +def test_ifd_signed_long(tmp_path: Path) -> None: im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() @@ -390,7 +397,7 @@ def test_ifd_signed_long(tmp_path): assert reloaded.tag_v2[37000] == -60000 -def test_empty_values(): +def test_empty_values() -> None: data = io.BytesIO( b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00" @@ -405,7 +412,7 @@ def test_empty_values(): assert 33432 in info -def test_photoshop_info(tmp_path): +def test_photoshop_info(tmp_path: Path) -> None: with Image.open("Tests/images/issue_2278.tif") as im: assert len(im.tag_v2[34377]) == 70 assert isinstance(im.tag_v2[34377], bytes) @@ -416,7 +423,7 @@ def test_photoshop_info(tmp_path): assert isinstance(reloaded.tag_v2[34377], bytes) -def test_too_many_entries(): +def test_too_many_entries() -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() # 277: ("SamplesPerPixel", SHORT, 1), @@ -428,7 +435,7 @@ def test_too_many_entries(): assert ifd[277] == 4 -def test_tag_group_data(): +def test_tag_group_data() -> None: base_ifd = TiffImagePlugin.ImageFileDirectory_v2() interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965) for ifd in (base_ifd, interop_ifd): @@ -442,7 +449,7 @@ def test_tag_group_data(): assert base_ifd.tagtype[2] != interop_ifd.tagtype[256] -def test_empty_subifd(tmp_path): +def test_empty_subifd(tmp_path: Path) -> None: out = str(tmp_path / "temp.jpg") im = hopper() diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index 0b84d0320..b34975e83 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import WalImageFile from .helper import assert_image_equal_tofile @@ -6,7 +7,7 @@ from .helper import assert_image_equal_tofile TEST_FILE = "Tests/images/hopper.wal" -def test_open(): +def test_open() -> None: with WalImageFile.open(TEST_FILE) as im: assert im.format == "WAL" assert im.format_description == "Quake2 Texture" @@ -18,7 +19,7 @@ def test_open(): assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") -def test_load(): +def test_load() -> None: with WalImageFile.open(TEST_FILE) as im: assert im.load()[0, 0] == 122 diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index c91818ef6..249846da4 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,8 +1,10 @@ from __future__ import annotations + import io import re import sys import warnings +from pathlib import Path import pytest @@ -25,7 +27,7 @@ except ImportError: class TestUnsupportedWebp: - def test_unsupported(self): + def test_unsupported(self) -> None: if HAVE_WEBP: WebPImagePlugin.SUPPORTED = False @@ -41,15 +43,15 @@ class TestUnsupportedWebp: @skip_unless_feature("webp") class TestFileWebp: - def setup_method(self): + def setup_method(self) -> None: self.rgb_mode = "RGB" - def test_version(self): + def test_version(self) -> None: _webp.WebPDecoderVersion() _webp.WebPDecoderBuggyAlpha() assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp")) - def test_read_rgb(self): + def test_read_rgb(self) -> None: """ Can we read a RGB mode WebP file without error? Does it have the bits we expect? @@ -66,7 +68,7 @@ class TestFileWebp: # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0) - def _roundtrip(self, tmp_path, mode, epsilon, args={}): + def _roundtrip(self, tmp_path: Path, mode, epsilon, args={}) -> None: temp_file = str(tmp_path / "temp.webp") hopper(mode).save(temp_file, **args) @@ -92,7 +94,7 @@ class TestFileWebp: target = target.convert(self.rgb_mode) assert_image_similar(image, target, epsilon) - def test_write_rgb(self, tmp_path): + def test_write_rgb(self, tmp_path: Path) -> None: """ Can we write a RGB mode file to webp without error? Does it have the bits we expect? @@ -100,7 +102,7 @@ class TestFileWebp: self._roundtrip(tmp_path, self.rgb_mode, 12.5) - def test_write_method(self, tmp_path): + def test_write_method(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.0, {"method": 6}) buffer_no_args = io.BytesIO() @@ -111,7 +113,7 @@ class TestFileWebp: assert buffer_no_args.getbuffer() != buffer_method.getbuffer() @skip_unless_feature("webp_anim") - def test_save_all(self, tmp_path): + def test_save_all(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (1, 1)) im2 = Image.new("RGB", (1, 1), "#f00") @@ -123,14 +125,14 @@ class TestFileWebp: reloaded.seek(1) assert_image_similar(im2, reloaded, 1) - def test_icc_profile(self, tmp_path): + def test_icc_profile(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) if _webp.HAVE_WEBPANIM: self._roundtrip( tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} ) - def test_write_unsupported_mode_L(self, tmp_path): + def test_write_unsupported_mode_L(self, tmp_path: Path) -> None: """ Saving a black-and-white file to WebP format should work, and be similar to the original file. @@ -138,7 +140,7 @@ class TestFileWebp: self._roundtrip(tmp_path, "L", 10.0) - def test_write_unsupported_mode_P(self, tmp_path): + def test_write_unsupported_mode_P(self, tmp_path: Path) -> None: """ Saving a palette-based file to WebP format should work, and be similar to the original file. @@ -147,14 +149,14 @@ class TestFileWebp: self._roundtrip(tmp_path, "P", 50.0) @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") - def test_write_encoding_error_message(self, tmp_path): + def test_write_encoding_error_message(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) with pytest.raises(ValueError) as e: im.save(temp_file, method=0) assert str(e.value) == "encoding error 6" - def test_WebPEncode_with_invalid_args(self): + def test_WebPEncode_with_invalid_args(self) -> None: """ Calling encoder functions with no arguments should result in an error. """ @@ -165,7 +167,7 @@ class TestFileWebp: with pytest.raises(TypeError): _webp.WebPEncode() - def test_WebPDecode_with_invalid_args(self): + def test_WebPDecode_with_invalid_args(self) -> None: """ Calling decoder functions with no arguments should result in an error. """ @@ -176,14 +178,14 @@ class TestFileWebp: with pytest.raises(TypeError): _webp.WebPDecode() - def test_no_resource_warning(self, tmp_path): + def test_no_resource_warning(self, tmp_path: Path) -> None: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: temp_file = str(tmp_path / "temp.webp") with warnings.catch_warnings(): image.save(temp_file) - def test_file_pointer_could_be_reused(self): + def test_file_pointer_could_be_reused(self) -> None: file_path = "Tests/images/hopper.webp" with open(file_path, "rb") as blob: Image.open(blob).load() @@ -194,14 +196,14 @@ class TestFileWebp: (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), ) @skip_unless_feature("webp_anim") - def test_invalid_background(self, background, tmp_path): + def test_invalid_background(self, background, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = hopper() with pytest.raises(OSError): im.save(temp_file, save_all=True, append_images=[im], background=background) @skip_unless_feature("webp_anim") - def test_background_from_gif(self, tmp_path): + def test_background_from_gif(self, tmp_path: Path) -> None: # 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") @@ -226,7 +228,7 @@ class TestFileWebp: assert difference < 5 @skip_unless_feature("webp_anim") - def test_duration(self, tmp_path): + def test_duration(self, tmp_path: Path) -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: assert im.info["duration"] == 1000 @@ -237,7 +239,7 @@ class TestFileWebp: reloaded.load() assert reloaded.info["duration"] == 1000 - def test_roundtrip_rgba_palette(self, tmp_path): + def test_roundtrip_rgba_palette(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGBA", (1, 1)).convert("P") assert im.mode == "P" diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 79d01a444..a95434624 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image @@ -13,12 +16,12 @@ from .helper import ( _webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") -def setup_module(): +def setup_module() -> None: if _webp.WebPDecoderBuggyAlpha(): pytest.skip("Buggy early version of WebP installed, not testing transparency") -def test_read_rgba(): +def test_read_rgba() -> None: """ Can we read an RGBA mode file without error? Does it have the bits we expect? @@ -38,7 +41,7 @@ def test_read_rgba(): assert_image_similar_tofile(image, "Tests/images/transparent.png", 20.0) -def test_write_lossless_rgb(tmp_path): +def test_write_lossless_rgb(tmp_path: Path) -> None: """ Can we write an RGBA mode file with lossless compression without error? Does it have the bits we expect? @@ -67,7 +70,7 @@ def test_write_lossless_rgb(tmp_path): assert_image_equal(image, pil_image) -def test_write_rgba(tmp_path): +def test_write_rgba(tmp_path: Path) -> None: """ Can we write a RGBA mode file to WebP without error. Does it have the bits we expect? @@ -98,7 +101,7 @@ def test_write_rgba(tmp_path): assert_image_similar(image, pil_image, 1.0) -def test_keep_rgb_values_when_transparent(tmp_path): +def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: """ Saving transparent pixels should retain their original RGB values when using the "exact" parameter. @@ -127,7 +130,7 @@ def test_keep_rgb_values_when_transparent(tmp_path): assert_image_equal(reloaded.convert("RGB"), image) -def test_write_unsupported_mode_PA(tmp_path): +def test_write_unsupported_mode_PA(tmp_path: Path) -> None: """ Saving a palette-based file with transparency to WebP format should work, and be similar to the original file. diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 22acb4be6..9a730f1f9 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from packaging.version import parse as parse_version @@ -17,7 +20,7 @@ pytestmark = [ ] -def test_n_frames(): +def test_n_frames() -> None: """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" with Image.open("Tests/images/hopper.webp") as im: @@ -29,7 +32,7 @@ def test_n_frames(): assert im.is_animated -def test_write_animation_L(tmp_path): +def test_write_animation_L(tmp_path: Path) -> None: """ Convert an animated GIF to animated WebP, then compare the frame count, and first and last frames to ensure they're visually similar. @@ -59,13 +62,13 @@ def test_write_animation_L(tmp_path): assert_image_similar(im, orig.convert("RGBA"), 32.9) -def test_write_animation_RGB(tmp_path): +def test_write_animation_RGB(tmp_path: Path) -> None: """ Write an animated WebP from RGB frames, and ensure the frames are visually similar to the originals. """ - def check(temp_file): + def check(temp_file) -> None: with Image.open(temp_file) as im: assert im.n_frames == 2 @@ -104,7 +107,7 @@ def test_write_animation_RGB(tmp_path): check(temp_file2) -def test_timestamp_and_duration(tmp_path): +def test_timestamp_and_duration(tmp_path: Path) -> None: """ Try passing a list of durations, and make sure the encoded timestamps and durations are correct. @@ -135,7 +138,7 @@ def test_timestamp_and_duration(tmp_path): ts += durations[frame] -def test_float_duration(tmp_path): +def test_float_duration(tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") with Image.open("Tests/images/iss634.apng") as im: assert im.info["duration"] == 70.0 @@ -147,7 +150,7 @@ def test_float_duration(tmp_path): assert reloaded.info["duration"] == 70 -def test_seeking(tmp_path): +def test_seeking(tmp_path: Path) -> None: """ Create an animated WebP file, and then try seeking through frames in reverse-order, verifying the timestamps and durations are correct. @@ -178,7 +181,7 @@ def test_seeking(tmp_path): ts -= dur -def test_seek_errors(): +def test_seek_errors() -> None: with Image.open("Tests/images/iss634.webp") as im: with pytest.raises(EOFError): im.seek(-1) diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 6acf58ac3..32e29de56 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image @@ -9,7 +12,7 @@ _webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") RGB_MODE = "RGB" -def test_write_lossless_rgb(tmp_path): +def test_write_lossless_rgb(tmp_path: Path) -> None: if _webp.WebPDecoderVersion() < 0x0200: pytest.skip("lossless not included") diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index a7b7bbcf6..875941240 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,5 +1,8 @@ from __future__ import annotations + from io import BytesIO +from pathlib import Path +from types import ModuleType import pytest @@ -12,13 +15,14 @@ pytestmark = [ skip_unless_feature("webp_mux"), ] +ElementTree: ModuleType | None try: from defusedxml import ElementTree except ImportError: ElementTree = None -def test_read_exif_metadata(): +def test_read_exif_metadata() -> None: file_path = "Tests/images/flower.webp" with Image.open(file_path) as image: assert image.format == "WEBP" @@ -36,7 +40,7 @@ def test_read_exif_metadata(): assert exif_data == expected_exif -def test_read_exif_metadata_without_prefix(): +def test_read_exif_metadata_without_prefix() -> None: with Image.open("Tests/images/flower2.webp") as im: # Assert prefix is not present assert im.info["exif"][:6] != b"Exif\x00\x00" @@ -48,7 +52,7 @@ def test_read_exif_metadata_without_prefix(): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_write_exif_metadata(): +def test_write_exif_metadata() -> None: file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() with Image.open(file_path) as image: @@ -62,7 +66,7 @@ def test_write_exif_metadata(): assert webp_exif == expected_exif[6:], "WebP EXIF didn't match" -def test_read_icc_profile(): +def test_read_icc_profile() -> None: file_path = "Tests/images/flower2.webp" with Image.open(file_path) as image: assert image.format == "WEBP" @@ -79,7 +83,7 @@ def test_read_icc_profile(): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_write_icc_metadata(): +def test_write_icc_metadata() -> None: file_path = "Tests/images/flower2.jpg" test_buffer = BytesIO() with Image.open(file_path) as image: @@ -99,7 +103,7 @@ def test_write_icc_metadata(): @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) -def test_read_no_exif(): +def test_read_no_exif() -> None: file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() with Image.open(file_path) as image: @@ -112,7 +116,7 @@ def test_read_no_exif(): assert not webp_image._getexif() -def test_getxmp(): +def test_getxmp() -> None: with Image.open("Tests/images/flower.webp") as im: assert "xmp" not in im.info assert im.getxmp() == {} @@ -132,7 +136,7 @@ def test_getxmp(): @skip_unless_feature("webp_anim") -def test_write_animated_metadata(tmp_path): +def test_write_animated_metadata(tmp_path: Path) -> None: iccp_data = b"" exif_data = b"" xmp_data = b"" diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 596dc8ba1..b43e3f296 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image, WmfImagePlugin @@ -6,7 +9,7 @@ from PIL import Image, WmfImagePlugin from .helper import assert_image_similar_tofile, hopper -def test_load_raw(): +def test_load_raw() -> None: # Test basic EMF open and rendering with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): @@ -24,17 +27,17 @@ def test_load_raw(): assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref.png", 2.0) -def test_load(): +def test_load() -> None: 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): +def test_register_handler(tmp_path: Path) -> None: class TestHandler: methodCalled = False - def save(self, im, fp, filename): + def save(self, im, fp, filename) -> None: self.methodCalled = True handler = TestHandler() @@ -50,12 +53,12 @@ def test_register_handler(tmp_path): WmfImagePlugin.register_handler(original_handler) -def test_load_float_dpi(): +def test_load_float_dpi() -> None: with Image.open("Tests/images/drawing.emf") as im: assert im.info["dpi"] == 1423.7668161434979 -def test_load_set_dpi(): +def test_load_set_dpi() -> None: with Image.open("Tests/images/drawing.wmf") as im: assert im.size == (82, 82) @@ -67,7 +70,7 @@ def test_load_set_dpi(): @pytest.mark.parametrize("ext", (".wmf", ".emf")) -def test_save(ext, tmp_path): +def test_save(ext, tmp_path: Path) -> None: im = hopper() tmpfile = str(tmp_path / ("temp" + ext)) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index b086ffd68..44dd2541f 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,5 +1,7 @@ from __future__ import annotations + from io import BytesIO +from pathlib import Path import pytest @@ -31,14 +33,14 @@ static char basic_bits[] = { """ -def test_pil151(): +def test_pil151() -> None: with Image.open(BytesIO(PIL151)) as im: im.load() assert im.mode == "1" assert im.size == (32, 32) -def test_open(): +def test_open() -> None: # Arrange # Created with `convert hopper.png hopper.xbm` filename = "Tests/images/hopper.xbm" @@ -50,7 +52,7 @@ def test_open(): assert im.size == (128, 128) -def test_open_filename_with_underscore(): +def test_open_filename_with_underscore() -> None: # Arrange # Created with `convert hopper.png hopper_underscore.xbm` filename = "Tests/images/hopper_underscore.xbm" @@ -62,14 +64,14 @@ def test_open_filename_with_underscore(): assert im.size == (128, 128) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): XbmImagePlugin.XbmImageFile(invalid_file) -def test_save_wrong_mode(tmp_path): +def test_save_wrong_mode(tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.xbm") @@ -77,7 +79,7 @@ def test_save_wrong_mode(tmp_path): im.save(out) -def test_hotspot(tmp_path): +def test_hotspot(tmp_path: Path) -> None: im = hopper("1") out = str(tmp_path / "temp.xbm") diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 265feab42..26afe93f4 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, XpmImagePlugin @@ -8,7 +9,7 @@ from .helper import assert_image_similar, hopper TEST_FILE = "Tests/images/hopper.xpm" -def test_sanity(): +def test_sanity() -> None: with Image.open(TEST_FILE) as im: im.load() assert im.mode == "P" @@ -19,14 +20,14 @@ def test_sanity(): assert_image_similar(im.convert("RGB"), hopper("RGB"), 60) -def test_invalid_file(): +def test_invalid_file() -> None: invalid_file = "Tests/images/flower.jpg" with pytest.raises(SyntaxError): XpmImagePlugin.XpmImageFile(invalid_file) -def test_load_read(): +def test_load_read() -> None: # Arrange with Image.open(TEST_FILE) as im: dummy_bytes = 1 diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index 5848995c1..6b8115930 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, XVThumbImagePlugin @@ -8,7 +9,7 @@ from .helper import assert_image_similar, hopper TEST_FILE = "Tests/images/hopper.p7" -def test_open(): +def test_open() -> None: # Act with Image.open(TEST_FILE) as im: # Assert @@ -19,7 +20,7 @@ def test_open(): assert_image_similar(im, im_hopper, 9) -def test_unexpected_eof(): +def test_unexpected_eof() -> None: # Test unexpected EOF reading XV thumbnail file # Arrange bad_file = "Tests/images/hopper_bad.p7" @@ -29,7 +30,7 @@ def test_unexpected_eof(): XVThumbImagePlugin.XVThumbImageFile(bad_file) -def test_invalid_file(): +def test_invalid_file() -> None: # Arrange invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 1e5eff2f1..136070f9e 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import BdfFontFile, FontFile @@ -6,7 +7,7 @@ from PIL import BdfFontFile, FontFile filename = "Tests/images/courB08.bdf" -def test_sanity(): +def test_sanity() -> None: with open(filename, "rb") as test_file: font = BdfFontFile.BdfFontFile(test_file) @@ -14,7 +15,7 @@ def test_sanity(): assert len([_f for _f in font.glyph if _f]) == 190 -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): BdfFontFile.BdfFontFile(fp) diff --git a/Tests/test_font_crash.py b/Tests/test_font_crash.py index 388ee7118..b82340ef7 100644 --- a/Tests/test_font_crash.py +++ b/Tests/test_font_crash.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageDraw, ImageFont @@ -7,7 +8,7 @@ from .helper import skip_unless_feature class TestFontCrash: - def _fuzz_font(self, font): + def _fuzz_font(self, font: ImageFont.FreeTypeFont) -> None: # from fuzzers.fuzz_font font.getbbox("ABC") font.getmask("test text") @@ -17,7 +18,7 @@ class TestFontCrash: draw.text((10, 10), "Test Text", font=font, fill="#000") @skip_unless_feature("freetype2") - def test_segfault(self): + def test_segfault(self) -> None: with pytest.raises(OSError): font = ImageFont.truetype("Tests/fonts/fuzz_font-5203009437302784") self._fuzz_font(font) diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 6a038bb40..241f455b8 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image, ImageDraw, ImageFont from .helper import PillowLeakTestCase, skip_unless_feature @@ -9,7 +10,7 @@ class TestTTypeFontLeak(PillowLeakTestCase): iterations = 10 mem_limit = 4096 # k - def _test_font(self, font): + def _test_font(self, font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (255, 255), "white") draw = ImageDraw.ImageDraw(im) self._test_leak( @@ -19,7 +20,7 @@ class TestTTypeFontLeak(PillowLeakTestCase): ) @skip_unless_feature("freetype2") - def test_leak(self): + def test_leak(self) -> None: ttype = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20) self._test_font(ttype) @@ -29,6 +30,6 @@ class TestDefaultFontLeak(TestTTypeFontLeak): iterations = 100 mem_limit = 1024 # k - def test_leak(self): + def test_leak(self) -> None: default_font = ImageFont.load_default() self._test_font(default_font) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 4365b9310..997809e46 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,5 +1,7 @@ from __future__ import annotations + import os +from pathlib import Path import pytest @@ -19,7 +21,7 @@ message = "hello, world" pytestmark = skip_unless_feature("zlib") -def save_font(request, tmp_path): +def save_font(request: pytest.FixtureRequest, tmp_path: Path) -> str: with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file) assert isinstance(font, FontFile.FontFile) @@ -28,7 +30,7 @@ def save_font(request, tmp_path): tempname = str(tmp_path / "temp.pil") - def delete_tempfile(): + def delete_tempfile() -> None: try: os.remove(tempname[:-4] + ".pbm") except OSError: @@ -46,11 +48,11 @@ def save_font(request, tmp_path): return tempname -def test_sanity(request, tmp_path): +def test_sanity(request: pytest.FixtureRequest, tmp_path: Path) -> None: save_font(request, tmp_path) -def test_less_than_256_characters(): +def test_less_than_256_characters() -> None: with open("Tests/fonts/10x20-ISO8859-1-fewer-characters.pcf", "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file) assert isinstance(font, FontFile.FontFile) @@ -58,13 +60,13 @@ def test_less_than_256_characters(): assert len([_f for _f in font.glyph if _f]) == 127 -def test_invalid_file(): +def test_invalid_file() -> None: with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): PcfFontFile.PcfFontFile(fp) -def test_draw(request, tmp_path): +def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) im = Image.new("L", (130, 30), "white") @@ -73,7 +75,7 @@ def test_draw(request, tmp_path): assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) -def test_textsize(request, tmp_path): +def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) for i in range(255): @@ -89,7 +91,9 @@ def test_textsize(request, tmp_path): assert font.getbbox(msg) == (0, 0, len(msg) * 10, 20) -def _test_high_characters(request, tmp_path, message): +def _test_high_characters( + request: pytest.FixtureRequest, tmp_path: Path, message: str | bytes +) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) im = Image.new("L", (750, 30), "white") @@ -98,7 +102,7 @@ def _test_high_characters(request, tmp_path, message): assert_image_similar_tofile(im, "Tests/images/high_ascii_chars.png", 0) -def test_high_characters(request, tmp_path): +def test_high_characters(request: pytest.FixtureRequest, tmp_path: Path) -> None: message = "".join(chr(i + 1) for i in range(140, 232)) _test_high_characters(request, tmp_path, message) # accept bytes instances. diff --git a/Tests/test_font_pcf_charsets.py b/Tests/test_font_pcf_charsets.py index 950e5029f..895458d9d 100644 --- a/Tests/test_font_pcf_charsets.py +++ b/Tests/test_font_pcf_charsets.py @@ -1,5 +1,8 @@ from __future__ import annotations + import os +from pathlib import Path +from typing import TypedDict import pytest @@ -13,7 +16,14 @@ from .helper import ( fontname = "Tests/fonts/ter-x20b.pcf" -charsets = { + +class Charset(TypedDict): + glyph_count: int + message: str + image1: str + + +charsets: dict[str, Charset] = { "iso8859-1": { "glyph_count": 223, "message": "hello, world", @@ -35,7 +45,7 @@ charsets = { pytestmark = skip_unless_feature("zlib") -def save_font(request, tmp_path, encoding): +def save_font(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> str: with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file, encoding) assert isinstance(font, FontFile.FontFile) @@ -44,7 +54,7 @@ def save_font(request, tmp_path, encoding): tempname = str(tmp_path / "temp.pil") - def delete_tempfile(): + def delete_tempfile() -> None: try: os.remove(tempname[:-4] + ".pbm") except OSError: @@ -63,12 +73,12 @@ def save_font(request, tmp_path, encoding): @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_sanity(request, tmp_path, encoding): +def test_sanity(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None: save_font(request, tmp_path, encoding) @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_draw(request, tmp_path, encoding): +def test_draw(request: pytest.FixtureRequest, tmp_path: Path, encoding: str) -> None: tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) im = Image.new("L", (150, 30), "white") @@ -79,7 +89,9 @@ def test_draw(request, tmp_path, encoding): @pytest.mark.parametrize("encoding", ("iso8859-1", "iso8859-2", "cp1250")) -def test_textsize(request, tmp_path, encoding): +def test_textsize( + request: pytest.FixtureRequest, tmp_path: Path, encoding: str +) -> None: tempname = save_font(request, tmp_path, encoding) font = ImageFont.load(tempname) for i in range(255): diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py new file mode 100644 index 000000000..206499a04 --- /dev/null +++ b/Tests/test_fontfile.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from PIL import FontFile + + +def test_save(tmp_path: Path) -> None: + tempname = str(tmp_path / "temp.pil") + + font = FontFile.FontFile() + with pytest.raises(ValueError): + font.save(tempname) diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index fd47fae39..c07024a2c 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -1,30 +1,28 @@ from __future__ import annotations + import colorsys import itertools +from typing import Callable from PIL import Image from .helper import assert_image_similar, hopper -def int_to_float(i): +def int_to_float(i: int) -> float: return i / 255 -def str_to_float(i): - return ord(i) / 255 - - -def tuple_to_ints(tp): +def tuple_to_ints(tp: tuple[float, float, float]) -> tuple[int, int, int]: x, y, z = tp return int(x * 255.0), int(y * 255.0), int(z * 255.0) -def test_sanity(): +def test_sanity() -> None: Image.new("HSV", (100, 100)) -def wedge(): +def wedge() -> Image.Image: w = Image._wedge() w90 = w.rotate(90) @@ -48,7 +46,11 @@ def wedge(): return img -def to_xxx_colorsys(im, func, mode): +def to_xxx_colorsys( + im: Image.Image, + func: Callable[[float, float, float], tuple[float, float, float]], + mode: str, +) -> Image.Image: # convert the hard way using the library colorsys routines. (r, g, b) = im.split() @@ -69,15 +71,15 @@ def to_xxx_colorsys(im, func, mode): return hsv -def to_hsv_colorsys(im): +def to_hsv_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.rgb_to_hsv, "HSV") -def to_rgb_colorsys(im): +def to_rgb_colorsys(im: Image.Image) -> Image.Image: return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB") -def test_wedge(): +def test_wedge() -> None: src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR) im = src.convert("HSV") comparable = to_hsv_colorsys(src) @@ -109,7 +111,7 @@ def test_wedge(): ) -def test_convert(): +def test_convert() -> None: im = hopper("RGB").convert("HSV") comparable = to_hsv_colorsys(hopper("RGB")) @@ -127,7 +129,7 @@ def test_convert(): ) -def test_hsv_to_rgb(): +def test_hsv_to_rgb() -> None: comparable = to_hsv_colorsys(hopper("RGB")) converted = comparable.convert("RGB") comparable = to_rgb_colorsys(comparable) diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index c7610ce8a..4fcc37e88 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -1,8 +1,9 @@ from __future__ import annotations + from PIL import Image -def test_white(): +def test_white() -> None: with Image.open("Tests/images/lab.tif") as i: i.load() @@ -23,7 +24,7 @@ def test_white(): assert list(b) == [128] * 100 -def test_green(): +def test_green() -> None: # l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS # == RGB: 0, 152, 117 with Image.open("Tests/images/lab-green.tif") as i: @@ -31,7 +32,7 @@ def test_green(): assert k == (128, 28, 128) -def test_red(): +def test_red() -> None: # l= 50 (/100), a = 100 (-128 .. 128) b=0 in PS # == RGB: 255, 0, 124 with Image.open("Tests/images/lab-red.tif") as i: diff --git a/Tests/test_image.py b/Tests/test_image.py index 615e00e40..4c04e0da4 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,4 +1,5 @@ from __future__ import annotations + import io import logging import os @@ -6,6 +7,8 @@ import shutil import sys import tempfile import warnings +from pathlib import Path +from typing import IO import pytest @@ -59,19 +62,19 @@ class TestImage: "HSV", ), ) - def test_image_modes_success(self, mode): + def test_image_modes_success(self, mode: str) -> None: Image.new(mode, (1, 1)) @pytest.mark.parametrize("mode", ("", "bad", "very very long")) - def test_image_modes_fail(self, mode): + def test_image_modes_fail(self, mode: str) -> None: with pytest.raises(ValueError) as e: Image.new(mode, (1, 1)) assert str(e.value) == "unrecognized image mode" - def test_exception_inheritance(self): + def test_exception_inheritance(self) -> None: assert issubclass(UnidentifiedImageError, OSError) - def test_sanity(self): + def test_sanity(self) -> None: im = Image.new("L", (100, 100)) assert repr(im)[:45] == " None: class Pretty: - def text(self, text): + def text(self, text: str) -> None: self.pretty_output = text im = Image.new("L", (100, 100)) @@ -107,7 +110,7 @@ class TestImage: im._repr_pretty_(p, None) assert p.pretty_output == "" - def test_open_formats(self): + def test_open_formats(self) -> None: PNGFILE = "Tests/images/hopper.png" JPGFILE = "Tests/images/hopper.jpg" @@ -129,7 +132,7 @@ class TestImage: assert im.mode == "RGB" assert im.size == (128, 128) - def test_width_height(self): + def test_width_height(self) -> None: im = Image.new("RGB", (1, 2)) assert im.width == 1 assert im.height == 2 @@ -137,31 +140,29 @@ class TestImage: with pytest.raises(AttributeError): im.size = (3, 4) - def test_set_mode(self): + def test_set_mode(self) -> None: im = Image.new("RGB", (1, 1)) with pytest.raises(AttributeError): im.mode = "P" - def test_invalid_image(self): + def test_invalid_image(self) -> None: im = io.BytesIO(b"") with pytest.raises(UnidentifiedImageError): with Image.open(im): pass - def test_bad_mode(self): + def test_bad_mode(self) -> None: with pytest.raises(ValueError): with Image.open("filename", "bad mode"): pass - def test_stringio(self): + def test_stringio(self) -> None: with pytest.raises(ValueError): with Image.open(io.StringIO()): pass - def test_pathlib(self, tmp_path): - from PIL.Image import Path - + def test_pathlib(self, tmp_path: Path) -> None: with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: assert im.mode == "P" assert im.size == (10, 10) @@ -178,11 +179,13 @@ class TestImage: os.remove(temp_file) im.save(Path(temp_file)) - def test_fp_name(self, tmp_path): + def test_fp_name(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.jpg") class FP: - def write(self, b): + name: str + + def write(self, b: bytes) -> None: pass fp = FP() @@ -191,7 +194,7 @@ class TestImage: im = hopper() im.save(fp) - def test_tempfile(self): + def test_tempfile(self) -> None: # see #1460, pathlib support breaks tempfile.TemporaryFile on py27 # Will error out on save on 3.0.0 im = hopper() @@ -200,13 +203,13 @@ class TestImage: fp.seek(0) assert_image_similar_tofile(im, fp, 20) - def test_unknown_extension(self, tmp_path): + def test_unknown_extension(self, tmp_path: Path) -> None: im = hopper() temp_file = str(tmp_path / "temp.unknown") with pytest.raises(ValueError): im.save(temp_file) - def test_internals(self): + def test_internals(self) -> None: im = Image.new("L", (100, 100)) im.readonly = 1 im._copy() @@ -221,7 +224,7 @@ class TestImage: sys.platform == "cygwin", reason="Test requires opening an mmaped file for writing", ) - def test_readonly_save(self, tmp_path): + def test_readonly_save(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.bmp") shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) @@ -229,7 +232,7 @@ class TestImage: assert im.readonly im.save(temp_file) - def test_dump(self, tmp_path): + def test_dump(self, tmp_path: Path) -> None: im = Image.new("L", (10, 10)) im._dump(str(tmp_path / "temp_L.ppm")) @@ -240,7 +243,7 @@ class TestImage: with pytest.raises(ValueError): im._dump(str(tmp_path / "temp_HSV.ppm")) - def test_comparison_with_other_type(self): + def test_comparison_with_other_type(self) -> None: # Arrange item = Image.new("RGB", (25, 25), "#000") num = 12 @@ -250,7 +253,7 @@ class TestImage: assert item is not None assert item != num - def test_expand_x(self): + def test_expand_x(self) -> None: # Arrange im = hopper() orig_size = im.size @@ -263,7 +266,7 @@ class TestImage: assert im.size[0] == orig_size[0] + 2 * xmargin assert im.size[1] == orig_size[1] + 2 * xmargin - def test_expand_xy(self): + def test_expand_xy(self) -> None: # Arrange im = hopper() orig_size = im.size @@ -277,12 +280,12 @@ class TestImage: assert im.size[0] == orig_size[0] + 2 * xmargin assert im.size[1] == orig_size[1] + 2 * ymargin - def test_getbands(self): + def test_getbands(self) -> None: # Assert assert hopper("RGB").getbands() == ("R", "G", "B") assert hopper("YCbCr").getbands() == ("Y", "Cb", "Cr") - def test_getchannel_wrong_params(self): + def test_getchannel_wrong_params(self) -> None: im = hopper() with pytest.raises(ValueError): @@ -294,7 +297,7 @@ class TestImage: with pytest.raises(ValueError): im.getchannel("1") - def test_getchannel(self): + def test_getchannel(self) -> None: im = hopper("YCbCr") Y, Cb, Cr = im.split() @@ -305,7 +308,7 @@ class TestImage: assert_image_equal(Cr, im.getchannel(2)) assert_image_equal(Cr, im.getchannel("Cr")) - def test_getbbox(self): + def test_getbbox(self) -> None: # Arrange im = hopper() @@ -315,7 +318,7 @@ class TestImage: # Assert assert bbox == (0, 0, 128, 128) - def test_ne(self): + def test_ne(self) -> None: # Arrange im1 = Image.new("RGB", (25, 25), "black") im2 = Image.new("RGB", (25, 25), "white") @@ -323,7 +326,7 @@ class TestImage: # Act / Assert assert im1 != im2 - def test_alpha_composite(self): + def test_alpha_composite(self) -> None: # https://stackoverflow.com/questions/3374878 # Arrange expected_colors = sorted( @@ -354,7 +357,7 @@ class TestImage: img_colors = sorted(img.getcolors()) assert img_colors == expected_colors - def test_alpha_inplace(self): + def test_alpha_inplace(self) -> None: src = Image.new("RGBA", (128, 128), "blue") over = Image.new("RGBA", (128, 128), "red") @@ -406,7 +409,7 @@ class TestImage: with pytest.raises(ValueError): source.alpha_composite(over, (0, 0), (0, -1)) - def test_register_open_duplicates(self): + def test_register_open_duplicates(self) -> None: # Arrange factory, accept = Image.OPEN["JPEG"] id_length = len(Image.ID) @@ -417,7 +420,7 @@ class TestImage: # Assert assert len(Image.ID) == id_length - def test_registered_extensions_uninitialized(self): + def test_registered_extensions_uninitialized(self) -> None: # Arrange Image._initialized = 0 @@ -427,7 +430,7 @@ class TestImage: # Assert assert Image._initialized == 2 - def test_registered_extensions(self): + def test_registered_extensions(self) -> None: # Arrange # Open an image to trigger plugin registration with Image.open("Tests/images/rgb.jpg"): @@ -441,7 +444,7 @@ class TestImage: for ext in [".cur", ".icns", ".tif", ".tiff"]: assert ext in extensions - def test_effect_mandelbrot(self): + def test_effect_mandelbrot(self) -> None: # Arrange size = (512, 512) extent = (-3, -2.5, 2, 2.5) @@ -454,7 +457,7 @@ class TestImage: assert im.size == (512, 512) assert_image_equal_tofile(im, "Tests/images/effect_mandelbrot.png") - def test_effect_mandelbrot_bad_arguments(self): + def test_effect_mandelbrot_bad_arguments(self) -> None: # Arrange size = (512, 512) # Get coordinates the wrong way round: @@ -466,7 +469,7 @@ class TestImage: with pytest.raises(ValueError): Image.effect_mandelbrot(size, extent, quality) - def test_effect_noise(self): + def test_effect_noise(self) -> None: # Arrange size = (100, 100) sigma = 128 @@ -484,7 +487,7 @@ class TestImage: p4 = im.getpixel((0, 4)) assert_not_all_same([p0, p1, p2, p3, p4]) - def test_effect_spread(self): + def test_effect_spread(self) -> None: # Arrange im = hopper() distance = 10 @@ -496,7 +499,7 @@ class TestImage: assert im.size == (128, 128) assert_image_similar_tofile(im2, "Tests/images/effect_spread.png", 110) - def test_effect_spread_zero(self): + def test_effect_spread_zero(self) -> None: # Arrange im = hopper() distance = 0 @@ -507,7 +510,7 @@ class TestImage: # Assert assert_image_equal(im, im2) - def test_check_size(self): + def test_check_size(self) -> None: # Checking that the _check_size function throws value errors when we want it to with pytest.raises(ValueError): Image.new("RGB", 0) # not a tuple @@ -536,10 +539,10 @@ class TestImage: "PILLOW_VALGRIND_TEST" in os.environ, reason="Valgrind is slower" ) @pytest.mark.parametrize("size", ((0, 100000000), (100000000, 0))) - def test_empty_image(self, size): + def test_empty_image(self, size: tuple[int, int]) -> None: Image.new("RGB", size) - def test_storage_neg(self): + def test_storage_neg(self) -> None: # Storage.c accepted negative values for xsize, ysize. Was # test_neg_ppm, but the core function for that has been # removed Calling directly into core to test the error in @@ -548,13 +551,13 @@ class TestImage: with pytest.raises(ValueError): Image.core.fill("RGB", (2, -2), (0, 0, 0)) - def test_one_item_tuple(self): + def test_one_item_tuple(self) -> None: for mode in ("I", "F", "L"): im = Image.new(mode, (100, 100), (5,)) px = im.load() assert px[0, 0] == 5 - def test_linear_gradient_wrong_mode(self): + def test_linear_gradient_wrong_mode(self) -> None: # Arrange wrong_mode = "RGB" @@ -563,7 +566,7 @@ class TestImage: Image.linear_gradient(wrong_mode) @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_linear_gradient(self, mode): + def test_linear_gradient(self, mode: str) -> None: # Arrange target_file = "Tests/images/linear_gradient.png" @@ -579,7 +582,7 @@ class TestImage: target = target.convert(mode) assert_image_equal(im, target) - def test_radial_gradient_wrong_mode(self): + def test_radial_gradient_wrong_mode(self) -> None: # Arrange wrong_mode = "RGB" @@ -588,7 +591,7 @@ class TestImage: Image.radial_gradient(wrong_mode) @pytest.mark.parametrize("mode", ("L", "P", "I", "F")) - def test_radial_gradient(self, mode): + def test_radial_gradient(self, mode: str) -> None: # Arrange target_file = "Tests/images/radial_gradient.png" @@ -604,7 +607,7 @@ class TestImage: target = target.convert(mode) assert_image_equal(im, target) - def test_register_extensions(self): + def test_register_extensions(self) -> None: test_format = "a" exts = ["b", "c"] for ext in exts: @@ -620,7 +623,7 @@ class TestImage: assert ext_individual == ext_multiple - def test_remap_palette(self): + def test_remap_palette(self) -> None: # Test identity transform with Image.open("Tests/images/hopper.gif") as im: assert_image_equal(im, im.remap_palette(list(range(256)))) @@ -639,7 +642,7 @@ class TestImage: with pytest.raises(ValueError): im.remap_palette(None) - def test_remap_palette_transparency(self): + def test_remap_palette_transparency(self) -> None: im = Image.new("P", (1, 2), (0, 0, 0)) im.putpixel((0, 1), (255, 0, 0)) im.info["transparency"] = 0 @@ -654,7 +657,7 @@ class TestImage: im_remapped = im.remap_palette([1, 0]) assert "transparency" not in im_remapped.info - def test__new(self): + def test__new(self) -> None: im = hopper("RGB") im_p = hopper("P") @@ -663,7 +666,11 @@ class TestImage: blank_p.palette = None blank_pa.palette = None - def _make_new(base_image, image, palette_result=None): + def _make_new( + base_image: Image.Image, + image: Image.Image, + palette_result: ImagePalette.ImagePalette | None = None, + ) -> None: new_image = base_image._new(image.im) assert new_image.mode == image.mode assert new_image.size == image.size @@ -678,7 +685,7 @@ class TestImage: _make_new(im, blank_p, ImagePalette.ImagePalette()) _make_new(im, blank_pa, ImagePalette.ImagePalette()) - def test_p_from_rgb_rgba(self): + def test_p_from_rgb_rgba(self) -> None: for mode, color in [ ("RGB", "#DDEEFF"), ("RGB", (221, 238, 255)), @@ -688,7 +695,7 @@ class TestImage: expected = Image.new(mode, (100, 100), color) assert_image_equal(im.convert(mode), expected) - def test_no_resource_warning_on_save(self, tmp_path): + def test_no_resource_warning_on_save(self, tmp_path: Path) -> None: # https://github.com/python-pillow/Pillow/issues/835 # Arrange test_file = "Tests/images/hopper.png" @@ -699,7 +706,7 @@ class TestImage: with warnings.catch_warnings(): im.save(temp_file) - def test_no_new_file_on_error(self, tmp_path): + def test_no_new_file_on_error(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.jpg") im = Image.new("RGB", (0, 0)) @@ -708,10 +715,10 @@ class TestImage: assert not os.path.exists(temp_file) - def test_load_on_nonexclusive_multiframe(self): + def test_load_on_nonexclusive_multiframe(self) -> None: with open("Tests/images/frozenpond.mpo", "rb") as fp: - def act(fp): + def act(fp: IO[bytes]) -> None: im = Image.open(fp) im.load() @@ -722,7 +729,7 @@ class TestImage: assert not fp.closed - def test_empty_exif(self): + def test_empty_exif(self) -> None: with Image.open("Tests/images/exif.png") as im: exif = im.getexif() assert dict(exif) @@ -738,7 +745,7 @@ class TestImage: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_exif_jpeg(self, tmp_path): + def test_exif_jpeg(self, tmp_path: Path) -> None: with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian exif = im.getexif() assert 258 not in exif @@ -784,7 +791,7 @@ class TestImage: @skip_unless_feature("webp") @skip_unless_feature("webp_anim") - def test_exif_webp(self, tmp_path): + def test_exif_webp(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.webp") as im: exif = im.getexif() assert exif == {} @@ -794,7 +801,7 @@ class TestImage: exif[40963] = 455 exif[305] = "Pillow test" - def check_exif(): + def check_exif() -> None: with Image.open(out) as reloaded: reloaded_exif = reloaded.getexif() assert reloaded_exif[258] == 8 @@ -806,7 +813,7 @@ class TestImage: im.save(out, exif=exif, save_all=True) check_exif() - def test_exif_png(self, tmp_path): + def test_exif_png(self, tmp_path: Path) -> None: with Image.open("Tests/images/exif.png") as im: exif = im.getexif() assert exif == {274: 1} @@ -822,7 +829,7 @@ class TestImage: reloaded_exif = reloaded.getexif() assert reloaded_exif == {258: 8, 40963: 455, 305: "Pillow test"} - def test_exif_interop(self): + def test_exif_interop(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() assert exif.get_ifd(0xA005) == { @@ -836,7 +843,7 @@ class TestImage: reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0xA005) == exif.get_ifd(0xA005) - def test_exif_ifd1(self): + def test_exif_ifd1(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() assert exif.get_ifd(ExifTags.IFD.IFD1) == { @@ -848,7 +855,7 @@ class TestImage: 283: 180.0, } - def test_exif_ifd(self): + def test_exif_ifd(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() del exif.get_ifd(0x8769)[0xA005] @@ -857,7 +864,7 @@ class TestImage: reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) - def test_exif_load_from_fp(self): + def test_exif_load_from_fp(self) -> None: with Image.open("Tests/images/flower.jpg") as im: data = im.info["exif"] if data.startswith(b"Exif\x00\x00"): @@ -878,7 +885,7 @@ class TestImage: 34665: 196, } - def test_exif_hide_offsets(self): + def test_exif_hide_offsets(self) -> None: with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() @@ -904,18 +911,18 @@ class TestImage: assert exif.get_ifd(0xA005) @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_tobytes(self, size): + def test_zero_tobytes(self, size: tuple[int, int]) -> None: im = Image.new("RGB", size) assert im.tobytes() == b"" @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero_frombytes(self, size): + def test_zero_frombytes(self, size: tuple[int, int]) -> None: Image.frombytes("RGB", size, b"") im = Image.new("RGB", size) im.frombytes(b"") - def test_has_transparency_data(self): + def test_has_transparency_data(self) -> None: for mode in ("1", "L", "P", "RGB"): im = Image.new(mode, (1, 1)) assert not im.has_transparency_data @@ -940,7 +947,7 @@ class TestImage: assert im.palette.mode == "RGBA" assert im.has_transparency_data - def test_apply_transparency(self): + def test_apply_transparency(self) -> None: im = Image.new("P", (1, 1)) im.putpalette((0, 0, 0, 1, 1, 1)) assert im.palette.colors == {(0, 0, 0): 0, (1, 1, 1): 1} @@ -969,7 +976,7 @@ class TestImage: im.apply_transparency() assert im.palette.colors[(27, 35, 6, 214)] == 24 - def test_constants(self): + def test_constants(self) -> None: for enum in ( Image.Transpose, Image.Transform, @@ -994,7 +1001,7 @@ class TestImage: "01r_00.pcx", ], ) - def test_overrun(self, path): + def test_overrun(self, path: str) -> None: """For overrun completeness, test as: valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c """ @@ -1008,7 +1015,7 @@ class TestImage: assert buffer_overrun or truncated - def test_fli_overrun2(self): + def test_fli_overrun2(self) -> None: with Image.open("Tests/images/fli_overrun2.bin") as im: try: im.seek(1) @@ -1016,7 +1023,12 @@ class TestImage: except OSError as e: assert str(e) == "buffer overrun when reading image file" - def test_close_graceful(self, caplog): + def test_exit_fp(self) -> None: + with Image.new("L", (1, 1)) as im: + pass + assert not hasattr(im, "fp") + + def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None: with Image.open("Tests/images/hopper.jpg") as im: copy = im.copy() with caplog.at_level(logging.DEBUG): @@ -1027,17 +1039,17 @@ class TestImage: class MockEncoder: - pass + args: tuple[str, ...] -def mock_encode(*args): +def mock_encode(*args: str) -> MockEncoder: encoder = MockEncoder() encoder.args = args return encoder class TestRegistry: - def test_encode_registry(self): + def test_encode_registry(self) -> None: Image.register_encoder("MOCK", mock_encode) assert "MOCK" in Image.ENCODERS @@ -1046,6 +1058,6 @@ class TestRegistry: assert isinstance(enc, MockEncoder) assert enc.args == ("RGB", "args", "extra") - def test_encode_registry_fail(self): + def test_encode_registry_fail(self) -> None: with pytest.raises(OSError): Image._getencoder("RGB", "DoesNotExist", ("args",), extra=("extra",)) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 4a794371d..380b89de8 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -1,8 +1,10 @@ from __future__ import annotations + import os import subprocess import sys import sysconfig +from types import ModuleType import pytest @@ -22,6 +24,7 @@ else: except ImportError: cffi = None +numpy: ModuleType | None try: import numpy except ImportError: @@ -34,16 +37,16 @@ class AccessTest: _need_cffi_access = False @classmethod - def setup_class(cls): + def setup_class(cls) -> None: Image.USE_CFFI_ACCESS = cls._need_cffi_access @classmethod - def teardown_class(cls): + def teardown_class(cls) -> None: Image.USE_CFFI_ACCESS = cls._init_cffi_access class TestImagePutPixel(AccessTest): - def test_sanity(self): + def test_sanity(self) -> None: im1 = hopper() im2 = Image.new(im1.mode, im1.size, 0) @@ -70,9 +73,10 @@ class TestImagePutPixel(AccessTest): pix1 = im1.load() pix2 = im2.load() - for x, y in ((0, "0"), ("0", 0)): - with pytest.raises(TypeError): - pix1[x, y] + with pytest.raises(TypeError): + pix1[0, "0"] + with pytest.raises(TypeError): + pix1["0", 0] for y in range(im1.size[1]): for x in range(im1.size[0]): @@ -80,7 +84,7 @@ class TestImagePutPixel(AccessTest): assert_image_equal(im1, im2) - def test_sanity_negative_index(self): + def test_sanity_negative_index(self) -> None: im1 = hopper() im2 = Image.new(im1.mode, im1.size, 0) @@ -118,16 +122,17 @@ class TestImagePutPixel(AccessTest): assert_image_equal(im1, im2) @pytest.mark.skipif(numpy is None, reason="NumPy not installed") - def test_numpy(self): + def test_numpy(self) -> None: im = hopper() pix = im.load() + assert numpy is not None assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) class TestImageGetPixel(AccessTest): @staticmethod - def color(mode): + def color(mode: str) -> int | tuple[int, ...]: bands = Image.getmodebands(mode) if bands == 1: return 1 @@ -137,12 +142,13 @@ class TestImageGetPixel(AccessTest): return (16, 32, 49) return tuple(range(1, bands + 1)) - def check(self, mode, expected_color=None): + def check(self, mode: str, expected_color_int: int | None = None) -> None: if self._need_cffi_access and mode.startswith("BGR;"): pytest.skip("Support not added to deprecated module for BGR;* modes") - if not expected_color: - expected_color = self.color(mode) + expected_color = ( + self.color(mode) if expected_color_int is None else expected_color_int + ) # check putpixel im = Image.new(mode, (1, 1), None) @@ -221,25 +227,23 @@ class TestImageGetPixel(AccessTest): "YCbCr", ), ) - def test_basic(self, mode): + def test_basic(self, mode: str) -> None: self.check(mode) - def test_list(self): + def test_list(self) -> None: im = hopper() assert im.getpixel([0, 0]) == (20, 20, 70) @pytest.mark.parametrize("mode", ("I;16", "I;16B")) - @pytest.mark.parametrize( - "expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1) - ) - def test_signedness(self, mode, expected_color): + @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) + def test_signedness(self, mode: str, expected_color: int) -> None: # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* self.check(mode, expected_color) @pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) - def test_p_putpixel_rgb_rgba(self, mode, color): + def test_p_putpixel_rgb_rgba(self, mode: str, color: tuple[int, ...]) -> None: im = Image.new(mode, (1, 1)) im.putpixel((0, 0), color) @@ -263,7 +267,7 @@ class TestCffiGetPixel(TestImageGetPixel): class TestCffi(AccessTest): _need_cffi_access = True - def _test_get_access(self, im): + def _test_get_access(self, im: Image.Image) -> None: """Do we get the same thing as the old pixel access Using private interfaces, forcing a capi access and @@ -281,7 +285,7 @@ class TestCffi(AccessTest): with pytest.raises(ValueError): access[(access.xsize + 1, access.ysize + 1)] - def test_get_vs_c(self): + def test_get_vs_c(self) -> None: with pytest.warns(DeprecationWarning): rgb = hopper("RGB") rgb.load() @@ -300,7 +304,7 @@ class TestCffi(AccessTest): # im = Image.new('I;32B', (10, 10), 2**10) # self._test_get_access(im) - def _test_set_access(self, im, color): + def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None: """Are we writing the correct bits into the image? Using private interfaces, forcing a capi access and @@ -321,7 +325,7 @@ class TestCffi(AccessTest): with pytest.raises(ValueError): access[(0, 0)] = color - def test_set_vs_c(self): + def test_set_vs_c(self) -> None: rgb = hopper("RGB") with pytest.warns(DeprecationWarning): rgb.load() @@ -344,11 +348,11 @@ class TestCffi(AccessTest): # self._test_set_access(im, 2**13-1) @pytest.mark.filterwarnings("ignore::DeprecationWarning") - def test_not_implemented(self): + def test_not_implemented(self) -> None: assert PyAccess.new(hopper("BGR;15")) is None # ref https://github.com/python-pillow/Pillow/pull/2009 - def test_reference_counting(self): + def test_reference_counting(self) -> None: size = 10 for _ in range(10): @@ -360,7 +364,7 @@ class TestCffi(AccessTest): assert px[i, 0] == 0 @pytest.mark.parametrize("mode", ("P", "PA")) - def test_p_putpixel_rgb_rgba(self, mode): + def test_p_putpixel_rgb_rgba(self, mode: str) -> None: for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): im = Image.new(mode, (1, 1)) with pytest.warns(DeprecationWarning): @@ -378,7 +382,7 @@ class TestImagePutPixelError(AccessTest): INVALID_TYPES = ["foo", 1.0, None] @pytest.mark.parametrize("mode", IMAGE_MODES1) - def test_putpixel_type_error1(self, mode): + def test_putpixel_type_error1(self, mode: str) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises(TypeError, match="color must be int or tuple"): @@ -401,14 +405,16 @@ class TestImagePutPixelError(AccessTest): ), ), ) - def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match): + def test_putpixel_invalid_number_of_bands( + self, mode: str, band_numbers: tuple[int, ...], match: str + ) -> None: im = hopper(mode) for band_number in band_numbers: with pytest.raises(TypeError, match=match): im.putpixel((0, 0), (0,) * band_number) @pytest.mark.parametrize("mode", IMAGE_MODES2) - def test_putpixel_type_error2(self, mode): + def test_putpixel_type_error2(self, mode: str) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises( @@ -417,7 +423,7 @@ class TestImagePutPixelError(AccessTest): im.putpixel((0, 0), v) @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) - def test_putpixel_overflow_error(self, mode): + def test_putpixel_overflow_error(self, mode: str) -> None: im = hopper(mode) with pytest.raises(OverflowError): im.putpixel((0, 0), 2**80) @@ -426,10 +432,10 @@ class TestImagePutPixelError(AccessTest): class TestEmbeddable: @pytest.mark.xfail(reason="failing test") @pytest.mark.skipif(not is_win32(), reason="requires Windows") - def test_embeddable(self): + def test_embeddable(self) -> None: import ctypes - from setuptools.command.build_ext import new_compiler + from setuptools.command import build_ext with open("embed_pil.c", "w", encoding="utf-8") as fh: fh.write( @@ -458,7 +464,7 @@ int main(int argc, char* argv[]) % sys.prefix.replace("\\", "\\\\") ) - compiler = new_compiler() + compiler = getattr(build_ext, "new_compiler")() compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( @@ -472,7 +478,7 @@ int main(int argc, char* argv[]) env["PATH"] = sys.prefix + ";" + env["PATH"] # do not display the Windows Error Reporting dialog - ctypes.windll.kernel32.SetErrorMode(0x0002) + getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) process = subprocess.Popen(["embed_pil.exe"], env=env) process.communicate() diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index b3e5d9e3e..cf85ee4fa 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from typing import Any + import pytest from packaging.version import parse as parse_version @@ -11,12 +14,12 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed") im = hopper().resize((128, 100)) -def test_toarray(): - def test(mode): +def test_toarray() -> None: + def test(mode: str) -> tuple[tuple[int, ...], str, int]: ai = numpy.array(im.convert(mode)) return ai.shape, ai.dtype.str, ai.nbytes - def test_with_dtype(dtype): + def test_with_dtype(dtype) -> None: ai = numpy.array(im, dtype=dtype) assert ai.dtype == dtype @@ -45,18 +48,18 @@ def test_toarray(): numpy.array(im_truncated) -def test_fromarray(): +def test_fromarray() -> None: class Wrapper: """Class with API matching Image.fromarray""" - def __init__(self, img, arr_params): + def __init__(self, img: Image.Image, arr_params: dict[str, Any]) -> None: self.img = img self.__array_interface__ = arr_params - def tobytes(self): + def tobytes(self) -> bytes: return self.img.tobytes() - def test(mode): + def test(mode: str) -> tuple[str, tuple[int, int], bool]: i = im.convert(mode) a = numpy.array(i) # Make wrapper instance for image, new array interface @@ -88,7 +91,7 @@ def test_fromarray(): Image.fromarray(wrapped) -def test_fromarray_palette(): +def test_fromarray_palette() -> None: # Arrange i = im.convert("L") a = numpy.array(i) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 7c17040d3..f154de123 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image @@ -6,8 +9,8 @@ from PIL import Image from .helper import assert_image, assert_image_equal, assert_image_similar, hopper -def test_sanity(): - def convert(im, mode): +def test_sanity() -> None: + def convert(im: Image.Image, mode: str) -> None: out = im.convert(mode) assert out.mode == mode assert out.size == im.size @@ -39,13 +42,13 @@ def test_sanity(): convert(im, output_mode) -def test_unsupported_conversion(): +def test_unsupported_conversion() -> None: im = hopper() with pytest.raises(ValueError): im.convert("INVALID") -def test_default(): +def test_default() -> None: im = hopper("P") assert im.mode == "P" converted_im = im.convert() @@ -61,18 +64,18 @@ def test_default(): # ref https://github.com/python-pillow/Pillow/issues/274 -def _test_float_conversion(im): +def _test_float_conversion(im: Image.Image) -> None: orig = im.getpixel((5, 5)) converted = im.convert("F").getpixel((5, 5)) assert orig == converted -def test_8bit(): +def test_8bit() -> None: with Image.open("Tests/images/hopper.jpg") as im: _test_float_conversion(im.convert("L")) -def test_16bit(): +def test_16bit() -> None: with Image.open("Tests/images/16bit.cropped.tif") as im: _test_float_conversion(im) @@ -82,19 +85,19 @@ def test_16bit(): assert im_i16.getpixel((0, 0)) == 65535 -def test_16bit_workaround(): +def test_16bit_workaround() -> None: with Image.open("Tests/images/16bit.cropped.tif") as im: _test_float_conversion(im.convert("I")) -def test_opaque(): +def test_opaque() -> None: alpha = hopper("P").convert("PA").getchannel("A") solid = Image.new("L", (128, 128), 255) assert_image_equal(alpha, solid) -def test_rgba_p(): +def test_rgba_p() -> None: im = hopper("RGBA") im.putalpha(hopper("L")) @@ -104,14 +107,14 @@ def test_rgba_p(): assert_image_similar(im, comparable, 20) -def test_rgba(): +def test_rgba() -> None: with Image.open("Tests/images/transparent.png") as im: assert im.mode == "RGBA" assert_image_similar(im.convert("RGBa").convert("RGB"), im.convert("RGB"), 1.5) -def test_trns_p(tmp_path): +def test_trns_p(tmp_path: Path) -> None: im = hopper("P") im.info["transparency"] = 0 @@ -130,7 +133,7 @@ def test_trns_p(tmp_path): @pytest.mark.parametrize("mode", ("LA", "PA", "RGBA")) -def test_trns_p_transparency(mode): +def test_trns_p_transparency(mode: str) -> None: # Arrange im = hopper("P") im.info["transparency"] = 128 @@ -147,7 +150,7 @@ def test_trns_p_transparency(mode): assert converted_im.palette is None -def test_trns_l(tmp_path): +def test_trns_l(tmp_path: Path) -> None: im = hopper("L") im.info["transparency"] = 128 @@ -170,7 +173,7 @@ def test_trns_l(tmp_path): im_p.save(f) -def test_trns_RGB(tmp_path): +def test_trns_RGB(tmp_path: Path) -> None: im = hopper("RGB") im.info["transparency"] = im.getpixel((0, 0)) @@ -200,7 +203,7 @@ def test_trns_RGB(tmp_path): @pytest.mark.parametrize("convert_mode", ("L", "LA", "I")) -def test_l_macro_rounding(convert_mode): +def test_l_macro_rounding(convert_mode: str) -> None: for mode in ("P", "PA"): im = Image.new(mode, (1, 1)) im.palette.getcolor((0, 1, 2)) @@ -213,7 +216,7 @@ def test_l_macro_rounding(convert_mode): assert converted_color == 1 -def test_gif_with_rgba_palette_to_p(): +def test_gif_with_rgba_palette_to_p() -> None: # See https://github.com/python-pillow/Pillow/issues/2433 with Image.open("Tests/images/hopper.gif") as im: im.info["transparency"] = 255 @@ -225,7 +228,7 @@ def test_gif_with_rgba_palette_to_p(): im_p.load() -def test_p_la(): +def test_p_la() -> None: im = hopper("RGBA") alpha = hopper("L") im.putalpha(alpha) @@ -235,7 +238,7 @@ def test_p_la(): assert_image_similar(alpha, comparable, 5) -def test_p2pa_alpha(): +def test_p2pa_alpha() -> None: with Image.open("Tests/images/tiny.png") as im: assert im.mode == "P" @@ -249,13 +252,13 @@ def test_p2pa_alpha(): assert im_a.getpixel((x, y)) == alpha -def test_p2pa_palette(): +def test_p2pa_palette() -> None: with Image.open("Tests/images/tiny.png") as im: im_pa = im.convert("PA") assert im_pa.getpalette() == im.getpalette() -def test_matrix_illegal_conversion(): +def test_matrix_illegal_conversion() -> None: # Arrange im = hopper("CMYK") # fmt: off @@ -271,7 +274,7 @@ def test_matrix_illegal_conversion(): im.convert(mode="CMYK", matrix=matrix) -def test_matrix_wrong_mode(): +def test_matrix_wrong_mode() -> None: # Arrange im = hopper("L") # fmt: off @@ -288,7 +291,7 @@ def test_matrix_wrong_mode(): @pytest.mark.parametrize("mode", ("RGB", "L")) -def test_matrix_xyz(mode): +def test_matrix_xyz(mode: str) -> None: # Arrange im = hopper("RGB") im.info["transparency"] = (255, 0, 0) @@ -316,7 +319,7 @@ def test_matrix_xyz(mode): assert converted_im.info["transparency"] == 105 -def test_matrix_identity(): +def test_matrix_identity() -> None: # Arrange im = hopper("RGB") # fmt: off diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index abf5f846f..027e5338b 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,4 +1,5 @@ from __future__ import annotations + import copy import pytest @@ -9,7 +10,7 @@ from .helper import hopper, skip_unless_feature @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_copy(mode): +def test_copy(mode: str) -> None: cropped_coordinates = (10, 10, 20, 20) cropped_size = (10, 10) @@ -38,7 +39,7 @@ def test_copy(mode): assert out.size == cropped_size -def test_copy_zero(): +def test_copy_zero() -> None: im = Image.new("RGB", (0, 0)) out = im.copy() assert out.mode == im.mode @@ -46,7 +47,7 @@ def test_copy_zero(): @skip_unless_feature("libtiff") -def test_deepcopy(): +def test_deepcopy() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: out = copy.deepcopy(im) assert out.size == (590, 88) diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 0bb54e5d8..d095364ba 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image @@ -7,7 +8,7 @@ from .helper import assert_image_equal, hopper @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_crop(mode): +def test_crop(mode: str) -> None: im = hopper(mode) assert_image_equal(im.crop(), im) @@ -16,8 +17,8 @@ def test_crop(mode): assert cropped.size == (50, 50) -def test_wide_crop(): - def crop(*bbox): +def test_wide_crop() -> None: + def crop(*bbox: int) -> tuple[int, ...]: i = im.crop(bbox) h = i.histogram() while h and not h[-1]: @@ -46,14 +47,14 @@ def test_wide_crop(): @pytest.mark.parametrize("box", ((8, 2, 2, 8), (2, 8, 8, 2), (8, 8, 2, 2))) -def test_negative_crop(box): +def test_negative_crop(box: tuple[int, int, int, int]) -> None: im = Image.new("RGB", (10, 10)) with pytest.raises(ValueError): im.crop(box) -def test_crop_float(): +def test_crop_float() -> None: # Check cropping floats are rounded to nearest integer # https://github.com/python-pillow/Pillow/issues/1744 @@ -68,7 +69,7 @@ def test_crop_float(): assert cropped.size == (3, 5) -def test_crop_crash(): +def test_crop_crash() -> None: # Image.crop crashes prepatch with an access violation # apparently a use after free on Windows, see # https://github.com/python-pillow/Pillow/issues/1077 @@ -86,7 +87,7 @@ def test_crop_crash(): img.load() -def test_crop_zero(): +def test_crop_zero() -> None: im = Image.new("RGB", (0, 0), "white") cropped = im.crop((0, 0, 0, 0)) diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 774272dd1..1ce1a7cd8 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -1,4 +1,5 @@ from __future__ import annotations + from PIL import Image from .helper import fromstring, skip_unless_feature, tostring @@ -6,7 +7,12 @@ from .helper import fromstring, skip_unless_feature, tostring pytestmark = skip_unless_feature("jpg") -def draft_roundtrip(in_mode, in_size, req_mode, req_size): +def draft_roundtrip( + in_mode: str, + in_size: tuple[int, int], + req_mode: str | None, + req_size: tuple[int, int] | None, +) -> Image.Image: im = Image.new(in_mode, in_size) data = tostring(im, "JPEG") im = fromstring(data) @@ -18,7 +24,7 @@ def draft_roundtrip(in_mode, in_size, req_mode, req_size): return im -def test_size(): +def test_size() -> None: for in_size, req_size, out_size in [ ((435, 361), (2048, 2048), (435, 361)), # bigger ((435, 361), (435, 361), (435, 361)), # same @@ -47,7 +53,7 @@ def test_size(): assert im.size == out_size -def test_mode(): +def test_mode() -> None: for in_mode, req_mode, out_mode in [ ("RGB", "1", "RGB"), ("RGB", "L", "L"), @@ -67,7 +73,7 @@ def test_mode(): assert im.mode == out_mode -def test_several_drafts(): +def test_several_drafts() -> None: im = draft_roundtrip("L", (128, 128), None, (64, 64)) im.draft(None, (64, 64)) im.load() diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py index 031fceda3..c1dbb879b 100644 --- a/Tests/test_image_entropy.py +++ b/Tests/test_image_entropy.py @@ -1,9 +1,10 @@ from __future__ import annotations + from .helper import hopper -def test_entropy(): - def entropy(mode): +def test_entropy() -> None: + def entropy(mode: str) -> float: return hopper(mode).entropy() assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0 diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 5bd7ee0d2..6a10ae453 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageFilter @@ -35,7 +36,7 @@ from .helper import assert_image_equal, hopper ), ) @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity(filter_to_apply, mode): +def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: im = hopper(mode) if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): out = im.filter(filter_to_apply) @@ -44,7 +45,7 @@ def test_sanity(filter_to_apply, mode): @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity_error(mode): +def test_sanity_error(mode: str) -> None: with pytest.raises(TypeError): im = hopper(mode) im.filter("hello") @@ -52,7 +53,7 @@ def test_sanity_error(mode): # crashes on small images @pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) -def test_crash(size): +def test_crash(size: tuple[int, int]) -> None: im = Image.new("RGB", size) im.filter(ImageFilter.SMOOTH) @@ -66,7 +67,10 @@ def test_crash(size): ("RGB", ((4, 0, 0), (0, 0, 0))), ), ) -def test_modefilter(mode, expected): +def test_modefilter( + mode: str, + expected: tuple[int, int] | tuple[tuple[int, int, int], tuple[int, int, int]], +) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -89,7 +93,13 @@ def test_modefilter(mode, expected): ("F", (0.0, 4.0, 8.0)), ), ) -def test_rankfilter(mode, expected): +def test_rankfilter( + mode: str, + expected: ( + tuple[float, float, float] + | tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]] + ), +) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -105,7 +115,7 @@ def test_rankfilter(mode, expected): @pytest.mark.parametrize( "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) ) -def test_rankfilter_error(filter): +def test_rankfilter_error(filter: ImageFilter.RankFilter) -> None: with pytest.raises(ValueError): im = Image.new("P", (3, 3), None) im.putdata(list(range(9))) @@ -116,27 +126,27 @@ def test_rankfilter_error(filter): im.filter(filter).getpixel((1, 1)) -def test_rankfilter_properties(): +def test_rankfilter_properties() -> None: rankfilter = ImageFilter.RankFilter(1, 2) assert rankfilter.size == 1 assert rankfilter.rank == 2 -def test_builtinfilter_p(): +def test_builtinfilter_p() -> None: builtin_filter = ImageFilter.BuiltinFilter() with pytest.raises(ValueError): builtin_filter.filter(hopper("P")) -def test_kernel_not_enough_coefficients(): +def test_kernel_not_enough_coefficients() -> None: with pytest.raises(ValueError): ImageFilter.Kernel((3, 3), (0, 0)) @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_3x3(mode): +def test_consistency_3x3(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: reference_name = "hopper_emboss" reference_name += "_I.png" if mode == "I" else ".bmp" @@ -162,7 +172,7 @@ def test_consistency_3x3(mode): @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_5x5(mode): +def test_consistency_5x5(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: reference_name = "hopper_emboss_more" reference_name += "_I.png" if mode == "I" else ".bmp" @@ -198,7 +208,7 @@ def test_consistency_5x5(mode): (2, -2), ), ) -def test_invalid_box_blur_filter(radius): +def test_invalid_box_blur_filter(radius: int | tuple[int, int]) -> None: with pytest.raises(ValueError): ImageFilter.BoxBlur(radius) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 017da499d..98c0ea0b4 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image @@ -7,7 +8,7 @@ from .helper import assert_image_equal, hopper @pytest.mark.parametrize("data_type", ("bytes", "memoryview")) -def test_sanity(data_type): +def test_sanity(data_type: str) -> None: im1 = hopper() data = im1.tobytes() diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index b3ca43bde..ea31a9de9 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -1,5 +1,7 @@ from __future__ import annotations + import warnings +from typing import Generator import pytest @@ -17,7 +19,7 @@ pytestmark = pytest.mark.skipif( @pytest.fixture -def test_images(): +def test_images() -> Generator[Image.Image, None, None]: ims = [ hopper(), Image.open("Tests/images/transparent.png"), @@ -30,7 +32,7 @@ def test_images(): im.close() -def roundtrip(expected): +def roundtrip(expected: Image.Image) -> None: # PIL -> Qt intermediate = expected.toqimage() # Qt -> PIL @@ -42,26 +44,26 @@ def roundtrip(expected): assert_image_equal(result, expected.convert("RGB")) -def test_sanity_1(test_images): +def test_sanity_1(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("1")) -def test_sanity_rgb(test_images): +def test_sanity_rgb(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("RGB")) -def test_sanity_rgba(test_images): +def test_sanity_rgba(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("RGBA")) -def test_sanity_l(test_images): +def test_sanity_l(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("L")) -def test_sanity_p(test_images): +def test_sanity_p(test_images: Generator[Image.Image, None, None]) -> None: for im in test_images: roundtrip(im.convert("P")) diff --git a/Tests/test_image_getbands.py b/Tests/test_image_getbands.py index e7701dbc4..887553fc0 100644 --- a/Tests/test_image_getbands.py +++ b/Tests/test_image_getbands.py @@ -1,8 +1,9 @@ from __future__ import annotations + from PIL import Image -def test_getbands(): +def test_getbands() -> None: assert Image.new("1", (1, 1)).getbands() == ("1",) assert Image.new("L", (1, 1)).getbands() == ("L",) assert Image.new("I", (1, 1)).getbands() == ("I",) diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index 9e792cfdf..18c6f6579 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image @@ -6,13 +7,13 @@ from PIL import Image from .helper import hopper -def test_sanity(): +def test_sanity() -> None: bbox = hopper().getbbox() assert isinstance(bbox, tuple) -def test_bbox(): - def check(im, fill_color): +def test_bbox() -> None: + def check(im: Image.Image, fill_color: int | tuple[int, ...]) -> None: assert im.getbbox() is None im.paste(fill_color, (10, 25, 90, 75)) @@ -33,8 +34,8 @@ def test_bbox(): check(im, 255) for mode in ("RGBA", "RGBa"): - for color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)): - im = Image.new(mode, (100, 100), color) + for rgba_color in ((0, 0, 0, 0), (127, 127, 127, 0), (255, 255, 255, 0)): + im = Image.new(mode, (100, 100), rgba_color) check(im, (255, 255, 255, 255)) for mode in ("La", "LA", "PA"): @@ -44,7 +45,7 @@ def test_bbox(): @pytest.mark.parametrize("mode", ("RGBA", "RGBa", "La", "LA", "PA")) -def test_bbox_alpha_only_false(mode): +def test_bbox_alpha_only_false(mode: str) -> None: im = Image.new(mode, (100, 100)) assert im.getbbox(alpha_only=False) is None diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index dea3a60a1..8f8870f4f 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -1,9 +1,10 @@ from __future__ import annotations + from .helper import hopper -def test_getcolors(): - def getcolors(mode, limit=None): +def test_getcolors() -> None: + def getcolors(mode: str, limit: int | None = None) -> int | None: im = hopper(mode) if limit: colors = im.getcolors(limit) @@ -38,7 +39,7 @@ def test_getcolors(): # -------------------------------------------------------------------- -def test_pack(): +def test_pack() -> None: # Pack problems for small tables (@PIL209) im = hopper().quantize(3).convert("RGB") diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index 873cc65bf..ac27400be 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,10 +1,11 @@ from __future__ import annotations + from PIL import Image from .helper import hopper -def test_sanity(): +def test_sanity() -> None: data = hopper().getdata() len(data) @@ -13,8 +14,8 @@ def test_sanity(): assert data[0] == (20, 20, 70) -def test_roundtrip(): - def getdata(mode): +def test_roundtrip() -> None: + def getdata(mode: str) -> tuple[float | tuple[int, ...], int, int]: 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_getextrema.py b/Tests/test_image_getextrema.py index b17c8a786..a5b974459 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -1,11 +1,12 @@ from __future__ import annotations + from PIL import Image from .helper import hopper -def test_extrema(): - def extrema(mode): +def test_extrema() -> None: + def extrema(mode: str) -> tuple[int, int] | tuple[tuple[int, int], ...]: return hopper(mode).getextrema() assert extrema("1") == (0, 255) @@ -19,7 +20,7 @@ def test_extrema(): assert extrema("I;16") == (1, 255) -def test_true_16(): +def test_true_16() -> None: with Image.open("Tests/images/16_bit_noise.tif") as im: assert im.mode == "I;16" extrema = im.getextrema() diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index e969c8164..9afa02b0a 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,8 +1,9 @@ from __future__ import annotations + from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() type_repr = repr(type(im.getim())) diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index a5be972d3..6a8f157fc 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -1,11 +1,12 @@ from __future__ import annotations + from PIL import Image from .helper import hopper -def test_palette(): - def palette(mode): +def test_palette() -> None: + def palette(mode: str) -> list[int] | None: p = hopper(mode).getpalette() if p: return p[:10] @@ -22,7 +23,7 @@ def test_palette(): assert palette("YCbCr") is None -def test_palette_rawmode(): +def test_palette_rawmode() -> None: im = Image.new("P", (1, 1)) im.putpalette((1, 2, 3)) diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index aa47be3b2..2b5a758ed 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -1,10 +1,11 @@ from __future__ import annotations + from PIL import Image from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() projection = im.getprojection() diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index 7ba2f10b7..dbd55d4c2 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -1,9 +1,10 @@ from __future__ import annotations + from .helper import hopper -def test_histogram(): - def histogram(mode): +def test_histogram() -> None: + def histogram(mode: str) -> tuple[int, int, int]: h = hopper(mode).histogram() return len(h), min(h), max(h) diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 17847c4fd..0605821e0 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -1,4 +1,5 @@ from __future__ import annotations + import logging import os @@ -9,14 +10,14 @@ from PIL import Image from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() pix = im.load() assert pix[0, 0] == (20, 20, 70) -def test_close(): +def test_close() -> None: im = Image.open("Tests/images/hopper.gif") im.close() with pytest.raises(ValueError): @@ -25,7 +26,7 @@ def test_close(): im.getpixel((0, 0)) -def test_close_after_load(caplog): +def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None: im = Image.open("Tests/images/hopper.gif") im.load() with caplog.at_level(logging.DEBUG): @@ -33,7 +34,7 @@ def test_close_after_load(caplog): assert len(caplog.records) == 0 -def test_contextmanager(): +def test_contextmanager() -> None: fn = None with Image.open("Tests/images/hopper.gif") as im: fn = im.fp.fileno() @@ -43,7 +44,7 @@ def test_contextmanager(): os.fstat(fn) -def test_contextmanager_non_exclusive_fp(): +def test_contextmanager_non_exclusive_fp() -> None: with open("Tests/images/hopper.gif", "rb") as fp: with Image.open(fp): pass diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index ad90d1250..8e94aafc5 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageMode @@ -6,7 +7,7 @@ from PIL import Image, ImageMode from .helper import hopper -def test_sanity(): +def test_sanity() -> None: with hopper() as im: im.mode @@ -68,7 +69,7 @@ def test_sanity(): ) def test_properties( mode, expected_base, expected_type, expected_bands, expected_band_names -): +) -> None: assert Image.getmodebase(mode) == expected_base assert Image.getmodetype(mode) == expected_type assert Image.getmodebands(mode) == expected_bands diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 0b87f6072..2966f724f 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image @@ -7,13 +8,11 @@ from .helper import CachedProperty, assert_image_equal class TestImagingPaste: - masks = {} size = 128 - def assert_9points_image(self, im, expected): - expected = [ - point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected - ] + def assert_9points_image( + self, im: Image.Image, expected: list[tuple[int, int, int, int]] + ) -> None: px = im.load() actual = [ px[0, 0], @@ -26,9 +25,17 @@ class TestImagingPaste: px[self.size // 2, self.size - 1], px[self.size - 1, self.size - 1], ] - assert actual == expected + assert actual == [ + point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected + ] - def assert_9points_paste(self, im, im2, mask, expected): + def assert_9points_paste( + self, + im: Image.Image, + im2: Image.Image, + mask: Image.Image, + expected: list[tuple[int, int, int, int]], + ) -> None: im3 = im.copy() im3.paste(im2, (0, 0), mask) self.assert_9points_image(im3, expected) @@ -38,7 +45,7 @@ class TestImagingPaste: self.assert_9points_image(im, expected) @CachedProperty - def mask_1(self): + def mask_1(self) -> Image.Image: mask = Image.new("1", (self.size, self.size)) px = mask.load() for y in range(mask.height): @@ -47,11 +54,11 @@ class TestImagingPaste: return mask @CachedProperty - def mask_L(self): + def mask_L(self) -> Image.Image: return self.gradient_L.transpose(Image.Transpose.ROTATE_270) @CachedProperty - def gradient_L(self): + def gradient_L(self) -> Image.Image: gradient = Image.new("L", (self.size, self.size)) px = gradient.load() for y in range(gradient.height): @@ -60,7 +67,7 @@ class TestImagingPaste: return gradient @CachedProperty - def gradient_RGB(self): + def gradient_RGB(self) -> Image.Image: return Image.merge( "RGB", [ @@ -71,7 +78,7 @@ class TestImagingPaste: ) @CachedProperty - def gradient_LA(self): + def gradient_LA(self) -> Image.Image: return Image.merge( "LA", [ @@ -81,7 +88,7 @@ class TestImagingPaste: ) @CachedProperty - def gradient_RGBA(self): + def gradient_RGBA(self) -> Image.Image: return Image.merge( "RGBA", [ @@ -93,7 +100,7 @@ class TestImagingPaste: ) @CachedProperty - def gradient_RGBa(self): + def gradient_RGBa(self) -> Image.Image: return Image.merge( "RGBa", [ @@ -105,7 +112,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_solid(self, mode): + def test_image_solid(self, mode: str) -> None: im = Image.new(mode, (200, 200), "red") im2 = getattr(self, "gradient_" + mode) @@ -115,7 +122,7 @@ class TestImagingPaste: assert_image_equal(im, im2) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_1(self, mode): + def test_image_mask_1(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -137,7 +144,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_L(self, mode): + def test_image_mask_L(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -159,7 +166,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_LA(self, mode): + def test_image_mask_LA(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -181,7 +188,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBA(self, mode): + def test_image_mask_RGBA(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -203,7 +210,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBa(self, mode): + def test_image_mask_RGBa(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -225,7 +232,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_solid(self, mode): + def test_color_solid(self, mode: str) -> None: im = Image.new(mode, (200, 200), "black") rect = (12, 23, 128 + 12, 128 + 23) @@ -238,7 +245,7 @@ class TestImagingPaste: assert sum(head[:255]) == 0 @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_1(self, mode): + def test_color_mask_1(self, mode: str) -> None: im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) color = (10, 20, 30, 40)[: len(mode)] @@ -260,7 +267,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_L(self, mode): + def test_color_mask_L(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -282,7 +289,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBA(self, mode): + def test_color_mask_RGBA(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -304,7 +311,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBa(self, mode): + def test_color_mask_RGBa(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -325,7 +332,7 @@ class TestImagingPaste: ], ) - def test_different_sizes(self): + def test_different_sizes(self) -> None: im = Image.new("RGB", (100, 100)) im2 = Image.new("RGB", (50, 50)) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index fce45ec4f..05f209351 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,10 +1,11 @@ from __future__ import annotations + import pytest from .helper import assert_image_equal, hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() with pytest.raises(ValueError): @@ -38,7 +39,7 @@ def test_sanity(): im.point(lambda x: x // 2) -def test_16bit_lut(): +def test_16bit_lut() -> None: """Tests for 16 bit -> 8 bit lut for converting I->L images see https://github.com/python-pillow/Pillow/issues/440 """ @@ -46,7 +47,7 @@ def test_16bit_lut(): im.point(list(range(256)) * 256, "L") -def test_f_lut(): +def test_f_lut() -> None: """Tests for floating point lut of 8bit gray image""" im = hopper("L") lut = [0.5 * float(x) for x in range(256)] @@ -57,7 +58,7 @@ def test_f_lut(): assert_image_equal(out.convert("L"), im.point(int_lut, "L")) -def test_f_mode(): +def test_f_mode() -> None: im = hopper("F") with pytest.raises(ValueError): im.point(None) diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index 0ba7e5919..2c92911d1 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -1,8 +1,9 @@ from __future__ import annotations + from PIL import Image -def test_interface(): +def test_interface() -> None: im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) assert im.getpixel((0, 0)) == (1, 2, 3, 0) @@ -16,7 +17,7 @@ def test_interface(): assert im.getpixel((0, 0)) == (1, 2, 3, 5) -def test_promote(): +def test_promote() -> None: im = Image.new("L", (1, 1), 1) assert im.getpixel((0, 0)) == 1 @@ -39,7 +40,7 @@ def test_promote(): assert im.getpixel((0, 0)) == (1, 2, 3, 4) -def test_readonly(): +def test_readonly() -> None: im = Image.new("RGB", (1, 1), (1, 2, 3)) im.readonly = 1 diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index d3cb13e2e..73145faac 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys from array import array @@ -9,7 +10,7 @@ from PIL import Image from .helper import assert_image_equal, hopper -def test_sanity(): +def test_sanity() -> None: im1 = hopper() data = list(im1.getdata()) @@ -28,9 +29,9 @@ def test_sanity(): assert_image_equal(im1, im2) -def test_long_integers(): +def test_long_integers() -> None: # see bug-200802-systemerror - def put(value): + def put(value: int) -> tuple[int, int, int, int]: im = Image.new("RGBA", (1, 1)) im.putdata([value]) return im.getpixel((0, 0)) @@ -45,19 +46,19 @@ def test_long_integers(): assert put(sys.maxsize) == (255, 255, 255, 127) -def test_pypy_performance(): +def test_pypy_performance() -> None: im = Image.new("L", (256, 256)) im.putdata(list(range(256)) * 256) -def test_mode_with_L_with_float(): +def test_mode_with_L_with_float() -> None: im = Image.new("L", (1, 1), 0) im.putdata([2.0]) assert im.getpixel((0, 0)) == 2 @pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) -def test_mode_i(mode): +def test_mode_i(mode: str) -> None: src = hopper("L") data = list(src.getdata()) im = Image.new(mode, src.size, 0) @@ -67,7 +68,7 @@ def test_mode_i(mode): assert list(im.getdata()) == target -def test_mode_F(): +def test_mode_F() -> None: src = hopper("L") data = list(src.getdata()) im = Image.new("F", src.size, 0) @@ -78,7 +79,7 @@ def test_mode_F(): @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) -def test_mode_BGR(mode): +def test_mode_BGR(mode: str) -> None: data = [(16, 32, 49), (32, 32, 98)] im = Image.new(mode, (1, 2)) im.putdata(data) @@ -86,7 +87,7 @@ def test_mode_BGR(mode): assert list(im.getdata()) == data -def test_array_B(): +def test_array_B() -> None: # shouldn't segfault # see https://github.com/python-pillow/Pillow/issues/1008 @@ -97,7 +98,7 @@ def test_array_B(): assert len(im.getdata()) == len(arr) -def test_array_F(): +def test_array_F() -> None: # shouldn't segfault # see https://github.com/python-pillow/Pillow/issues/1008 @@ -108,7 +109,7 @@ def test_array_F(): assert len(im.getdata()) == len(arr) -def test_not_flattened(): +def test_not_flattened() -> None: im = Image.new("L", (1, 1)) with pytest.raises(TypeError): im.putdata([[0]]) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index de2d90242..cc7cf58f0 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImagePalette @@ -6,8 +7,8 @@ from PIL import Image, ImagePalette from .helper import assert_image_equal, assert_image_equal_tofile, hopper -def test_putpalette(): - def palette(mode): +def test_putpalette() -> None: + def palette(mode: str) -> str | tuple[str, list[int]]: im = hopper(mode).copy() im.putpalette(list(range(256)) * 3) p = im.getpalette() @@ -42,7 +43,7 @@ def test_putpalette(): im.putpalette(list(range(256)) * 3) -def test_imagepalette(): +def test_imagepalette() -> None: im = hopper("P") im.putpalette(ImagePalette.negative()) assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_negative.png") @@ -56,7 +57,7 @@ def test_imagepalette(): assert_image_equal_tofile(im.convert("RGB"), "Tests/images/palette_wedge.png") -def test_putpalette_with_alpha_values(): +def test_putpalette_with_alpha_values() -> None: with Image.open("Tests/images/transparent.gif") as im: expected = im.convert("RGBA") @@ -80,19 +81,19 @@ def test_putpalette_with_alpha_values(): ("RGBAX", (1, 2, 3, 4, 0)), ), ) -def test_rgba_palette(mode, palette): +def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: 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} -def test_empty_palette(): +def test_empty_palette() -> None: im = Image.new("P", (1, 1)) assert im.getpalette() == [] -def test_undefined_palette_index(): +def test_undefined_palette_index() -> None: im = Image.new("P", (1, 1), 3) im.putpalette((1, 2, 3)) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 0) diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 54c567aae..873a9bb5d 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from packaging.version import parse as parse_version @@ -7,7 +8,7 @@ from PIL import Image, features from .helper import assert_image_similar, hopper, is_ppc64le, skip_unless_feature -def test_sanity(): +def test_sanity() -> None: image = hopper() converted = image.quantize() assert converted.mode == "P" @@ -20,7 +21,7 @@ def test_sanity(): @skip_unless_feature("libimagequant") -def test_libimagequant_quantize(): +def test_libimagequant_quantize() -> None: image = hopper() if is_ppc64le(): libimagequant = parse_version(features.version_feature("libimagequant")) @@ -32,7 +33,7 @@ def test_libimagequant_quantize(): assert len(converted.getcolors()) == 100 -def test_octree_quantize(): +def test_octree_quantize() -> None: image = hopper() converted = image.quantize(100, Image.Quantize.FASTOCTREE) assert converted.mode == "P" @@ -40,7 +41,7 @@ def test_octree_quantize(): assert len(converted.getcolors()) == 100 -def test_rgba_quantize(): +def test_rgba_quantize() -> None: image = hopper("RGBA") with pytest.raises(ValueError): image.quantize(method=0) @@ -48,7 +49,7 @@ def test_rgba_quantize(): assert image.quantize().convert().mode == "RGBA" -def test_quantize(): +def test_quantize() -> None: with Image.open("Tests/images/caption_6_33_22.png") as image: image = image.convert("RGB") converted = image.quantize() @@ -56,7 +57,7 @@ def test_quantize(): assert_image_similar(converted.convert("RGB"), image, 1) -def test_quantize_no_dither(): +def test_quantize_no_dither() -> None: image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: palette = palette.convert("P") @@ -66,7 +67,7 @@ def test_quantize_no_dither(): assert converted.palette.palette == palette.palette.palette -def test_quantize_no_dither2(): +def test_quantize_no_dither2() -> None: im = Image.new("RGB", (9, 1)) im.putdata([(p,) * 3 for p in range(0, 36, 4)]) @@ -82,7 +83,7 @@ def test_quantize_no_dither2(): assert px[x, 0] == (0 if x < 5 else 1) -def test_quantize_dither_diff(): +def test_quantize_dither_diff() -> None: image = hopper() with Image.open("Tests/images/caption_6_33_22.png") as palette: palette = palette.convert("P") @@ -93,14 +94,14 @@ def test_quantize_dither_diff(): assert dither.tobytes() != nodither.tobytes() -def test_colors(): +def test_colors() -> None: im = hopper() colors = 2 converted = im.quantize(colors) assert len(converted.palette.palette) == colors * len("RGB") -def test_transparent_colors_equal(): +def test_transparent_colors_equal() -> None: im = Image.new("RGBA", (1, 2), (0, 0, 0, 0)) px = im.load() px[0, 1] = (255, 255, 255, 0) @@ -119,7 +120,7 @@ def test_transparent_colors_equal(): (Image.Quantize.FASTOCTREE, (0, 0, 0, 0)), ), ) -def test_palette(method, color): +def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None: im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color) converted = im.quantize(method=method) @@ -127,7 +128,7 @@ def test_palette(method, color): assert converted_px[0, 0] == converted.palette.colors[color] -def test_small_palette(): +def test_small_palette() -> None: # Arrange im = hopper() diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index a4d0f5107..33b33d6b7 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageMath, ImageMode @@ -47,7 +48,7 @@ gradients_image.load() ((1, 3), (10, 4)), ), ) -def test_args_factor(size, expected): +def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(size).size @@ -55,7 +56,7 @@ def test_args_factor(size, expected): @pytest.mark.parametrize( "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) ) -def test_args_factor_error(size, expected_error): +def test_args_factor_error(size: float | tuple[int, int], expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(size) @@ -68,7 +69,7 @@ def test_args_factor_error(size, expected_error): ((5, 5, 6, 6), (1, 1)), ), ) -def test_args_box(size, expected): +def test_args_box(size: tuple[int, int, int, int], expected: tuple[int, int]) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(2, size).size @@ -85,20 +86,20 @@ def test_args_box(size, expected): ((5, 0, 5, 10), ValueError), ), ) -def test_args_box_error(size, expected_error): +def test_args_box_error(size: str | tuple[int, int, int, int], expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(2, size).size @pytest.mark.parametrize("mode", ("P", "1", "I;16")) -def test_unsupported_modes(mode): +def test_unsupported_modes(mode: str) -> None: im = Image.new("P", (10, 10)) with pytest.raises(ValueError): im.reduce(3) -def get_image(mode): +def get_image(mode: str) -> Image.Image: mode_info = ImageMode.getmode(mode) if mode_info.basetype == "L": bands = [gradients_image] @@ -118,14 +119,19 @@ def get_image(mode): return im.crop((0, 0, im.width, im.height - 5)) -def compare_reduce_with_box(im, factor): +def compare_reduce_with_box(im: Image.Image, factor: int | tuple[int, int]) -> None: box = (11, 13, 146, 164) reduced = im.reduce(factor, box=box) reference = im.crop(box).reduce(factor) assert reduced == reference -def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1): +def compare_reduce_with_reference( + im: Image.Image, + factor: int | tuple[int, int], + average_diff: float = 0.4, + max_diff: int = 1, +) -> None: """Image.reduce() should look very similar to Image.resize(BOX). A reference image is compiled from a large source area @@ -170,7 +176,9 @@ def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1): assert_compare_images(reduced, reference, average_diff, max_diff) -def assert_compare_images(a, b, max_average_diff, max_diff=255): +def assert_compare_images( + a: Image.Image, b: Image.Image, max_average_diff: float, max_diff: int = 255 +) -> None: assert a.mode == b.mode, f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, f"got size {repr(a.size)}, expected {repr(b.size)}" @@ -198,20 +206,20 @@ def assert_compare_images(a, b, max_average_diff, max_diff=255): @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_L(factor): +def test_mode_L(factor: int | tuple[int, int]) -> None: im = get_image("L") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA(factor): +def test_mode_LA(factor: int | tuple[int, int]) -> None: im = get_image("LA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA_opaque(factor): +def test_mode_LA_opaque(factor: int | tuple[int, int]) -> None: im = get_image("LA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -220,27 +228,27 @@ def test_mode_LA_opaque(factor): @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_La(factor): +def test_mode_La(factor: int | tuple[int, int]) -> None: im = get_image("La") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGB(factor): +def test_mode_RGB(factor: int | tuple[int, int]) -> None: im = get_image("RGB") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA(factor): +def test_mode_RGBA(factor: int | tuple[int, int]) -> None: im = get_image("RGBA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA_opaque(factor): +def test_mode_RGBA_opaque(factor: int | tuple[int, int]) -> None: im = get_image("RGBA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -249,27 +257,27 @@ def test_mode_RGBA_opaque(factor): @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBa(factor): +def test_mode_RGBa(factor: int | tuple[int, int]) -> None: im = get_image("RGBa") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_I(factor): +def test_mode_I(factor: int | tuple[int, int]) -> None: im = get_image("I") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_F(factor): +def test_mode_F(factor: int | tuple[int, int]) -> None: im = get_image("F") compare_reduce_with_reference(im, factor, 0, 0) compare_reduce_with_box(im, factor) @skip_unless_feature("jpg_2000") -def test_jpeg2k(): +def test_jpeg2k() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert im.reduce(2).size == (320, 240) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index b4bf6c8df..7090ff9cd 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,5 +1,7 @@ from __future__ import annotations + from contextlib import contextmanager +from typing import Generator import pytest @@ -15,7 +17,7 @@ from .helper import ( class TestImagingResampleVulnerability: # see https://github.com/python-pillow/Pillow/issues/1710 - def test_overflow(self): + def test_overflow(self) -> None: im = hopper("L") size_too_large = 0x100000008 // 4 size_normal = 1000 # unimportant @@ -27,7 +29,7 @@ class TestImagingResampleVulnerability: # any resampling filter will do here im.im.resize((xsize, ysize), Image.Resampling.BILINEAR) - def test_invalid_size(self): + def test_invalid_size(self) -> None: im = hopper() # Should not crash @@ -39,7 +41,7 @@ class TestImagingResampleVulnerability: with pytest.raises(ValueError): im.resize((100, -100)) - def test_modify_after_resizing(self): + def test_modify_after_resizing(self) -> None: im = hopper("RGB") # get copy with same size copy = im.resize(im.size) @@ -50,7 +52,7 @@ class TestImagingResampleVulnerability: class TestImagingCoreResampleAccuracy: - def make_case(self, mode, size, color): + def make_case(self, mode: str, size: tuple[int, int], color: int) -> Image.Image: """Makes a sample image with two dark and two bright squares. For example: e0 e0 1f 1f @@ -65,7 +67,7 @@ class TestImagingCoreResampleAccuracy: return Image.merge(mode, [case] * len(mode)) - def make_sample(self, data, size): + def make_sample(self, data: str, size: tuple[int, int]) -> Image.Image: """Restores a sample image from given data string which contains hex-encoded pixels from the top left fourth of a sample. """ @@ -82,7 +84,7 @@ class TestImagingCoreResampleAccuracy: s_px[size[0] - x - 1, y] = 255 - val return sample - def check_case(self, case, sample): + def check_case(self, case: Image.Image, sample: Image.Image) -> None: s_px = sample.load() c_px = case.load() for y in range(case.size[1]): @@ -94,7 +96,7 @@ class TestImagingCoreResampleAccuracy: ) assert s_px[x, y] == c_px[x, y], message - def serialize_image(self, image): + def serialize_image(self, image: Image.Image) -> str: s_px = image.load() return "\n".join( " ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0])) @@ -102,7 +104,7 @@ class TestImagingCoreResampleAccuracy: ) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_box(self, mode): + def test_reduce_box(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -113,7 +115,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bilinear(self, mode): + def test_reduce_bilinear(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -124,7 +126,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_hamming(self, mode): + def test_reduce_hamming(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -135,7 +137,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bicubic(self, mode): + def test_reduce_bicubic(self, mode: str) -> None: case = self.make_case(mode, (12, 12), 0xE1) case = case.resize((6, 6), Image.Resampling.BICUBIC) # fmt: off @@ -147,7 +149,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (6, 6))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_lanczos(self, mode): + def test_reduce_lanczos(self, mode: str) -> None: case = self.make_case(mode, (16, 16), 0xE1) case = case.resize((8, 8), Image.Resampling.LANCZOS) # fmt: off @@ -160,7 +162,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_box(self, mode): + def test_enlarge_box(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -171,7 +173,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bilinear(self, mode): + def test_enlarge_bilinear(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -182,7 +184,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_hamming(self, mode): + def test_enlarge_hamming(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -193,7 +195,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bicubic(self, mode): + def test_enlarge_bicubic(self, mode: str) -> None: case = self.make_case(mode, (4, 4), 0xE1) case = case.resize((8, 8), Image.Resampling.BICUBIC) # fmt: off @@ -206,7 +208,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_lanczos(self, mode): + def test_enlarge_lanczos(self, mode: str) -> None: case = self.make_case(mode, (6, 6), 0xE1) case = case.resize((12, 12), Image.Resampling.LANCZOS) data = ( @@ -220,7 +222,7 @@ class TestImagingCoreResampleAccuracy: for channel in case.split(): self.check_case(channel, self.make_sample(data, (12, 12))) - def test_box_filter_correct_range(self): + def test_box_filter_correct_range(self) -> None: im = Image.new("RGB", (8, 8), "#1688ff").resize( (100, 100), Image.Resampling.BOX ) @@ -229,11 +231,13 @@ class TestImagingCoreResampleAccuracy: class TestCoreResampleConsistency: - def make_case(self, mode, fill): + def make_case( + self, mode: str, fill: tuple[int, int, int] | float + ) -> tuple[Image.Image, tuple[int, ...]]: im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] - def run_case(self, case): + def run_case(self, case: tuple[Image.Image, Image.Image]) -> None: channel, color = case px = channel.load() for x in range(channel.size[0]): @@ -242,7 +246,7 @@ class TestCoreResampleConsistency: message = f"{px[x, y]} != {color} for pixel {(x, y)}" assert px[x, y] == color, message - def test_8u(self): + def test_8u(self) -> None: im, color = self.make_case("RGB", (0, 64, 255)) r, g, b = im.split() self.run_case((r, color[0])) @@ -250,13 +254,13 @@ class TestCoreResampleConsistency: self.run_case((b, color[2])) self.run_case(self.make_case("L", 12)) - def test_32i(self): + def test_32i(self) -> None: self.run_case(self.make_case("I", 12)) self.run_case(self.make_case("I", 0x7FFFFFFF)) self.run_case(self.make_case("I", -12)) self.run_case(self.make_case("I", -1 << 31)) - def test_32f(self): + def test_32f(self) -> None: self.run_case(self.make_case("F", 1)) self.run_case(self.make_case("F", 3.40282306074e38)) self.run_case(self.make_case("F", 1.175494e-38)) @@ -264,7 +268,7 @@ class TestCoreResampleConsistency: class TestCoreResampleAlphaCorrect: - def make_levels_case(self, mode): + def make_levels_case(self, mode: str) -> Image.Image: i = Image.new(mode, (256, 16)) px = i.load() for y in range(i.size[1]): @@ -274,7 +278,7 @@ class TestCoreResampleAlphaCorrect: px[x, y] = tuple(pix) return i - def run_levels_case(self, i): + def run_levels_case(self, i: Image.Image) -> None: px = i.load() for y in range(i.size[1]): used_colors = {px[x, y][0] for x in range(i.size[0])} @@ -284,7 +288,7 @@ class TestCoreResampleAlphaCorrect: ) @pytest.mark.xfail(reason="Current implementation isn't precise enough") - def test_levels_rgba(self): + def test_levels_rgba(self) -> None: case = self.make_levels_case("RGBA") self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) @@ -293,7 +297,7 @@ class TestCoreResampleAlphaCorrect: 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): + def test_levels_la(self) -> None: case = self.make_levels_case("LA") self.run_levels_case(case.resize((512, 32), Image.Resampling.BOX)) self.run_levels_case(case.resize((512, 32), Image.Resampling.BILINEAR)) @@ -301,7 +305,9 @@ class TestCoreResampleAlphaCorrect: 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): + def make_dirty_case( + self, mode: str, clean_pixel: tuple[int, ...], dirty_pixel: tuple[int, ...] + ) -> Image.Image: i = Image.new(mode, (64, 64), dirty_pixel) px = i.load() xdiv4 = i.size[0] // 4 @@ -311,7 +317,7 @@ class TestCoreResampleAlphaCorrect: px[x + xdiv4, y + ydiv4] = clean_pixel return i - def run_dirty_case(self, i, clean_pixel): + def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None: px = i.load() for y in range(i.size[1]): for x in range(i.size[0]): @@ -322,7 +328,7 @@ class TestCoreResampleAlphaCorrect: ) assert px[x, y][:3] == clean_pixel, message - def test_dirty_pixels_rgba(self): + def test_dirty_pixels_rgba(self) -> None: case = self.make_dirty_case("RGBA", (255, 255, 0, 128), (0, 0, 255, 0)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255, 255, 0)) self.run_dirty_case( @@ -338,7 +344,7 @@ class TestCoreResampleAlphaCorrect: case.resize((20, 20), Image.Resampling.LANCZOS), (255, 255, 0) ) - def test_dirty_pixels_la(self): + def test_dirty_pixels_la(self) -> None: case = self.make_dirty_case("LA", (255, 128), (0, 0)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BOX), (255,)) self.run_dirty_case(case.resize((20, 20), Image.Resampling.BILINEAR), (255,)) @@ -349,27 +355,27 @@ class TestCoreResampleAlphaCorrect: class TestCoreResamplePasses: @contextmanager - def count(self, diff): + def count(self, diff: int) -> Generator[None, None, None]: count = Image.core.get_stats()["new_count"] yield assert Image.core.get_stats()["new_count"] - count == diff - def test_horizontal(self): + def test_horizontal(self) -> None: im = hopper("L") with self.count(1): im.resize((im.size[0] - 10, im.size[1]), Image.Resampling.BILINEAR) - def test_vertical(self): + def test_vertical(self) -> None: im = hopper("L") with self.count(1): im.resize((im.size[0], im.size[1] - 10), Image.Resampling.BILINEAR) - def test_both(self): + def test_both(self) -> None: im = hopper("L") with self.count(2): im.resize((im.size[0] - 10, im.size[1] - 10), Image.Resampling.BILINEAR) - def test_box_horizontal(self): + def test_box_horizontal(self) -> None: im = hopper("L") box = (20, 0, im.size[0] - 20, im.size[1]) with self.count(1): @@ -379,7 +385,7 @@ class TestCoreResamplePasses: cropped = im.crop(box).resize(im.size, Image.Resampling.BILINEAR) assert_image_similar(with_box, cropped, 0.1) - def test_box_vertical(self): + def test_box_vertical(self) -> None: im = hopper("L") box = (0, 20, im.size[0], im.size[1] - 20) with self.count(1): @@ -391,7 +397,7 @@ class TestCoreResamplePasses: class TestCoreResampleCoefficients: - def test_reduce(self): + def test_reduce(self) -> None: test_color = 254 for size in range(400000, 400010, 2): @@ -403,7 +409,7 @@ class TestCoreResampleCoefficients: if px[2, 0] != test_color // 2: assert test_color // 2 == px[2, 0] - def test_nonzero_coefficients(self): + def test_non_zero_coefficients(self) -> None: # 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)) @@ -431,7 +437,7 @@ class TestCoreResampleBox: Image.Resampling.LANCZOS, ), ) - def test_wrong_arguments(self, resample): + def test_wrong_arguments(self, resample: Image.Resampling) -> None: im = hopper() im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height)) @@ -458,8 +464,12 @@ class TestCoreResampleBox: with pytest.raises(ValueError, match="can't exceed"): im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) - def resize_tiled(self, im, dst_size, xtiles, ytiles): - def split_range(size, tiles): + def resize_tiled( + self, im: Image.Image, dst_size: tuple[int, int], xtiles: int, ytiles: int + ) -> Image.Image: + def split_range( + size: int, tiles: int + ) -> Generator[tuple[int, int], None, None]: scale = size / tiles for i in range(tiles): yield int(round(scale * i)), int(round(scale * (i + 1))) @@ -477,7 +487,7 @@ class TestCoreResampleBox: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_tiles(self): + def test_tiles(self) -> None: with Image.open("Tests/images/flower.jpg") as im: assert im.size == (480, 360) dst_size = (251, 188) @@ -490,7 +500,7 @@ class TestCoreResampleBox: @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) - def test_subsample(self): + def test_subsample(self) -> None: # This test shows advantages of the subpixel resizing # after supersampling (e.g. during JPEG decoding). with Image.open("Tests/images/flower.jpg") as im: @@ -517,14 +527,14 @@ class TestCoreResampleBox: @pytest.mark.parametrize( "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) ) - def test_formats(self, mode, resample): + def test_formats(self, mode: str, resample: Image.Resampling) -> None: im = hopper(mode) box = (20, 20, im.size[0] - 20, im.size[1] - 20) with_box = im.resize((32, 32), resample, box) cropped = im.crop(box).resize((32, 32), resample) assert_image_similar(cropped, with_box, 0.4) - def test_passthrough(self): + def test_passthrough(self) -> None: # When no resize is required im = hopper() @@ -538,7 +548,7 @@ class TestCoreResampleBox: assert res.size == size assert_image_equal(res, im.crop(box), f">>> {size} {box}") - def test_no_passthrough(self): + def test_no_passthrough(self) -> None: # When resize is required im = hopper() @@ -557,7 +567,7 @@ class TestCoreResampleBox: @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_horizontal(self, flt): + def test_skip_horizontal(self, flt: Image.Resampling) -> None: # Can skip resize for one dimension im = hopper() @@ -580,7 +590,7 @@ class TestCoreResampleBox: @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_vertical(self, flt): + def test_skip_vertical(self, flt: Image.Resampling) -> None: # Can skip resize for one dimension im = hopper() diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 0d3b43ee2..a64e4a846 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -1,8 +1,12 @@ """ Tests for resize functionality. """ + from __future__ import annotations + from itertools import permutations +from pathlib import Path +from typing import Generator import pytest @@ -18,7 +22,9 @@ from .helper import ( class TestImagingCoreResize: - def resize(self, im, size, f): + def resize( + self, im: Image.Image, size: tuple[int, int], f: Image.Resampling + ) -> Image.Image: # Image class independent version of resize. im.load() return im._new(im.im.resize(size, f)) @@ -26,14 +32,14 @@ class TestImagingCoreResize: @pytest.mark.parametrize( "mode", ("1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", "I;16") ) - def test_nearest_mode(self, mode): + def test_nearest_mode(self, mode: str) -> None: im = hopper(mode) r = self.resize(im, (15, 12), Image.Resampling.NEAREST) assert r.mode == mode assert r.size == (15, 12) assert r.im.bands == im.im.bands - def test_convolution_modes(self): + def test_convolution_modes(self) -> None: with pytest.raises(ValueError): self.resize(hopper("1"), (15, 12), Image.Resampling.BILINEAR) with pytest.raises(ValueError): @@ -58,7 +64,7 @@ class TestImagingCoreResize: Image.Resampling.LANCZOS, ), ) - def test_reduce_filters(self, resample): + def test_reduce_filters(self, resample: Image.Resampling) -> None: r = self.resize(hopper("RGB"), (15, 12), resample) assert r.mode == "RGB" assert r.size == (15, 12) @@ -74,7 +80,7 @@ class TestImagingCoreResize: Image.Resampling.LANCZOS, ), ) - def test_enlarge_filters(self, resample): + def test_enlarge_filters(self, resample: Image.Resampling) -> None: r = self.resize(hopper("RGB"), (212, 195), resample) assert r.mode == "RGB" assert r.size == (212, 195) @@ -98,7 +104,9 @@ class TestImagingCoreResize: ("LA", ("filled", "dirty")), ), ) - def test_endianness(self, resample, mode, channels_set): + def test_endianness( + self, resample: Image.Resampling, mode: str, channels_set: tuple[str, ...] + ) -> None: # Make an image with one colored pixel, in one channel. # When resized, that channel should be the same as a GS image. # Other channels should be unaffected. @@ -138,17 +146,17 @@ class TestImagingCoreResize: Image.Resampling.LANCZOS, ), ) - def test_enlarge_zero(self, resample): + def test_enlarge_zero(self, resample: Image.Resampling) -> None: r = self.resize(Image.new("RGB", (0, 0), "white"), (212, 195), resample) assert r.mode == "RGB" assert r.size == (212, 195) assert r.getdata()[0] == (0, 0, 0) - def test_unknown_filter(self): + def test_unknown_filter(self) -> None: with pytest.raises(ValueError): self.resize(hopper(), (10, 10), 9) - def test_cross_platform(self, tmp_path): + def test_cross_platform(self, tmp_path: Path) -> None: # This test is intended for only check for consistent behaviour across # platforms. So if a future Pillow change requires that the test file # be updated, that is okay. @@ -161,7 +169,7 @@ class TestImagingCoreResize: @pytest.fixture -def gradients_image(): +def gradients_image() -> Generator[Image.Image, None, None]: with Image.open("Tests/images/radial_gradients.png") as im: im.load() try: @@ -171,7 +179,7 @@ def gradients_image(): class TestReducingGapResize: - def test_reducing_gap_values(self, gradients_image): + def test_reducing_gap_values(self, gradients_image: Image.Image) -> None: ref = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, reducing_gap=None ) @@ -190,7 +198,12 @@ class TestReducingGapResize: "box, epsilon", ((None, 4), ((1.1, 2.2, 510.8, 510.9), 4), ((3, 10, 410, 256), 10)), ) - def test_reducing_gap_1(self, gradients_image, box, epsilon): + def test_reducing_gap_1( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=1.0 @@ -205,7 +218,12 @@ class TestReducingGapResize: "box, epsilon", ((None, 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5), ((3, 10, 410, 256), 1)), ) - def test_reducing_gap_2(self, gradients_image, box, epsilon): + def test_reducing_gap_2( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=2.0 @@ -220,7 +238,12 @@ class TestReducingGapResize: "box, epsilon", ((None, 1), ((1.1, 2.2, 510.8, 510.9), 1), ((3, 10, 410, 256), 0.5)), ) - def test_reducing_gap_3(self, gradients_image, box, epsilon): + def test_reducing_gap_3( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=3.0 @@ -232,7 +255,9 @@ class TestReducingGapResize: assert_image_similar(ref, im, epsilon) @pytest.mark.parametrize("box", (None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256))) - def test_reducing_gap_8(self, gradients_image, box): + def test_reducing_gap_8( + self, gradients_image: Image.Image, box: tuple[float, float, float, float] + ) -> None: ref = gradients_image.resize((52, 34), Image.Resampling.BICUBIC, box=box) im = gradients_image.resize( (52, 34), Image.Resampling.BICUBIC, box=box, reducing_gap=8.0 @@ -244,7 +269,12 @@ class TestReducingGapResize: "box, epsilon", (((0, 0, 512, 512), 5.5), ((0.9, 1.7, 128, 128), 9.5)), ) - def test_box_filter(self, gradients_image, box, epsilon): + def test_box_filter( + self, + gradients_image: Image.Image, + box: tuple[float, float, float, float], + epsilon: float, + ) -> None: 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 @@ -254,8 +284,8 @@ class TestReducingGapResize: class TestImageResize: - def test_resize(self): - def resize(mode, size): + def test_resize(self) -> None: + def resize(mode: str, size: tuple[int, int]) -> None: out = hopper(mode).resize(size) assert out.mode == mode assert out.size == size @@ -270,7 +300,7 @@ class TestImageResize: im.resize((10, 10), "unknown") @skip_unless_feature("libtiff") - def test_load_first(self): + def test_load_first(self) -> None: # 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: @@ -278,13 +308,13 @@ class TestImageResize: assert im.size == (64, 64) @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F")) - def test_default_filter_bicubic(self, mode): + def test_default_filter_bicubic(self, mode: str) -> None: im = hopper(mode) assert im.resize((20, 20), Image.Resampling.BICUBIC) == im.resize((20, 20)) @pytest.mark.parametrize( "mode", ("1", "P", "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16") ) - def test_default_filter_nearest(self, mode): + def test_default_filter_nearest(self, mode: str) -> None: im = hopper(mode) 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 0931aa32d..c10c96da6 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image @@ -11,7 +12,13 @@ from .helper import ( ) -def rotate(im, mode, angle, center=None, translate=None): +def rotate( + im: Image.Image, + mode: str, + angle: int, + center: tuple[int, int] | None = None, + translate: tuple[int, int] | None = None, +) -> None: out = im.rotate(angle, center=center, translate=translate) assert out.mode == mode assert out.size == im.size # default rotate clips output @@ -26,13 +33,13 @@ def rotate(im, mode, angle, center=None, translate=None): @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_mode(mode): +def test_mode(mode: str) -> None: im = hopper(mode) rotate(im, mode, 45) @pytest.mark.parametrize("angle", (0, 90, 180, 270)) -def test_angle(angle): +def test_angle(angle: int) -> None: with Image.open("Tests/images/test-card.png") as im: rotate(im, im.mode, angle) @@ -41,12 +48,12 @@ def test_angle(angle): @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) -def test_zero(angle): +def test_zero(angle: int) -> None: im = Image.new("RGB", (0, 0)) rotate(im, im.mode, angle) -def test_resample(): +def test_resample() -> None: # Target image creation, inspected by eye. # >>> im = Image.open('Tests/images/hopper.ppm') # >>> im = im.rotate(45, resample=Image.Resampling.BICUBIC, expand=True) @@ -63,7 +70,7 @@ def test_resample(): assert_image_similar(im, target, epsilon) -def test_center_0(): +def test_center_0() -> None: im = hopper() im = im.rotate(45, center=(0, 0), resample=Image.Resampling.BICUBIC) @@ -74,7 +81,7 @@ def test_center_0(): assert_image_similar(im, target, 15) -def test_center_14(): +def test_center_14() -> None: im = hopper() im = im.rotate(45, center=(14, 14), resample=Image.Resampling.BICUBIC) @@ -85,7 +92,7 @@ def test_center_14(): assert_image_similar(im, target, 10) -def test_translate(): +def test_translate() -> None: im = hopper() with Image.open("Tests/images/hopper_45.png") as target: target_origin = (target.size[1] / 2 - 64) - 5 @@ -98,7 +105,7 @@ def test_translate(): assert_image_similar(im, target, 1) -def test_fastpath_center(): +def test_fastpath_center() -> None: # if the center is -1,-1 and we rotate by 90<=x<=270 the # resulting image should be black for angle in (90, 180, 270): @@ -106,7 +113,7 @@ def test_fastpath_center(): assert_image_equal(im, Image.new("RGB", im.size, "black")) -def test_fastpath_translate(): +def test_fastpath_translate() -> None: # if we post-translate by -128 # resulting image should be black for angle in (0, 90, 180, 270): @@ -114,26 +121,26 @@ def test_fastpath_translate(): assert_image_equal(im, Image.new("RGB", im.size, "black")) -def test_center(): +def test_center() -> None: im = hopper() rotate(im, im.mode, 45, center=(0, 0)) rotate(im, im.mode, 45, translate=(im.size[0] / 2, 0)) rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0] / 2, 0)) -def test_rotate_no_fill(): +def test_rotate_no_fill() -> None: im = Image.new("RGB", (100, 100), "green") im = im.rotate(45) assert_image_equal_tofile(im, "Tests/images/rotate_45_no_fill.png") -def test_rotate_with_fill(): +def test_rotate_with_fill() -> None: im = Image.new("RGB", (100, 100), "green") im = im.rotate(45, fillcolor="white") assert_image_equal_tofile(im, "Tests/images/rotate_45_with_fill.png") -def test_alpha_rotate_no_fill(): +def test_alpha_rotate_no_fill() -> None: # Alpha images are handled differently internally im = Image.new("RGBA", (10, 10), "green") im = im.rotate(45, expand=1) @@ -141,7 +148,7 @@ def test_alpha_rotate_no_fill(): assert corner == (0, 0, 0, 0) -def test_alpha_rotate_with_fill(): +def test_alpha_rotate_with_fill() -> None: # Alpha images are handled differently internally im = Image.new("RGBA", (10, 10), "green") im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255)) diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 707508250..3385f81f5 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image, features @@ -6,8 +9,8 @@ from PIL import Image, features from .helper import assert_image_equal, hopper -def test_split(): - def split(mode): +def test_split() -> None: + def split(mode: str) -> list[tuple[str, int, int]]: layers = hopper(mode).split() return [(i.mode, i.size[0], i.size[1]) for i in layers] @@ -35,18 +38,18 @@ def test_split(): @pytest.mark.parametrize( "mode", ("1", "L", "I", "F", "P", "RGB", "RGBA", "CMYK", "YCbCr") ) -def test_split_merge(mode): +def test_split_merge(mode: str) -> None: expected = Image.merge(mode, hopper(mode).split()) assert_image_equal(hopper(mode), expected) -def test_split_open(tmp_path): +def test_split_open(tmp_path: Path) -> None: if features.check("zlib"): test_file = str(tmp_path / "temp.png") else: test_file = str(tmp_path / "temp.pcx") - def split_open(mode): + def split_open(mode: str) -> int: hopper(mode).save(test_file) with Image.open(test_file) as im: return len(im.split()) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 9e6796ca2..2ca1d2cfc 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image @@ -13,14 +14,14 @@ from .helper import ( ) -def test_sanity(): +def test_sanity() -> None: im = hopper() assert im.thumbnail((100, 100)) is None assert im.size == (100, 100) -def test_aspect(): +def test_aspect() -> None: im = Image.new("L", (128, 128)) im.thumbnail((100, 100)) assert im.size == (100, 100) @@ -66,19 +67,19 @@ def test_aspect(): assert im.size == (75, 23) # ratio is 3.260869565217 -def test_division_by_zero(): +def test_division_by_zero() -> None: im = Image.new("L", (200, 2)) im.thumbnail((75, 75)) assert im.size == (75, 1) -def test_float(): +def test_float() -> None: im = Image.new("L", (128, 128)) im.thumbnail((99.9, 99.9)) assert im.size == (99, 99) -def test_no_resize(): +def test_no_resize() -> None: # Check that draft() can resize the image to the destination size with Image.open("Tests/images/hopper.jpg") as im: im.draft(None, (64, 64)) @@ -91,7 +92,7 @@ def test_no_resize(): @skip_unless_feature("libtiff") -def test_load_first(): +def test_load_first() -> None: # 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: @@ -105,12 +106,12 @@ def test_load_first(): assert im.size == (590, 88) -def test_load_first_unless_jpeg(): +def test_load_first_unless_jpeg() -> None: # Test that thumbnail() still uses draft() for JPEG with Image.open("Tests/images/hopper.jpg") as im: draft = im.draft - def im_draft(mode, size): + def im_draft(mode: str, size: tuple[int, int]): result = draft(mode, size) assert result is not None @@ -123,7 +124,7 @@ def test_load_first_unless_jpeg(): # valgrind test is failing with memory allocated in libjpeg @pytest.mark.valgrind_known_error(reason="Known Failing") -def test_DCT_scaling_edges(): +def test_DCT_scaling_edges() -> None: # Make an image with red borders and size (N * 8) + 1 to cross DCT grid im = Image.new("RGB", (257, 257), "red") im.paste(Image.new("RGB", (235, 235)), (11, 11)) @@ -137,7 +138,7 @@ def test_DCT_scaling_edges(): assert_image_similar(thumb, ref, 1.5) -def test_reducing_gap_values(): +def test_reducing_gap_values() -> None: im = hopper() im.thumbnail((18, 18), Image.Resampling.BICUBIC) @@ -154,7 +155,7 @@ def test_reducing_gap_values(): assert_image_similar(ref, im, 3.5) -def test_reducing_gap_for_DCT_scaling(): +def test_reducing_gap_for_DCT_scaling() -> None: with Image.open("Tests/images/hopper.jpg") as ref: # thumbnail should call draft with reducing_gap scale ref.draft(None, (18 * 3, 18 * 3)) diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 156b9919d..f7a3cc41d 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -1,10 +1,11 @@ from __future__ import annotations + import pytest from .helper import assert_image_equal, fromstring, hopper -def test_sanity(): +def test_sanity() -> None: with pytest.raises(ValueError): hopper().tobitmap() diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index f6042bca5..d32b6c09b 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -1,7 +1,8 @@ from __future__ import annotations + from .helper import hopper -def test_sanity(): +def test_sanity() -> None: data = hopper().tobytes() assert isinstance(data, bytes) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 15939ef64..638d12247 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,5 +1,7 @@ from __future__ import annotations + import math +from typing import Callable import pytest @@ -9,21 +11,28 @@ from .helper import assert_image_equal, assert_image_similar, hopper class TestImageTransform: - def test_sanity(self): - im = Image.new("L", (100, 100)) + def test_sanity(self) -> None: + im = hopper() - seq = tuple(range(10)) + for transform in ( + ImageTransform.AffineTransform((1, 0, 0, 0, 1, 0)), + ImageTransform.PerspectiveTransform((1, 0, 0, 0, 1, 0, 0, 0)), + ImageTransform.ExtentTransform((0, 0) + im.size), + ImageTransform.QuadTransform( + (0, 0, 0, im.height, im.width, im.height, im.width, 0) + ), + ImageTransform.MeshTransform( + [ + ( + (0, 0) + im.size, + (0, 0, 0, im.height, im.width, im.height, im.width, 0), + ) + ] + ), + ): + assert_image_equal(im, im.transform(im.size, transform)) - transform = ImageTransform.AffineTransform(seq[:6]) - im.transform((100, 100), transform) - transform = ImageTransform.ExtentTransform(seq[:4]) - im.transform((100, 100), transform) - transform = ImageTransform.QuadTransform(seq[:8]) - im.transform((100, 100), transform) - transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])]) - im.transform((100, 100), transform) - - def test_info(self): + def test_info(self) -> None: comment = b"File written by Adobe Photoshop\xa8 4.0" with Image.open("Tests/images/hopper.gif") as im: @@ -33,14 +42,14 @@ class TestImageTransform: new_im = im.transform((100, 100), transform) assert new_im.info["comment"] == comment - def test_palette(self): + def test_palette(self) -> None: with Image.open("Tests/images/hopper.gif") as im: 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): + def test_extent(self) -> None: im = hopper("RGB") (w, h) = im.size transformed = im.transform( @@ -55,7 +64,7 @@ class TestImageTransform: # undone -- precision? assert_image_similar(transformed, scaled, 23) - def test_quad(self): + def test_quad(self) -> None: # one simple quad transform, equivalent to scale & crop upper left quad im = hopper("RGB") (w, h) = im.size @@ -83,7 +92,7 @@ class TestImageTransform: ("LA", (76, 0)), ), ) - def test_fill(self, mode, expected_pixel): + def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None: im = hopper(mode) (w, h) = im.size transformed = im.transform( @@ -95,7 +104,7 @@ class TestImageTransform: ) assert transformed.getpixel((w - 1, h - 1)) == expected_pixel - def test_mesh(self): + def test_mesh(self) -> None: # this should be a checkerboard of halfsized hoppers in ul, lr im = hopper("RGBA") (w, h) = im.size @@ -134,7 +143,9 @@ class TestImageTransform: assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2))) assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h))) - def _test_alpha_premult(self, op): + def _test_alpha_premult( + self, op: Callable[[Image.Image, tuple[int, int]], Image.Image] + ) -> None: # create image with half white, half black, # with the black half transparent. # do op, @@ -150,14 +161,14 @@ class TestImageTransform: hist = im_background.histogram() assert 40 * 10 == hist[-1] - def test_alpha_premult_resize(self): - def op(im, sz): + def test_alpha_premult_resize(self) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: return im.resize(sz, Image.Resampling.BILINEAR) self._test_alpha_premult(op) - def test_alpha_premult_transform(self): - def op(im, sz): + def test_alpha_premult_transform(self) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: (w, h) = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR @@ -165,7 +176,9 @@ class TestImageTransform: self._test_alpha_premult(op) - def _test_nearest(self, op, mode): + def _test_nearest( + self, op: Callable[[Image.Image, tuple[int, int]], Image.Image], mode: str + ) -> None: # create white image with half transparent, # do op, # the image should remain white with half transparent @@ -188,15 +201,15 @@ class TestImageTransform: ) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_resize(self, mode): - def op(im, sz): + def test_nearest_resize(self, mode: str) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: return im.resize(sz, Image.Resampling.NEAREST) self._test_nearest(op, mode) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_transform(self, mode): - def op(im, sz): + def test_nearest_transform(self, mode: str) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: (w, h) = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST @@ -204,7 +217,7 @@ class TestImageTransform: self._test_nearest(op, mode) - def test_blank_fill(self): + def test_blank_fill(self) -> None: # attempting to hit # https://github.com/python-pillow/Pillow/issues/254 reported # @@ -219,20 +232,22 @@ class TestImageTransform: # Running by default, but I'd totally understand not doing it in # the future - pattern = [Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)] + pattern: list[Image.Image] | None = [ + Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65) + ] # Yeah. Watch some JIT optimize this out. pattern = None # noqa: F841 self.test_mesh() - def test_missing_method_data(self): + def test_missing_method_data(self) -> None: with hopper() as im: with pytest.raises(ValueError): im.transform((100, 100), None) @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) - def test_unknown_resampling_filter(self, resample): + def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None: with hopper() as im: (w, h) = im.size with pytest.raises(ValueError): @@ -242,7 +257,7 @@ class TestImageTransform: class TestImageTransformAffine: transform = Image.Transform.AFFINE - def _test_image(self): + def _test_image(self) -> Image.Image: im = hopper("RGB") return im.crop((10, 20, im.width - 10, im.height - 20)) @@ -255,7 +270,7 @@ class TestImageTransformAffine: (270, Image.Transpose.ROTATE_270), ), ) - def test_rotate(self, deg, transpose): + def test_rotate(self, deg: int, transpose: Image.Transpose | None) -> None: im = self._test_image() angle = -math.radians(deg) @@ -305,7 +320,13 @@ class TestImageTransformAffine: (Image.Resampling.BICUBIC, 1), ), ) - def test_resize(self, scale, epsilon_scale, resample, epsilon): + def test_resize( + self, + scale: float, + epsilon_scale: float, + resample: Image.Resampling, + epsilon: int, + ) -> None: im = self._test_image() size_up = int(round(im.width * scale)), int(round(im.height * scale)) @@ -334,7 +355,14 @@ class TestImageTransformAffine: (Image.Resampling.BICUBIC, 1), ), ) - def test_translate(self, x, y, epsilon_scale, resample, epsilon): + def test_translate( + self, + x: float, + y: float, + epsilon_scale: float, + resample: Image.Resampling, + epsilon: float, + ) -> None: im = self._test_image() size_up = int(round(im.width + x)), int(round(im.height + y)) diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index 66a2d9e29..d384d8141 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,6 +1,8 @@ from __future__ import annotations + import pytest +from PIL import Image from PIL.Image import Transpose from . import helper @@ -13,7 +15,7 @@ HOPPER = { @pytest.mark.parametrize("mode", HOPPER) -def test_flip_left_right(mode): +def test_flip_left_right(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.FLIP_LEFT_RIGHT) assert out.mode == mode @@ -27,7 +29,7 @@ def test_flip_left_right(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_flip_top_bottom(mode): +def test_flip_top_bottom(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.FLIP_TOP_BOTTOM) assert out.mode == mode @@ -41,7 +43,7 @@ def test_flip_top_bottom(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_rotate_90(mode): +def test_rotate_90(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.ROTATE_90) assert out.mode == mode @@ -55,7 +57,7 @@ def test_rotate_90(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_rotate_180(mode): +def test_rotate_180(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.ROTATE_180) assert out.mode == mode @@ -69,7 +71,7 @@ def test_rotate_180(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_rotate_270(mode): +def test_rotate_270(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.ROTATE_270) assert out.mode == mode @@ -83,7 +85,7 @@ def test_rotate_270(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_transpose(mode): +def test_transpose(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.TRANSPOSE) assert out.mode == mode @@ -97,7 +99,7 @@ def test_transpose(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_tranverse(mode): +def test_tranverse(mode: str) -> None: im = HOPPER[mode] out = im.transpose(Transpose.TRANSVERSE) assert out.mode == mode @@ -111,10 +113,10 @@ def test_tranverse(mode): @pytest.mark.parametrize("mode", HOPPER) -def test_roundtrip(mode): +def test_roundtrip(mode: str) -> None: im = HOPPER[mode] - def transpose(first, second): + def transpose(first: Transpose, second: Transpose) -> Image.Image: return im.transpose(first).transpose(second) assert_image_equal( diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 8e3a738d7..7e2290c15 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from typing import Callable + from PIL import Image, ImageChops from .helper import assert_image_equal, hopper @@ -14,7 +17,7 @@ WHITE = (255, 255, 255) GRAY = 128 -def test_sanity(): +def test_sanity() -> None: im = hopper("L") ImageChops.constant(im, 128) @@ -47,7 +50,7 @@ def test_sanity(): ImageChops.offset(im, 10, 20) -def test_add(): +def test_add() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -59,7 +62,7 @@ def test_add(): assert new.getpixel((50, 50)) == ORANGE -def test_add_scale_offset(): +def test_add_scale_offset() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -71,7 +74,7 @@ def test_add_scale_offset(): assert new.getpixel((50, 50)) == (202, 151, 100) -def test_add_clip(): +def test_add_clip() -> None: # Arrange im = hopper() @@ -82,7 +85,7 @@ def test_add_clip(): assert new.getpixel((50, 50)) == (255, 255, 254) -def test_add_modulo(): +def test_add_modulo() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -94,7 +97,7 @@ def test_add_modulo(): assert new.getpixel((50, 50)) == ORANGE -def test_add_modulo_no_clip(): +def test_add_modulo_no_clip() -> None: # Arrange im = hopper() @@ -105,7 +108,7 @@ def test_add_modulo_no_clip(): assert new.getpixel((50, 50)) == (224, 76, 254) -def test_blend(): +def test_blend() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -117,7 +120,7 @@ def test_blend(): assert new.getpixel((50, 50)) == BROWN -def test_constant(): +def test_constant() -> None: # Arrange im = Image.new("RGB", (20, 10)) @@ -130,7 +133,7 @@ def test_constant(): assert new.getpixel((19, 9)) == GRAY -def test_darker_image(): +def test_darker_image() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -141,7 +144,7 @@ def test_darker_image(): assert_image_equal(new, im2) -def test_darker_pixel(): +def test_darker_pixel() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -152,7 +155,7 @@ def test_darker_pixel(): assert new.getpixel((50, 50)) == (240, 166, 0) -def test_difference(): +def test_difference() -> None: # Arrange with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1: with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2: @@ -163,7 +166,7 @@ def test_difference(): assert new.getbbox() == (25, 25, 76, 76) -def test_difference_pixel(): +def test_difference_pixel() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: @@ -174,7 +177,7 @@ def test_difference_pixel(): assert new.getpixel((50, 50)) == (240, 166, 128) -def test_duplicate(): +def test_duplicate() -> None: # Arrange im = hopper() @@ -185,7 +188,7 @@ def test_duplicate(): assert_image_equal(new, im) -def test_invert(): +def test_invert() -> None: # Arrange with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: # Act @@ -197,7 +200,7 @@ def test_invert(): assert new.getpixel((50, 50)) == CYAN -def test_lighter_image(): +def test_lighter_image() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -208,7 +211,7 @@ def test_lighter_image(): assert_image_equal(new, im1) -def test_lighter_pixel(): +def test_lighter_pixel() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -219,7 +222,7 @@ def test_lighter_pixel(): assert new.getpixel((50, 50)) == (255, 255, 127) -def test_multiply_black(): +def test_multiply_black() -> None: """If you multiply an image with a solid black image, the result is black.""" # Arrange @@ -233,7 +236,7 @@ def test_multiply_black(): assert_image_equal(new, black) -def test_multiply_green(): +def test_multiply_green() -> None: # Arrange with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: green = Image.new("RGB", im.size, "green") @@ -247,7 +250,7 @@ def test_multiply_green(): assert new.getpixel((50, 50)) == BLACK -def test_multiply_white(): +def test_multiply_white() -> None: """If you multiply with a solid white image, the image is unaffected.""" # Arrange im1 = hopper() @@ -260,7 +263,7 @@ def test_multiply_white(): assert_image_equal(new, im1) -def test_offset(): +def test_offset() -> None: # Arrange xoffset = 45 yoffset = 20 @@ -277,7 +280,7 @@ def test_offset(): assert ImageChops.offset(im, xoffset) == ImageChops.offset(im, xoffset, xoffset) -def test_screen(): +def test_screen() -> None: # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: @@ -289,7 +292,7 @@ def test_screen(): assert new.getpixel((50, 50)) == ORANGE -def test_subtract(): +def test_subtract() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -302,7 +305,7 @@ def test_subtract(): assert new.getpixel((50, 52)) == BLACK -def test_subtract_scale_offset(): +def test_subtract_scale_offset() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -314,7 +317,7 @@ def test_subtract_scale_offset(): assert new.getpixel((50, 50)) == (100, 202, 100) -def test_subtract_clip(): +def test_subtract_clip() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -325,7 +328,7 @@ def test_subtract_clip(): assert new.getpixel((50, 50)) == (0, 0, 127) -def test_subtract_modulo(): +def test_subtract_modulo() -> None: # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: @@ -338,7 +341,7 @@ def test_subtract_modulo(): assert new.getpixel((50, 52)) == BLACK -def test_subtract_modulo_no_clip(): +def test_subtract_modulo_no_clip() -> None: # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: @@ -349,7 +352,7 @@ def test_subtract_modulo_no_clip(): assert new.getpixel((50, 50)) == (241, 167, 127) -def test_soft_light(): +def test_soft_light() -> None: # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: @@ -361,7 +364,7 @@ def test_soft_light(): assert new.getpixel((15, 100)) == (1, 1, 3) -def test_hard_light(): +def test_hard_light() -> None: # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: @@ -373,7 +376,7 @@ def test_hard_light(): assert new.getpixel((15, 100)) == (1, 1, 2) -def test_overlay(): +def test_overlay() -> None: # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: @@ -385,8 +388,10 @@ def test_overlay(): assert new.getpixel((15, 100)) == (1, 1, 2) -def test_logical(): - def table(op, a, b): +def test_logical() -> None: + def table( + op: Callable[[Image.Image, Image.Image], Image.Image], a: int, b: int + ) -> tuple[int, int, int, int]: out = [] for x in (a, b): imx = Image.new("1", (1, 1), x) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 0dde82bd7..6be29a70f 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,9 +1,12 @@ from __future__ import annotations + import datetime import os import re import shutil from io import BytesIO +from pathlib import Path +from typing import Any import pytest @@ -31,7 +34,7 @@ SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc" HAVE_PROFILE = os.path.exists(SRGB) -def setup_module(): +def setup_module() -> None: try: from PIL import ImageCms @@ -41,16 +44,16 @@ def setup_module(): pytest.skip(str(v)) -def skip_missing(): +def skip_missing() -> None: if not HAVE_PROFILE: pytest.skip("SRGB profile not available") -def test_sanity(): +def test_sanity() -> None: # basic smoke test. # this mostly follows the cms_test outline. - - v = ImageCms.versions() # should return four strings + with pytest.warns(DeprecationWarning): + v = ImageCms.versions() # should return four strings assert v[0] == "1.0.0 pil" assert list(map(type, v)) == [str, str, str, str] @@ -90,7 +93,17 @@ def test_sanity(): hopper().point(t) -def test_name(): +def test_flags() -> None: + assert ImageCms.Flags.NONE == 0 + assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE + assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE + + assert ImageCms.Flags.GRIDPOINTS(255) == (255 << 16) + assert ImageCms.Flags.GRIDPOINTS(-1) == ImageCms.Flags.GRIDPOINTS(255) + assert ImageCms.Flags.GRIDPOINTS(511) == ImageCms.Flags.GRIDPOINTS(255) + + +def test_name() -> None: skip_missing() # get profile information for file assert ( @@ -99,7 +112,7 @@ def test_name(): ) -def test_info(): +def test_info() -> None: skip_missing() assert ImageCms.getProfileInfo(SRGB).splitlines() == [ "sRGB IEC61966-2-1 black scaled", @@ -109,7 +122,7 @@ def test_info(): ] -def test_copyright(): +def test_copyright() -> None: skip_missing() assert ( ImageCms.getProfileCopyright(SRGB).strip() @@ -117,12 +130,12 @@ def test_copyright(): ) -def test_manufacturer(): +def test_manufacturer() -> None: skip_missing() assert ImageCms.getProfileManufacturer(SRGB).strip() == "" -def test_model(): +def test_model() -> None: skip_missing() assert ( ImageCms.getProfileModel(SRGB).strip() @@ -130,14 +143,14 @@ def test_model(): ) -def test_description(): +def test_description() -> None: skip_missing() assert ( ImageCms.getProfileDescription(SRGB).strip() == "sRGB IEC61966-2-1 black scaled" ) -def test_intent(): +def test_intent() -> None: skip_missing() assert ImageCms.getDefaultIntent(SRGB) == 0 support = ImageCms.isIntentSupported( @@ -146,7 +159,7 @@ def test_intent(): assert support == 1 -def test_profile_object(): +def test_profile_object() -> None: # same, using profile object p = ImageCms.createProfile("sRGB") # assert ImageCms.getProfileName(p).strip() == "sRGB built-in - (lcms internal)" @@ -159,7 +172,7 @@ def test_profile_object(): assert support == 1 -def test_extensions(): +def test_extensions() -> None: # extensions with Image.open("Tests/images/rgb.jpg") as i: @@ -170,7 +183,7 @@ def test_extensions(): ) -def test_exceptions(): +def test_exceptions() -> None: # Test mode mismatch psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") @@ -196,17 +209,17 @@ def test_exceptions(): ImageCms.isIntentSupported(SRGB, None, None) -def test_display_profile(): +def test_display_profile() -> None: # try fetching the profile for the current display device ImageCms.get_display_profile() -def test_lab_color_profile(): +def test_lab_color_profile() -> None: ImageCms.createProfile("LAB", 5000) ImageCms.createProfile("LAB", 6500) -def test_unsupported_color_space(): +def test_unsupported_color_space() -> None: with pytest.raises( ImageCms.PyCMSError, match=re.escape( @@ -216,7 +229,7 @@ def test_unsupported_color_space(): ImageCms.createProfile("unsupported") -def test_invalid_color_temperature(): +def test_invalid_color_temperature() -> None: with pytest.raises( ImageCms.PyCMSError, match='Color temperature must be numeric, "invalid" not valid', @@ -225,7 +238,7 @@ def test_invalid_color_temperature(): @pytest.mark.parametrize("flag", ("my string", -1)) -def test_invalid_flag(flag): +def test_invalid_flag(flag: str | int) -> None: with hopper() as im: with pytest.raises( ImageCms.PyCMSError, match="flags must be an integer between 0 and " @@ -233,7 +246,7 @@ def test_invalid_flag(flag): ImageCms.profileToProfile(im, "foo", "bar", flags=flag) -def test_simple_lab(): +def test_simple_lab() -> None: i = Image.new("RGB", (10, 10), (128, 128, 128)) psRGB = ImageCms.createProfile("sRGB") @@ -257,7 +270,7 @@ def test_simple_lab(): assert list(b_data) == [128] * 100 -def test_lab_color(): +def test_lab_color() -> None: psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") @@ -272,7 +285,7 @@ def test_lab_color(): assert_image_similar_tofile(i, "Tests/images/hopper.Lab.tif", 3.5) -def test_lab_srgb(): +def test_lab_srgb() -> None: psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") @@ -289,7 +302,7 @@ def test_lab_srgb(): assert "sRGB" in ImageCms.getProfileDescription(profile) -def test_lab_roundtrip(): +def test_lab_roundtrip() -> None: # check to see if we're at least internally consistent. psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") @@ -306,7 +319,7 @@ def test_lab_roundtrip(): assert_image_similar(hopper(), out, 2) -def test_profile_tobytes(): +def test_profile_tobytes() -> None: with Image.open("Tests/images/rgb.jpg") as i: p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) @@ -318,22 +331,26 @@ def test_profile_tobytes(): assert ImageCms.getProfileDescription(p) == ImageCms.getProfileDescription(p2) -def test_extended_information(): +def test_extended_information() -> None: skip_missing() o = ImageCms.getOpenProfile(SRGB) p = o.profile - def assert_truncated_tuple_equal(tup1, tup2, digits=10): + def assert_truncated_tuple_equal( + tup1: tuple[Any, ...], tup2: tuple[Any, ...], digits: int = 10 + ) -> None: # Helper function to reduce precision of tuples of floats # recursively and then check equality. power = 10**digits - def truncate_tuple(tuple_or_float): + def truncate_tuple(tuple_value: tuple[Any, ...]) -> tuple[Any, ...]: return tuple( - truncate_tuple(val) - if isinstance(val, tuple) - else int(val * power) / power - for val in tuple_or_float + ( + truncate_tuple(val) + if isinstance(val, tuple) + else int(val * power) / power + ) + for val in tuple_value ) assert truncate_tuple(tup1) == truncate_tuple(tup2) @@ -465,7 +482,7 @@ def test_extended_information(): assert p.xcolor_space == "RGB " -def test_non_ascii_path(tmp_path): +def test_non_ascii_path(tmp_path: Path) -> None: skip_missing() tempfile = str(tmp_path / ("temp_" + chr(128) + ".icc")) try: @@ -478,7 +495,7 @@ def test_non_ascii_path(tmp_path): assert p.model == "IEC 61966-2-1 Default RGB Colour Space - sRGB" -def test_profile_typesafety(): +def test_profile_typesafety() -> None: """Profile init type safety prepatch, these would segfault, postpatch they should emit a typeerror @@ -490,8 +507,10 @@ def test_profile_typesafety(): ImageCms.ImageCmsProfile(1).tobytes() -def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel): - def create_test_image(): +def assert_aux_channel_preserved( + mode: str, transform_in_place: bool, preserved_channel: str +) -> None: + def create_test_image() -> Image.Image: # set up test image with something interesting in the tested aux channel. # fmt: off nine_grid_deltas = [ @@ -545,31 +564,31 @@ def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel): assert_image_equal(source_image_aux, result_image_aux) -def test_preserve_auxiliary_channels_rgba(): +def test_preserve_auxiliary_channels_rgba() -> None: assert_aux_channel_preserved( mode="RGBA", transform_in_place=False, preserved_channel="A" ) -def test_preserve_auxiliary_channels_rgba_in_place(): +def test_preserve_auxiliary_channels_rgba_in_place() -> None: assert_aux_channel_preserved( mode="RGBA", transform_in_place=True, preserved_channel="A" ) -def test_preserve_auxiliary_channels_rgbx(): +def test_preserve_auxiliary_channels_rgbx() -> None: assert_aux_channel_preserved( mode="RGBX", transform_in_place=False, preserved_channel="X" ) -def test_preserve_auxiliary_channels_rgbx_in_place(): +def test_preserve_auxiliary_channels_rgbx_in_place() -> None: assert_aux_channel_preserved( mode="RGBX", transform_in_place=True, preserved_channel="X" ) -def test_auxiliary_channels_isolated(): +def test_auxiliary_channels_isolated() -> None: # test data in aux channels does not affect non-aux channels aux_channel_formats = [ # format, profile, color-only format, source test image @@ -619,7 +638,7 @@ def test_auxiliary_channels_isolated(): @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) -def test_rgb_lab(mode): +def test_rgb_lab(mode: str) -> None: im = Image.new(mode, (1, 1)) converted_im = im.convert("LAB") assert converted_im.getpixel((0, 0)) == (0, 128, 128) @@ -627,3 +646,12 @@ def test_rgb_lab(mode): im = Image.new("LAB", (1, 1), (255, 0, 0)) converted_im = im.convert(mode) assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) + + +def test_deprecation() -> None: + with pytest.warns(DeprecationWarning): + assert ImageCms.DESCRIPTION.strip().startswith("pyCMS") + with pytest.warns(DeprecationWarning): + assert ImageCms.VERSION == "1.0.0 pil" + with pytest.warns(DeprecationWarning): + assert isinstance(ImageCms.FLAGS, dict) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index c0ffd2ebf..6eea7886d 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -1,10 +1,11 @@ from __future__ import annotations + import pytest from PIL import Image, ImageColor -def test_hash(): +def test_hash() -> None: # short 3 components assert (255, 0, 0) == ImageColor.getrgb("#f00") assert (0, 255, 0) == ImageColor.getrgb("#0f0") @@ -56,7 +57,7 @@ def test_hash(): ImageColor.getrgb("#f00000 ") -def test_colormap(): +def test_colormap() -> None: assert (0, 0, 0) == ImageColor.getrgb("black") assert (255, 255, 255) == ImageColor.getrgb("white") assert (255, 255, 255) == ImageColor.getrgb("WHITE") @@ -65,7 +66,7 @@ def test_colormap(): ImageColor.getrgb("black ") -def test_functions(): +def test_functions() -> None: # rgb numbers assert (255, 0, 0) == ImageColor.getrgb("rgb(255,0,0)") assert (0, 255, 0) == ImageColor.getrgb("rgb(0,255,0)") @@ -159,7 +160,7 @@ def test_functions(): # look for rounding errors (based on code by Tim Hatch) -def test_rounding_errors(): +def test_rounding_errors() -> None: for color in ImageColor.colormap: expected = Image.new("RGB", (1, 1), color).convert("L").getpixel((0, 0)) actual = ImageColor.getcolor(color, "L") @@ -194,11 +195,11 @@ def test_rounding_errors(): Image.new("LA", (1, 1), "white") -def test_color_hsv(): +def test_color_hsv() -> None: assert (170, 255, 255) == ImageColor.getcolor("hsv(240, 100%, 100%)", "HSV") -def test_color_too_long(): +def test_color_too_long() -> None: # Arrange color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)" diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 379fe78cd..f7aea3034 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,10 +1,12 @@ from __future__ import annotations + import contextlib import os.path import pytest from PIL import Image, ImageColor, ImageDraw, ImageFont, features +from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -46,7 +48,7 @@ KITE_POINTS = ( ) -def test_sanity(): +def test_sanity() -> None: im = hopper("RGB").copy() draw = ImageDraw.ImageDraw(im) @@ -58,13 +60,13 @@ def test_sanity(): draw.rectangle(list(range(4))) -def test_valueerror(): +def test_valueerror() -> None: with Image.open("Tests/images/chi.gif") as im: draw = ImageDraw.Draw(im) draw.line((0, 0), fill=(0, 0, 0)) -def test_mode_mismatch(): +def test_mode_mismatch() -> None: im = hopper("RGB").copy() with pytest.raises(ValueError): @@ -73,7 +75,7 @@ def test_mode_mismatch(): @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc(bbox, start, end): +def test_arc(bbox: Coords, start: float, end: float) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -86,7 +88,7 @@ def test_arc(bbox, start, end): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start(bbox): +def test_arc_end_le_start(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -101,7 +103,7 @@ def test_arc_end_le_start(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops(bbox): +def test_arc_no_loops(bbox: Coords) -> None: # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -117,7 +119,7 @@ def test_arc_no_loops(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width(bbox): +def test_arc_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -130,7 +132,7 @@ def test_arc_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large(bbox): +def test_arc_width_pieslice_large(bbox: Coords) -> None: # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) @@ -144,7 +146,7 @@ def test_arc_width_pieslice_large(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill(bbox): +def test_arc_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -157,7 +159,7 @@ def test_arc_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle(bbox): +def test_arc_width_non_whole_angle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -170,7 +172,7 @@ def test_arc_width_non_whole_angle(bbox): assert_image_similar_tofile(im, expected, 1) -def test_arc_high(): +def test_arc_high() -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -183,7 +185,7 @@ def test_arc_high(): assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_high.png") -def test_bitmap(): +def test_bitmap() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -199,7 +201,7 @@ def test_bitmap(): @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_chord(mode, bbox): +def test_chord(mode: str, bbox: Coords) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -213,7 +215,7 @@ def test_chord(mode, bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width(bbox): +def test_chord_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -226,7 +228,7 @@ def test_chord_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill(bbox): +def test_chord_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -239,7 +241,7 @@ def test_chord_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width(bbox): +def test_chord_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -251,7 +253,7 @@ def test_chord_zero_width(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png") -def test_chord_too_fat(): +def test_chord_too_fat() -> None: # Arrange im = Image.new("RGB", (100, 100)) draw = ImageDraw.Draw(im) @@ -265,7 +267,7 @@ def test_chord_too_fat(): @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(mode, bbox): +def test_ellipse(mode: str, bbox: Coords) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -279,7 +281,7 @@ def test_ellipse(mode, bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent(bbox): +def test_ellipse_translucent(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -292,7 +294,7 @@ def test_ellipse_translucent(bbox): assert_image_similar_tofile(im, expected, 1) -def test_ellipse_edge(): +def test_ellipse_edge() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -304,7 +306,7 @@ def test_ellipse_edge(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) -def test_ellipse_symmetric(): +def test_ellipse_symmetric() -> None: for width, bbox in ( (100, (24, 24, 75, 75)), (101, (25, 25, 75, 75)), @@ -316,7 +318,7 @@ def test_ellipse_symmetric(): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width(bbox): +def test_ellipse_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -328,7 +330,7 @@ def test_ellipse_width(bbox): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) -def test_ellipse_width_large(): +def test_ellipse_width_large() -> None: # Arrange im = Image.new("RGB", (500, 500)) draw = ImageDraw.Draw(im) @@ -341,7 +343,7 @@ def test_ellipse_width_large(): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill(bbox): +def test_ellipse_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -354,7 +356,7 @@ def test_ellipse_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width(bbox): +def test_ellipse_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -366,7 +368,7 @@ def test_ellipse_zero_width(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") -def ellipse_various_sizes_helper(filled): +def ellipse_various_sizes_helper(filled: bool) -> Image.Image: ellipse_sizes = range(32) image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1 im = Image.new("RGB", (image_size, image_size)) @@ -393,13 +395,13 @@ def ellipse_various_sizes_helper(filled): return im -def test_ellipse_various_sizes(): +def test_ellipse_various_sizes() -> None: im = ellipse_various_sizes_helper(False) assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_various_sizes.png") -def test_ellipse_various_sizes_filled(): +def test_ellipse_various_sizes_filled() -> None: im = ellipse_various_sizes_helper(True) assert_image_equal_tofile( @@ -408,7 +410,7 @@ def test_ellipse_various_sizes_filled(): @pytest.mark.parametrize("points", POINTS) -def test_line(points): +def test_line(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -420,7 +422,7 @@ def test_line(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -def test_shape1(): +def test_shape1() -> None: # Arrange im = Image.new("RGB", (100, 100), "white") draw = ImageDraw.Draw(im) @@ -441,7 +443,7 @@ def test_shape1(): assert_image_equal_tofile(im, "Tests/images/imagedraw_shape1.png") -def test_shape2(): +def test_shape2() -> None: # Arrange im = Image.new("RGB", (100, 100), "white") draw = ImageDraw.Draw(im) @@ -462,7 +464,7 @@ def test_shape2(): assert_image_equal_tofile(im, "Tests/images/imagedraw_shape2.png") -def test_transform(): +def test_transform() -> None: # Arrange im = Image.new("RGB", (100, 100), "white") expected = im.copy() @@ -481,7 +483,7 @@ def test_transform(): @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) -def test_pieslice(bbox, start, end): +def test_pieslice(bbox: Coords, start: float, end: float) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -494,7 +496,7 @@ def test_pieslice(bbox, start, end): @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width(bbox): +def test_pieslice_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -507,7 +509,7 @@ def test_pieslice_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill(bbox): +def test_pieslice_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -521,7 +523,7 @@ def test_pieslice_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width(bbox): +def test_pieslice_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -533,7 +535,7 @@ def test_pieslice_zero_width(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png") -def test_pieslice_wide(): +def test_pieslice_wide() -> None: # Arrange im = Image.new("RGB", (200, 100)) draw = ImageDraw.Draw(im) @@ -545,7 +547,7 @@ def test_pieslice_wide(): assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_wide.png") -def test_pieslice_no_spikes(): +def test_pieslice_no_spikes() -> None: im = Image.new("RGB", (161, 161), "white") draw = ImageDraw.Draw(im) cxs = ( @@ -576,7 +578,7 @@ def test_pieslice_no_spikes(): @pytest.mark.parametrize("points", POINTS) -def test_point(points): +def test_point(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -588,7 +590,7 @@ def test_point(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") -def test_point_I16(): +def test_point_I16() -> None: # Arrange im = Image.new("I;16", (1, 1)) draw = ImageDraw.Draw(im) @@ -601,7 +603,7 @@ def test_point_I16(): @pytest.mark.parametrize("points", POINTS) -def test_polygon(points): +def test_polygon(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -615,7 +617,9 @@ def test_polygon(points): @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("kite_points", KITE_POINTS) -def test_polygon_kite(mode, kite_points): +def test_polygon_kite( + mode: str, kite_points: tuple[tuple[int, int], ...] | list[tuple[int, int]] +) -> None: # Test drawing lines of different gradients (dx>dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines # Arrange @@ -630,7 +634,7 @@ def test_polygon_kite(mode, kite_points): assert_image_equal_tofile(im, expected) -def test_polygon_1px_high(): +def test_polygon_1px_high() -> None: # Test drawing a 1px high polygon # Arrange im = Image.new("RGB", (3, 3)) @@ -644,7 +648,7 @@ def test_polygon_1px_high(): assert_image_equal_tofile(im, expected) -def test_polygon_1px_high_translucent(): +def test_polygon_1px_high_translucent() -> None: # Test drawing a translucent 1px high polygon # Arrange im = Image.new("RGB", (4, 3)) @@ -658,7 +662,7 @@ def test_polygon_1px_high_translucent(): assert_image_equal_tofile(im, expected) -def test_polygon_translucent(): +def test_polygon_translucent() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -672,7 +676,7 @@ def test_polygon_translucent(): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox): +def test_rectangle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -684,7 +688,7 @@ def test_rectangle(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") -def test_big_rectangle(): +def test_big_rectangle() -> None: # Test drawing a rectangle bigger than the image # Arrange im = Image.new("RGB", (W, H)) @@ -699,7 +703,7 @@ def test_big_rectangle(): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width(bbox): +def test_rectangle_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -713,7 +717,7 @@ def test_rectangle_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill(bbox): +def test_rectangle_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -727,7 +731,7 @@ def test_rectangle_width_fill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width(bbox): +def test_rectangle_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -740,7 +744,7 @@ def test_rectangle_zero_width(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16(bbox): +def test_rectangle_I16(bbox: Coords) -> None: # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) @@ -753,7 +757,7 @@ def test_rectangle_I16(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline(bbox): +def test_rectangle_translucent_outline(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -771,7 +775,13 @@ def test_rectangle_translucent_outline(bbox): "xy", [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], ) -def test_rounded_rectangle(xy): +def test_rounded_rectangle( + xy: ( + tuple[int, int, int, int] + | tuple[list[int]] + | tuple[tuple[int, int], tuple[int, int]] + ) +) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -787,7 +797,9 @@ def test_rounded_rectangle(xy): @pytest.mark.parametrize("top_right", (True, False)) @pytest.mark.parametrize("bottom_right", (True, False)) @pytest.mark.parametrize("bottom_left", (True, False)) -def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_left): +def test_rounded_rectangle_corners( + top_left: bool, top_right: bool, bottom_right: bool, bottom_left: bool +) -> None: corners = (top_left, top_right, bottom_right, bottom_left) # Arrange @@ -821,7 +833,9 @@ def test_rounded_rectangle_corners(top_left, top_right, bottom_right, bottom_lef ((10, 20, 190, 181), 85, "height"), ], ) -def test_rounded_rectangle_non_integer_radius(xy, radius, type): +def test_rounded_rectangle_non_integer_radius( + xy: tuple[int, int, int, int], radius: float, type: str +) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -837,7 +851,7 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type): @pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius(bbox): +def test_rounded_rectangle_zero_radius(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -859,7 +873,9 @@ def test_rounded_rectangle_zero_radius(bbox): ((20, 20, 80, 80), "both"), ], ) -def test_rounded_rectangle_translucent(xy, suffix): +def test_rounded_rectangle_translucent( + xy: tuple[int, int, int, int], suffix: str +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -876,7 +892,7 @@ def test_rounded_rectangle_translucent(xy, suffix): @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill(bbox): +def test_floodfill(bbox: Coords) -> None: red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: @@ -909,7 +925,7 @@ def test_floodfill(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border(bbox): +def test_floodfill_border(bbox: Coords) -> None: # floodfill() is experimental # Arrange @@ -931,7 +947,7 @@ def test_floodfill_border(bbox): @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh(bbox): +def test_floodfill_thresh(bbox: Coords) -> None: # floodfill() is experimental # Arrange @@ -947,7 +963,7 @@ def test_floodfill_thresh(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") -def test_floodfill_not_negative(): +def test_floodfill_not_negative() -> None: # floodfill() is experimental # Test that floodfill does not extend into negative coordinates @@ -965,8 +981,11 @@ def test_floodfill_not_negative(): def create_base_image_draw( - size, mode=DEFAULT_MODE, background1=WHITE, background2=GRAY -): + size: tuple[int, int], + mode: str = DEFAULT_MODE, + background1: tuple[int, int, int] = WHITE, + background2: tuple[int, int, int] = GRAY, +) -> tuple[Image.Image, ImageDraw.ImageDraw]: img = Image.new(mode, size, background1) for x in range(0, size[0]): for y in range(0, size[1]): @@ -975,7 +994,7 @@ def create_base_image_draw( return img, ImageDraw.Draw(img) -def test_square(): +def test_square() -> None: expected = os.path.join(IMAGES_PATH, "square.png") img, draw = create_base_image_draw((10, 10)) draw.polygon([(2, 2), (2, 7), (7, 7), (7, 2)], BLACK) @@ -988,7 +1007,7 @@ def test_square(): assert_image_equal_tofile(img, expected, "square as normal rectangle failed") -def test_triangle_right(): +def test_triangle_right() -> None: img, draw = create_base_image_draw((20, 20)) draw.polygon([(3, 5), (17, 5), (10, 12)], BLACK) assert_image_equal_tofile( @@ -1000,7 +1019,7 @@ def test_triangle_right(): "fill, suffix", ((BLACK, "width"), (None, "width_no_fill")), ) -def test_triangle_right_width(fill, suffix): +def test_triangle_right_width(fill: tuple[int, int, int] | None, suffix: str) -> None: img, draw = create_base_image_draw((100, 100)) draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5) assert_image_equal_tofile( @@ -1008,7 +1027,7 @@ def test_triangle_right_width(fill, suffix): ) -def test_line_horizontal(): +def test_line_horizontal() -> None: img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 5), BLACK, 2) assert_image_equal_tofile( @@ -1046,7 +1065,7 @@ def test_line_horizontal(): ) -def test_line_h_s1_w2(): +def test_line_h_s1_w2() -> None: pytest.skip("failing") img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 6), BLACK, 2) @@ -1057,7 +1076,7 @@ def test_line_h_s1_w2(): ) -def test_line_vertical(): +def test_line_vertical() -> None: img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 5, 14), BLACK, 2) assert_image_equal_tofile( @@ -1103,7 +1122,7 @@ def test_line_vertical(): ) -def test_line_oblique_45(): +def test_line_oblique_45() -> None: expected = os.path.join(IMAGES_PATH, "line_oblique_45_w3px_a.png") img, draw = create_base_image_draw((20, 20)) draw.line((5, 5, 14, 14), BLACK, 3) @@ -1125,7 +1144,7 @@ def test_line_oblique_45(): ) -def test_wide_line_dot(): +def test_wide_line_dot() -> None: # Test drawing a wide "line" from one point to another just draws a single point # Arrange im = Image.new("RGB", (W, H)) @@ -1138,7 +1157,7 @@ def test_wide_line_dot(): assert_image_similar_tofile(im, "Tests/images/imagedraw_wide_line_dot.png", 1) -def test_wide_line_larger_than_int(): +def test_wide_line_larger_than_int() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -1232,7 +1251,7 @@ def test_wide_line_larger_than_int(): ], ], ) -def test_line_joint(xy): +def test_line_joint(xy: list[tuple[int, int]] | tuple[int, ...] | list[int]) -> None: im = Image.new("RGB", (500, 325)) draw = ImageDraw.Draw(im) @@ -1243,7 +1262,7 @@ def test_line_joint(xy): assert_image_similar_tofile(im, "Tests/images/imagedraw_line_joint_curve.png", 3) -def test_textsize_empty_string(): +def test_textsize_empty_string() -> None: # https://github.com/python-pillow/Pillow/issues/2783 # Arrange im = Image.new("RGB", (W, H)) @@ -1259,7 +1278,7 @@ def test_textsize_empty_string(): @skip_unless_feature("freetype2") -def test_textbbox_stroke(): +def test_textbbox_stroke() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -1273,7 +1292,7 @@ def test_textbbox_stroke(): @skip_unless_feature("freetype2") -def test_stroke(): +def test_stroke() -> None: for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items(): # Arrange im = Image.new("RGB", (120, 130)) @@ -1290,7 +1309,7 @@ def test_stroke(): @skip_unless_feature("freetype2") -def test_stroke_descender(): +def test_stroke_descender() -> None: # Arrange im = Image.new("RGB", (120, 130)) draw = ImageDraw.Draw(im) @@ -1304,7 +1323,7 @@ def test_stroke_descender(): @skip_unless_feature("freetype2") -def test_split_word(): +def test_split_word() -> None: # Arrange im = Image.new("RGB", (230, 55)) expected = im.copy() @@ -1325,7 +1344,7 @@ def test_split_word(): @skip_unless_feature("freetype2") -def test_stroke_multiline(): +def test_stroke_multiline() -> None: # Arrange im = Image.new("RGB", (100, 250)) draw = ImageDraw.Draw(im) @@ -1341,7 +1360,7 @@ def test_stroke_multiline(): @skip_unless_feature("freetype2") -def test_setting_default_font(): +def test_setting_default_font() -> None: # Arrange im = Image.new("RGB", (100, 250)) draw = ImageDraw.Draw(im) @@ -1358,7 +1377,7 @@ def test_setting_default_font(): assert isinstance(draw.getfont(), ImageFont.load_default().__class__) -def test_default_font_size(): +def test_default_font_size() -> None: freetype_support = features.check_module("freetype2") text = "Default font at a specific size." @@ -1385,7 +1404,7 @@ def test_default_font_size(): @pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline(bbox): +def test_same_color_outline(bbox: Coords) -> None: # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 @@ -1399,7 +1418,8 @@ def test_same_color_outline(bbox): # Begin for mode in ["RGB", "L"]: - for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: + fill = "red" + for outline in [None, "red", "#f00"]: for operation, args in { "chord": [bbox, 0, 180], "ellipse": [bbox], @@ -1414,6 +1434,7 @@ def test_same_color_outline(bbox): # Act draw_method = getattr(draw, operation) + assert isinstance(args, list) args += [fill, outline] draw_method(*args) @@ -1431,7 +1452,9 @@ def test_same_color_outline(bbox): (3, "triangle_width", {"width": 5, "outline": "yellow"}), ], ) -def test_draw_regular_polygon(n_sides, polygon_name, args): +def test_draw_regular_polygon( + n_sides: int, polygon_name: str, args: dict[str, int | str] +) -> None: im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) filename = f"Tests/images/imagedraw_{polygon_name}.png" draw = ImageDraw.Draw(im) @@ -1468,7 +1491,9 @@ def test_draw_regular_polygon(n_sides, polygon_name, args): ), ], ) -def test_compute_regular_polygon_vertices(n_sides, expected_vertices): +def test_compute_regular_polygon_vertices( + n_sides: int, expected_vertices: list[tuple[float, float]] +) -> None: bounding_circle = (W // 2, H // 2, 25) vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) assert vertices == expected_vertices @@ -1479,7 +1504,7 @@ def test_compute_regular_polygon_vertices(n_sides, expected_vertices): [ (None, (50, 50, 25), 0, TypeError, "n_sides should be an int"), (1, (50, 50, 25), 0, ValueError, "n_sides should be an int > 2"), - (3, 50, 0, TypeError, "bounding_circle should be a tuple"), + (3, 50, 0, TypeError, "bounding_circle should be a sequence"), ( 3, (50, 50, 100, 100), @@ -1520,13 +1545,13 @@ def test_compute_regular_polygon_vertices(n_sides, expected_vertices): ) def test_compute_regular_polygon_vertices_input_error_handling( n_sides, bounding_circle, rotation, expected_error, error_message -): +) -> None: with pytest.raises(expected_error) as e: ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) assert str(e.value) == error_message -def test_continuous_horizontal_edges_polygon(): +def test_continuous_horizontal_edges_polygon() -> None: xy = [ (2, 6), (6, 6), @@ -1545,7 +1570,7 @@ def test_continuous_horizontal_edges_polygon(): ) -def test_discontiguous_corners_polygon(): +def test_discontiguous_corners_polygon() -> None: 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) @@ -1557,7 +1582,7 @@ def test_discontiguous_corners_polygon(): assert_image_similar_tofile(img, expected, 1) -def test_polygon2(): +def test_polygon2() -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") @@ -1566,7 +1591,7 @@ def test_polygon2(): @pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) -def test_incorrectly_ordered_coordinates(xy): +def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) with pytest.raises(ValueError): diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index d729af14d..3171eb9ae 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -1,9 +1,11 @@ from __future__ import annotations + import os.path import pytest from PIL import Image, ImageDraw, ImageDraw2, features +from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -42,7 +44,7 @@ POINTS = ( FONT_PATH = "Tests/fonts/FreeMono.ttf" -def test_sanity(): +def test_sanity() -> None: im = hopper("RGB").copy() draw = ImageDraw2.Draw(im) @@ -55,7 +57,7 @@ def test_sanity(): @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(bbox): +def test_ellipse(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -69,7 +71,7 @@ def test_ellipse(bbox): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_RGB.png", 1) -def test_ellipse_edge(): +def test_ellipse_edge() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -83,7 +85,7 @@ def test_ellipse_edge(): @pytest.mark.parametrize("points", POINTS) -def test_line(points): +def test_line(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -97,7 +99,7 @@ def test_line(points): @pytest.mark.parametrize("points", POINTS) -def test_line_pen_as_brush(points): +def test_line_pen_as_brush(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -113,7 +115,7 @@ def test_line_pen_as_brush(points): @pytest.mark.parametrize("points", POINTS) -def test_polygon(points): +def test_polygon(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -128,7 +130,7 @@ def test_polygon(points): @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox): +def test_rectangle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -142,7 +144,7 @@ def test_rectangle(bbox): assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle.png") -def test_big_rectangle(): +def test_big_rectangle() -> None: # Test drawing a rectangle bigger than the image # Arrange im = Image.new("RGB", (W, H)) @@ -159,7 +161,7 @@ def test_big_rectangle(): @skip_unless_feature("freetype2") -def test_text(): +def test_text() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -174,7 +176,7 @@ def test_text(): @skip_unless_feature("freetype2") -def test_textbbox(): +def test_textbbox() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -189,7 +191,7 @@ def test_textbbox(): @skip_unless_feature("freetype2") -def test_textsize_empty_string(): +def test_textsize_empty_string() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -205,7 +207,7 @@ def test_textsize_empty_string(): @skip_unless_feature("freetype2") -def test_flush(): +def test_flush() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index f4e4d59be..6ebc61e1b 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageEnhance @@ -6,7 +7,7 @@ from PIL import Image, ImageEnhance from .helper import assert_image_equal, hopper -def test_sanity(): +def test_sanity() -> None: # FIXME: assert_image # Implicit asserts no exception: ImageEnhance.Color(hopper()).enhance(0.5) @@ -15,13 +16,13 @@ def test_sanity(): ImageEnhance.Sharpness(hopper()).enhance(0.5) -def test_crash(): +def test_crash() -> None: # crashes on small images im = Image.new("RGB", (1, 1)) ImageEnhance.Sharpness(im).enhance(0.5) -def _half_transparent_image(): +def _half_transparent_image() -> Image.Image: # returns an image, half transparent, half solid im = hopper("RGB") @@ -33,7 +34,9 @@ def _half_transparent_image(): return im -def _check_alpha(im, original, op, amount): +def _check_alpha( + im: Image.Image, original: Image.Image, op: str, amount: float +) -> None: assert im.getbands() == original.getbands() assert_image_equal( im.getchannel("A"), @@ -43,7 +46,7 @@ def _check_alpha(im, original, op, amount): @pytest.mark.parametrize("op", ("Color", "Brightness", "Contrast", "Sharpness")) -def test_alpha(op): +def test_alpha(op: str) -> None: # Issue https://github.com/python-pillow/Pillow/issues/899 # Is alpha preserved through image enhancement? diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 4804a554f..44521a8b3 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO import pytest @@ -29,8 +30,8 @@ SAFEBLOCK = ImageFile.SAFEBLOCK class TestImageFile: - def test_parser(self): - def roundtrip(format): + def test_parser(self) -> None: + def roundtrip(format: str) -> tuple[Image.Image, Image.Image]: im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") @@ -83,7 +84,7 @@ class TestImageFile: with pytest.raises(OSError): roundtrip("PDF") - def test_ico(self): + def test_ico(self) -> None: with open("Tests/images/python.ico", "rb") as f: data = f.read() with ImageFile.Parser() as p: @@ -92,7 +93,7 @@ class TestImageFile: @skip_unless_feature("webp") @skip_unless_feature("webp_anim") - def test_incremental_webp(self): + def test_incremental_webp(self) -> None: with ImageFile.Parser() as p: with open("Tests/images/hopper.webp", "rb") as f: p.feed(f.read(1024)) @@ -104,7 +105,7 @@ class TestImageFile: assert (128, 128) == p.image.size @skip_unless_feature("zlib") - def test_safeblock(self): + def test_safeblock(self) -> None: im1 = hopper() try: @@ -115,17 +116,17 @@ class TestImageFile: assert_image_equal(im1, im2) - def test_raise_oserror(self): + def test_raise_oserror(self) -> None: with pytest.warns(DeprecationWarning): with pytest.raises(OSError): ImageFile.raise_oserror(1) - def test_raise_typeerror(self): + def test_raise_typeerror(self) -> None: with pytest.raises(TypeError): parser = ImageFile.Parser() parser.feed(1) - def test_negative_stride(self): + def test_negative_stride(self) -> None: with open("Tests/images/raw_negative_stride.bin", "rb") as f: input = f.read() p = ImageFile.Parser() @@ -133,11 +134,11 @@ class TestImageFile: with pytest.raises(OSError): p.close() - def test_no_format(self): + def test_no_format(self) -> None: buf = BytesIO(b"\x00" * 255) class DummyImageFile(ImageFile.ImageFile): - def _open(self): + def _open(self) -> None: self._mode = "RGB" self._size = (1, 1) @@ -145,12 +146,12 @@ class TestImageFile: assert im.format is None assert im.get_format_mimetype() is None - def test_oserror(self): + def test_oserror(self) -> None: im = Image.new("RGB", (1, 1)) with pytest.raises(OSError): im.save(BytesIO(), "JPEG2000", num_resolutions=2) - def test_truncated(self): + def test_truncated(self) -> None: b = BytesIO( b"BM000000000000" # head_data + _binary.o32le( @@ -165,7 +166,7 @@ class TestImageFile: assert str(e.value) == "Truncated File Read" @skip_unless_feature("zlib") - def test_truncated_with_errors(self): + def test_truncated_with_errors(self) -> None: with Image.open("Tests/images/truncated_image.png") as im: with pytest.raises(OSError): im.load() @@ -175,7 +176,7 @@ class TestImageFile: im.load() @skip_unless_feature("zlib") - def test_truncated_without_errors(self): + def test_truncated_without_errors(self) -> None: with Image.open("Tests/images/truncated_image.png") as im: ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -184,13 +185,13 @@ class TestImageFile: ImageFile.LOAD_TRUNCATED_IMAGES = False @skip_unless_feature("zlib") - def test_broken_datastream_with_errors(self): + def test_broken_datastream_with_errors(self) -> None: with Image.open("Tests/images/broken_data_stream.png") as im: with pytest.raises(OSError): im.load() @skip_unless_feature("zlib") - def test_broken_datastream_without_errors(self): + def test_broken_datastream_without_errors(self) -> None: with Image.open("Tests/images/broken_data_stream.png") as im: ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -209,7 +210,7 @@ class MockPyEncoder(ImageFile.PyEncoder): def encode(self, buffer): return 1, 1, b"" - def cleanup(self): + def cleanup(self) -> None: self.cleanup_called = True @@ -217,7 +218,7 @@ xoff, yoff, xsize, ysize = 10, 20, 100, 100 class MockImageFile(ImageFile.ImageFile): - def _open(self): + def _open(self) -> None: self.rawmode = "RGBA" self._mode = "RGBA" self._size = (200, 200) @@ -226,7 +227,7 @@ class MockImageFile(ImageFile.ImageFile): class CodecsTest: @classmethod - def setup_class(cls): + def setup_class(cls) -> None: cls.decoder = MockPyDecoder(None) cls.encoder = MockPyEncoder(None) @@ -243,7 +244,7 @@ class CodecsTest: class TestPyDecoder(CodecsTest): - def test_setimage(self): + def test_setimage(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -258,7 +259,7 @@ class TestPyDecoder(CodecsTest): with pytest.raises(ValueError): self.decoder.set_as_raw(b"\x00") - def test_extents_none(self): + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -271,7 +272,7 @@ class TestPyDecoder(CodecsTest): assert self.decoder.state.xsize == 200 assert self.decoder.state.ysize == 200 - def test_negsize(self): + def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -284,7 +285,7 @@ class TestPyDecoder(CodecsTest): with pytest.raises(ValueError): im.load() - def test_oversize(self): + def test_oversize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -297,14 +298,14 @@ class TestPyDecoder(CodecsTest): with pytest.raises(ValueError): im.load() - def test_decode(self): + def test_decode(self) -> None: decoder = ImageFile.PyDecoder(None) with pytest.raises(NotImplementedError): decoder.decode(None) class TestPyEncoder(CodecsTest): - def test_setimage(self): + def test_setimage(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -319,7 +320,7 @@ class TestPyEncoder(CodecsTest): assert self.encoder.state.xsize == xsize assert self.encoder.state.ysize == ysize - def test_extents_none(self): + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -333,7 +334,7 @@ class TestPyEncoder(CodecsTest): assert self.encoder.state.xsize == 200 assert self.encoder.state.ysize == 200 - def test_negsize(self): + def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -351,7 +352,7 @@ class TestPyEncoder(CodecsTest): im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] ) - def test_oversize(self): + def test_oversize(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -371,7 +372,7 @@ class TestPyEncoder(CodecsTest): [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], ) - def test_encode(self): + def test_encode(self) -> None: encoder = ImageFile.PyEncoder(None) with pytest.raises(NotImplementedError): encoder.encode(None) @@ -387,6 +388,6 @@ class TestPyEncoder(CodecsTest): with pytest.raises(NotImplementedError): encoder.encode_to_file(None, None) - def test_zero_height(self): + def test_zero_height(self) -> None: with pytest.raises(UnidentifiedImageError): Image.open("Tests/images/zero_height.j2k") diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 6ad56e2b1..05b5d4716 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,4 +1,5 @@ from __future__ import annotations + import copy import os import re @@ -6,11 +7,13 @@ import shutil import sys from io import BytesIO from pathlib import Path +from typing import Any, BinaryIO import pytest from packaging.version import parse as parse_version from PIL import Image, ImageDraw, ImageFont, features +from PIL._typing import StrOrBytesPath from .helper import ( assert_image_equal, @@ -30,7 +33,7 @@ TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" pytestmark = skip_unless_feature("freetype2") -def test_sanity(): +def test_sanity() -> None: assert re.search(r"\d+\.\d+\.\d+$", features.version_module("freetype2")) @@ -41,16 +44,16 @@ def test_sanity(): pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), ], ) -def layout_engine(request): +def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout: return request.param @pytest.fixture(scope="module") -def font(layout_engine): +def font(layout_engine: ImageFont.Layout) -> ImageFont.FreeTypeFont: return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=layout_engine) -def test_font_properties(font): +def test_font_properties(font: ImageFont.FreeTypeFont) -> None: assert font.path == FONT_PATH assert font.size == FONT_SIZE @@ -66,7 +69,9 @@ def test_font_properties(font): assert font_copy.path == second_font_path -def _render(font, layout_engine): +def _render( + font: StrOrBytesPath | BinaryIO, layout_engine: ImageFont.Layout +) -> Image.Image: txt = "Hello World!" ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=layout_engine) ttf.getbbox(txt) @@ -79,12 +84,12 @@ def _render(font, layout_engine): @pytest.mark.parametrize("font", (FONT_PATH, Path(FONT_PATH))) -def test_font_with_name(layout_engine, font): +def test_font_with_name(layout_engine: ImageFont.Layout, font: str | Path) -> None: _render(font, layout_engine) -def test_font_with_filelike(layout_engine): - def _font_as_bytes(): +def test_font_with_filelike(layout_engine: ImageFont.Layout) -> None: + def _font_as_bytes() -> BytesIO: with open(FONT_PATH, "rb") as f: font_bytes = BytesIO(f.read()) return font_bytes @@ -101,12 +106,12 @@ def test_font_with_filelike(layout_engine): # _render(shared_bytes) -def test_font_with_open_file(layout_engine): +def test_font_with_open_file(layout_engine: ImageFont.Layout) -> None: with open(FONT_PATH, "rb") as f: _render(f, layout_engine) -def test_render_equal(layout_engine): +def test_render_equal(layout_engine: ImageFont.Layout) -> None: img_path = _render(FONT_PATH, layout_engine) with open(FONT_PATH, "rb") as f: font_filelike = BytesIO(f.read()) @@ -115,7 +120,7 @@ def test_render_equal(layout_engine): assert_image_equal(img_path, img_filelike) -def test_non_ascii_path(tmp_path, layout_engine): +def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) try: shutil.copy(FONT_PATH, tempfile) @@ -125,7 +130,7 @@ def test_non_ascii_path(tmp_path, layout_engine): ImageFont.truetype(tempfile, FONT_SIZE, layout_engine=layout_engine) -def test_transparent_background(font): +def test_transparent_background(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGBA", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -139,7 +144,7 @@ def test_transparent_background(font): assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_I16(font): +def test_I16(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="I;16", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -152,7 +157,7 @@ def test_I16(font): assert_image_similar_tofile(im.convert("L"), target, 0.01) -def test_textbbox_equal(font): +def test_textbbox_equal(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -180,8 +185,14 @@ def test_textbbox_equal(font): ), ) def test_getlength( - text, mode, fontname, size, layout_engine, length_basic, length_raqm -): + text: str, + mode: str, + fontname: str, + size: int, + layout_engine: ImageFont.Layout, + length_basic: int, + length_raqm: float, +) -> None: f = ImageFont.truetype("Tests/fonts/" + fontname, size, layout_engine=layout_engine) im = Image.new(mode, (1, 1), 0) @@ -196,7 +207,7 @@ def test_getlength( assert length == length_raqm -def test_float_size(): +def test_float_size() -> None: lengths = [] for size in (48, 48.5, 49): f = ImageFont.truetype( @@ -206,7 +217,7 @@ def test_float_size(): assert lengths[0] != lengths[1] != lengths[2] -def test_render_multiline(font): +def test_render_multiline(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) line_spacing = font.getbbox("A")[3] + 4 @@ -222,7 +233,7 @@ def test_render_multiline(font): assert_image_similar_tofile(im, "Tests/images/multiline_text.png", 6.2) -def test_render_multiline_text(font): +def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None: # Test that text() correctly connects to multiline_text() # and that align defaults to left im = Image.new(mode="RGB", size=(300, 100)) @@ -242,7 +253,9 @@ def test_render_multiline_text(font): @pytest.mark.parametrize( "align, ext", (("left", ""), ("center", "_center"), ("right", "_right")) ) -def test_render_multiline_text_align(font, align, ext): +def test_render_multiline_text_align( + font: ImageFont.FreeTypeFont, align: str, ext: str +) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, align=align) @@ -250,7 +263,7 @@ def test_render_multiline_text_align(font, align, ext): assert_image_similar_tofile(im, f"Tests/images/multiline_text{ext}.png", 0.01) -def test_unknown_align(font): +def test_unknown_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -259,14 +272,14 @@ def test_unknown_align(font): draw.multiline_text((0, 0), TEST_TEXT, font=font, align="unknown") -def test_draw_align(font): +def test_draw_align(font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (300, 100), "white") draw = ImageDraw.Draw(im) line = "some text" draw.text((100, 40), line, (0, 0, 0), font=font, align="left") -def test_multiline_bbox(font): +def test_multiline_bbox(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -284,7 +297,7 @@ def test_multiline_bbox(font): draw.textbbox((0, 0), TEST_TEXT, font=font, spacing=4) -def test_multiline_width(font): +def test_multiline_width(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -294,7 +307,7 @@ def test_multiline_width(font): ) -def test_multiline_spacing(font): +def test_multiline_spacing(font: ImageFont.FreeTypeFont) -> None: im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) draw.multiline_text((0, 0), TEST_TEXT, font=font, spacing=10) @@ -305,7 +318,9 @@ def test_multiline_spacing(font): @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font(font, orientation): +def test_rotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -346,7 +361,9 @@ def test_rotated_transposed_font(font, orientation): Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font(font, orientation): +def test_unrotated_transposed_font( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: img_gray = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_gray) word = "testing" @@ -381,7 +398,9 @@ def test_unrotated_transposed_font(font, orientation): @pytest.mark.parametrize( "orientation", (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270) ) -def test_rotated_transposed_font_get_mask(font, orientation): +def test_rotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -402,7 +421,9 @@ def test_rotated_transposed_font_get_mask(font, orientation): Image.Transpose.FLIP_TOP_BOTTOM, ), ) -def test_unrotated_transposed_font_get_mask(font, orientation): +def test_unrotated_transposed_font_get_mask( + font: ImageFont.FreeTypeFont, orientation: Image.Transpose +) -> None: # Arrange text = "mask this" transposed_font = ImageFont.TransposedFont(font, orientation=orientation) @@ -414,11 +435,11 @@ def test_unrotated_transposed_font_get_mask(font, orientation): assert mask.size == (108, 13) -def test_free_type_font_get_name(font): +def test_free_type_font_get_name(font: ImageFont.FreeTypeFont) -> None: assert ("FreeMono", "Regular") == font.getname() -def test_free_type_font_get_metrics(font): +def test_free_type_font_get_metrics(font: ImageFont.FreeTypeFont) -> None: ascent, descent = font.getmetrics() assert isinstance(ascent, int) @@ -426,7 +447,7 @@ def test_free_type_font_get_metrics(font): assert (ascent, descent) == (16, 4) -def test_free_type_font_get_mask(font): +def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None: # Arrange text = "mask this" @@ -437,7 +458,7 @@ def test_free_type_font_get_mask(font): assert mask.size == (108, 13) -def test_load_path_not_found(): +def test_load_path_not_found() -> None: # Arrange filename = "somefilenamethatdoesntexist.ttf" @@ -448,13 +469,13 @@ def test_load_path_not_found(): ImageFont.truetype(filename) -def test_load_non_font_bytes(): +def test_load_non_font_bytes() -> None: with open("Tests/images/hopper.jpg", "rb") as f: with pytest.raises(OSError): ImageFont.truetype(f) -def test_default_font(): +def test_default_font() -> None: # Arrange txt = "This is a default font using FreeType support." im = Image.new(mode="RGB", size=(300, 100)) @@ -472,16 +493,16 @@ def test_default_font(): @pytest.mark.parametrize("mode", (None, "1", "RGBA")) -def test_getbbox(font, mode): +def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None: assert (0, 4, 12, 16) == font.getbbox("A", mode) -def test_getbbox_empty(font): +def test_getbbox_empty(font: ImageFont.FreeTypeFont) -> None: # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") -def test_render_empty(font): +def test_render_empty(font: ImageFont.FreeTypeFont) -> None: # issue 2666 im = Image.new(mode="RGB", size=(300, 100)) target = im.copy() @@ -491,7 +512,7 @@ def test_render_empty(font): assert_image_equal(im, target) -def test_unicode_extended(layout_engine): +def test_unicode_extended(layout_engine: ImageFont.Layout) -> None: # issue #3777 text = "A\u278A\U0001F12B" target = "Tests/images/unicode_extended.png" @@ -514,21 +535,23 @@ def test_unicode_extended(layout_engine): (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), ) @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") -def test_find_font(monkeypatch, platform, font_directory): - def _test_fake_loading_font(path_to_fake, fontname): +def test_find_font( + monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str +) -> None: + def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with monkeypatch.context() as m: m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) - def loadable_font(filepath, size, index, encoding, *args, **kwargs): + def loadable_font( + filepath: str, size: int, index: int, encoding: str, *args: Any + ): if filepath == path_to_fake: return ImageFont._FreeTypeFont( - FONT_PATH, size, index, encoding, *args, **kwargs + FONT_PATH, size, index, encoding, *args ) - return ImageFont._FreeTypeFont( - filepath, size, index, encoding, *args, **kwargs - ) + return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args) m.setattr(ImageFont, "FreeTypeFont", loadable_font) font = ImageFont.truetype(fontname) @@ -542,7 +565,7 @@ def test_find_font(monkeypatch, platform, font_directory): if platform == "linux": monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") - def fake_walker(path): + def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]: if path == font_directory: return [ ( @@ -566,7 +589,7 @@ def test_find_font(monkeypatch, platform, font_directory): _test_fake_loading_font(font_directory + "/Duplicate.ttf", "Duplicate") -def test_imagefont_getters(font): +def test_imagefont_getters(font: ImageFont.FreeTypeFont) -> None: assert font.getmetrics() == (16, 4) assert font.font.ascent == 16 assert font.font.descent == 4 @@ -587,7 +610,7 @@ def test_imagefont_getters(font): @pytest.mark.parametrize("stroke_width", (0, 2)) -def test_getsize_stroke(font, stroke_width): +def test_getsize_stroke(font: ImageFont.FreeTypeFont, stroke_width: int) -> None: assert font.getbbox("A", stroke_width=stroke_width) == ( 0 - stroke_width, 4 - stroke_width, @@ -596,7 +619,7 @@ def test_getsize_stroke(font, stroke_width): ) -def test_complex_font_settings(): +def test_complex_font_settings() -> None: t = ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.BASIC) with pytest.raises(KeyError): t.getmask("абвг", direction="rtl") @@ -606,7 +629,7 @@ def test_complex_font_settings(): t.getmask("абвг", language="sr") -def test_variation_get(font): +def test_variation_get(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -661,7 +684,7 @@ def test_variation_get(font): ] -def _check_text(font, path, epsilon): +def _check_text(font: ImageFont.FreeTypeFont, path: str, epsilon: float) -> None: im = Image.new("RGB", (100, 75), "white") d = ImageDraw.Draw(im) d.text((10, 10), "Text", font=font, fill="black") @@ -676,7 +699,7 @@ def _check_text(font, path, epsilon): raise -def test_variation_set_by_name(font): +def test_variation_set_by_name(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -701,7 +724,7 @@ def test_variation_set_by_name(font): _check_text(font, "Tests/images/variation_tiny_name.png", 40) -def test_variation_set_by_axes(font): +def test_variation_set_by_axes(font: ImageFont.FreeTypeFont) -> None: freetype = parse_version(features.version_module("freetype2")) if freetype < parse_version("2.9.1"): with pytest.raises(NotImplementedError): @@ -736,7 +759,9 @@ def test_variation_set_by_axes(font): ), ids=("ls", "ms", "rs", "ma", "mt", "mm", "mb", "md"), ) -def test_anchor(layout_engine, anchor, left, top): +def test_anchor( + layout_engine: ImageFont.Layout, anchor: str, left: int, top: int +) -> None: name, text = "quick", "Quick" path = f"Tests/images/test_anchor_{name}_{anchor}.png" @@ -781,7 +806,9 @@ def test_anchor(layout_engine, anchor, left, top): ("md", "center"), ), ) -def test_anchor_multiline(layout_engine, anchor, align): +def test_anchor_multiline( + layout_engine: ImageFont.Layout, anchor: str, align: str +) -> None: target = f"Tests/images/test_anchor_multiline_{anchor}_{align}.png" text = "a\nlong\ntext sample" @@ -799,7 +826,7 @@ def test_anchor_multiline(layout_engine, anchor, align): assert_image_similar_tofile(im, target, 4) -def test_anchor_invalid(font): +def test_anchor_invalid(font: ImageFont.FreeTypeFont) -> None: im = Image.new("RGB", (100, 100), "white") d = ImageDraw.Draw(im) d.font = font @@ -825,7 +852,7 @@ def test_anchor_invalid(font): @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) -def test_bitmap_font(layout_engine, bpp): +def test_bitmap_font(layout_engine: ImageFont.Layout, bpp: int) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_{bpp}_{layout_name}.png" @@ -842,7 +869,7 @@ def test_bitmap_font(layout_engine, bpp): assert_image_equal_tofile(im, target) -def test_bitmap_font_stroke(layout_engine): +def test_bitmap_font_stroke(layout_engine: ImageFont.Layout) -> None: text = "Bitmap Font" layout_name = ["basic", "raqm"][layout_engine] target = f"Tests/images/bitmap_font_stroke_{layout_name}.png" @@ -859,7 +886,20 @@ def test_bitmap_font_stroke(layout_engine): assert_image_similar_tofile(im, target, 0.03) -def test_standard_embedded_color(layout_engine): +@pytest.mark.parametrize("embedded_color", (False, True)) +def test_bitmap_blend(layout_engine: ImageFont.Layout, embedded_color: bool) -> None: + font = ImageFont.truetype( + "Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine + ) + + im = Image.new("RGBA", (128, 96), "white") + d = ImageDraw.Draw(im) + d.text((16, 16), "AA", font=font, fill="#8E2F52", embedded_color=embedded_color) + + assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png") + + +def test_standard_embedded_color(layout_engine: ImageFont.Layout) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) ttf.getbbox(txt) @@ -872,7 +912,7 @@ def test_standard_embedded_color(layout_engine): @pytest.mark.parametrize("fontmode", ("1", "L", "RGBA")) -def test_float_coord(layout_engine, fontmode): +def test_float_coord(layout_engine: ImageFont.Layout, fontmode: str) -> None: txt = "Hello World!" ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine) @@ -894,43 +934,41 @@ def test_float_coord(layout_engine, fontmode): raise -def test_cbdt(layout_engine): +def test_cbdt(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( - "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine + "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine ) - im = Image.new("RGB", (150, 150), "white") + im = Image.new("RGB", (128, 96), "white") d = ImageDraw.Draw(im) - d.text((10, 10), "\U0001f469", font=font, embedded_color=True) + d.text((16, 16), "AB", font=font, embedded_color=True) - assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2) + assert_image_equal_tofile(im, "Tests/images/cbdt.png") 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") -def test_cbdt_mask(layout_engine): +def test_cbdt_mask(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( - "Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine + "Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine ) - im = Image.new("RGB", (150, 150), "white") + im = Image.new("RGB", (128, 96), "white") d = ImageDraw.Draw(im) - d.text((10, 10), "\U0001f469", "black", font=font) + d.text((16, 16), "AB", "green", font=font) - assert_image_similar_tofile( - im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2 - ) + assert_image_equal_tofile(im, "Tests/images/cbdt_mask.png") 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") -def test_sbix(layout_engine): +def test_sbix(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -947,7 +985,7 @@ def test_sbix(layout_engine): pytest.skip("freetype compiled without libpng or SBIX support") -def test_sbix_mask(layout_engine): +def test_sbix_mask(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/chromacheck-sbix.woff", size=300, layout_engine=layout_engine @@ -965,7 +1003,7 @@ def test_sbix_mask(layout_engine): @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr(layout_engine): +def test_colr(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -981,7 +1019,7 @@ def test_colr(layout_engine): @skip_unless_feature_version("freetype2", "2.10.0") -def test_colr_mask(layout_engine): +def test_colr_mask(layout_engine: ImageFont.Layout) -> None: font = ImageFont.truetype( "Tests/fonts/BungeeColor-Regular_colr_Windows.ttf", size=64, @@ -996,7 +1034,7 @@ def test_colr_mask(layout_engine): assert_image_similar_tofile(im, "Tests/images/colr_bungee_mask.png", 22) -def test_woff2(layout_engine): +def test_woff2(layout_engine: ImageFont.Layout) -> None: try: font = ImageFont.truetype( "Tests/fonts/OpenSans.woff2", @@ -1015,7 +1053,7 @@ def test_woff2(layout_engine): assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5) -def test_render_mono_size(): +def test_render_mono_size() -> None: # issue 4177 im = Image.new("P", (100, 30), "white") @@ -1030,7 +1068,7 @@ def test_render_mono_size(): assert_image_equal_tofile(im, "Tests/images/text_mono.gif") -def test_too_many_characters(font): +def test_too_many_characters(font: ImageFont.FreeTypeFont) -> None: with pytest.raises(ValueError): font.getlength("A" * 1_000_001) with pytest.raises(ValueError): @@ -1042,11 +1080,13 @@ def test_too_many_characters(font): with pytest.raises(ValueError): transposed_font.getlength("A" * 1_000_001) - default_font = ImageFont.load_default() + imagefont = ImageFont.ImageFont() with pytest.raises(ValueError): - default_font.getlength("A" * 1_000_001) + imagefont.getlength("A" * 1_000_001) with pytest.raises(ValueError): - default_font.getbbox("A" * 1_000_001) + imagefont.getbbox("A" * 1_000_001) + with pytest.raises(ValueError): + imagefont.getmask("A" * 1_000_001) @pytest.mark.parametrize( @@ -1056,14 +1096,14 @@ def test_too_many_characters(font): "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", ], ) -def test_oom(test_file): +def test_oom(test_file: str) -> None: with open(test_file, "rb") as f: font = ImageFont.truetype(BytesIO(f.read())) with pytest.raises(Image.DecompressionBombError): font.getmask("Test Text") -def test_raqm_missing_warning(monkeypatch): +def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) with pytest.warns(UserWarning) as record: font = ImageFont.truetype( @@ -1077,6 +1117,8 @@ def test_raqm_missing_warning(monkeypatch): @pytest.mark.parametrize("size", [-1, 0]) -def test_invalid_truetype_sizes_raise_valueerror(layout_engine, size): +def test_invalid_truetype_sizes_raise_valueerror( + layout_engine: ImageFont.Layout, size: int +) -> None: with pytest.raises(ValueError): ImageFont.truetype(FONT_PATH, size, layout_engine=layout_engine) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index bea532b05..24c7b871a 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageDraw, ImageFont @@ -11,7 +12,7 @@ FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" pytestmark = skip_unless_feature("raqm") -def test_english(): +def test_english() -> None: # smoke test, this should not fail ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -19,7 +20,7 @@ def test_english(): draw.text((0, 0), "TEST", font=ttf, fill=500, direction="ltr") -def test_complex_text(): +def test_complex_text() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -30,7 +31,7 @@ def test_complex_text(): assert_image_similar_tofile(im, target, 0.5) -def test_y_offset(): +def test_y_offset() -> None: ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -41,7 +42,7 @@ def test_y_offset(): assert_image_similar_tofile(im, target, 1.7) -def test_complex_unicode_text(): +def test_complex_unicode_text() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -61,7 +62,7 @@ def test_complex_unicode_text(): assert_image_similar_tofile(im, target, 2.33) -def test_text_direction_rtl(): +def test_text_direction_rtl() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -72,7 +73,7 @@ def test_text_direction_rtl(): assert_image_similar_tofile(im, target, 0.5) -def test_text_direction_ltr(): +def test_text_direction_ltr() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -83,7 +84,7 @@ def test_text_direction_ltr(): assert_image_similar_tofile(im, target, 0.5) -def test_text_direction_rtl2(): +def test_text_direction_rtl2() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -94,7 +95,7 @@ def test_text_direction_rtl2(): assert_image_similar_tofile(im, target, 0.5) -def test_text_direction_ttb(): +def test_text_direction_ttb() -> None: ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", FONT_SIZE) im = Image.new(mode="RGB", size=(100, 300)) @@ -109,7 +110,7 @@ def test_text_direction_ttb(): assert_image_similar_tofile(im, target, 2.8) -def test_text_direction_ttb_stroke(): +def test_text_direction_ttb_stroke() -> None: ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50) im = Image.new(mode="RGB", size=(100, 300)) @@ -132,7 +133,7 @@ def test_text_direction_ttb_stroke(): assert_image_similar_tofile(im, target, 19.4) -def test_ligature_features(): +def test_ligature_features() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -145,7 +146,7 @@ def test_ligature_features(): assert liga_bbox == (0, 4, 13, 19) -def test_kerning_features(): +def test_kerning_features() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -156,7 +157,7 @@ def test_kerning_features(): assert_image_similar_tofile(im, target, 0.5) -def test_arabictext_features(): +def test_arabictext_features() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -173,7 +174,7 @@ def test_arabictext_features(): assert_image_similar_tofile(im, target, 0.5) -def test_x_max_and_y_offset(): +def test_x_max_and_y_offset() -> None: ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40) im = Image.new(mode="RGB", size=(50, 100)) @@ -184,7 +185,7 @@ def test_x_max_and_y_offset(): assert_image_similar_tofile(im, target, 0.5) -def test_language(): +def test_language() -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode="RGB", size=(300, 100)) @@ -207,7 +208,9 @@ def test_language(): ), ids=("None", "ltr", "rtl2", "rtl", "ttb"), ) -def test_getlength(mode, text, direction, expected): +def test_getlength( + mode: str, text: str, direction: str | None, expected: float +) -> None: ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode, (1, 1), 0) d = ImageDraw.Draw(im) @@ -229,7 +232,7 @@ def test_getlength(mode, text, direction, expected): ("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"), ids=("caron-above", "caron-below", "double-breve", "overline"), ) -def test_getlength_combine(mode, direction, text): +def test_getlength_combine(mode: str, direction: str, text: str) -> None: if text == "i\u0305i" and direction == "ttb": pytest.skip("fails with this font") @@ -249,7 +252,7 @@ def test_getlength_combine(mode, direction, text): @pytest.mark.parametrize("anchor", ("lt", "mm", "rb", "sm")) -def test_anchor_ttb(anchor): +def test_anchor_ttb(anchor: str) -> None: text = "f" path = f"Tests/images/test_anchor_ttb_{text}_{anchor}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 120) @@ -305,7 +308,9 @@ combine_tests = ( @pytest.mark.parametrize( "name, text, anchor, dir, epsilon", combine_tests, ids=[r[0] for r in combine_tests] ) -def test_combine(name, text, dir, anchor, epsilon): +def test_combine( + name: str, text: str, dir: str | None, anchor: str | None, epsilon: float +) -> None: path = f"Tests/images/test_combine_{name}.png" f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48) @@ -336,7 +341,7 @@ def test_combine(name, text, dir, anchor, epsilon): ("rm", "right"), # pass with getsize ), ) -def test_combine_multiline(anchor, align): +def test_combine_multiline(anchor: str, align: str) -> None: # test that multiline text uses getlength, not getsize or getbbox path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png" @@ -354,7 +359,7 @@ def test_combine_multiline(anchor, align): assert_image_similar_tofile(im, path, 0.015) -def test_anchor_invalid_ttb(): +def test_anchor_invalid_ttb() -> None: font = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new("RGB", (100, 100), "white") d = ImageDraw.Draw(im) diff --git a/Tests/test_imagefontpil.py b/Tests/test_imagefontpil.py index 21b4dee3c..3b1c14b4e 100644 --- a/Tests/test_imagefontpil.py +++ b/Tests/test_imagefontpil.py @@ -1,17 +1,27 @@ from __future__ import annotations + +import struct +from io import BytesIO + import pytest -from PIL import Image, ImageDraw, ImageFont, features +from PIL import Image, ImageDraw, ImageFont, _util, features from .helper import assert_image_equal_tofile -pytestmark = pytest.mark.skipif( - features.check_module("freetype2"), - reason="PILfont superseded if FreeType is supported", -) +original_core = ImageFont.core -def test_default_font(): +def setup_module() -> None: + if features.check_module("freetype2"): + ImageFont.core = _util.DeferredError(ImportError) + + +def teardown_module() -> None: + ImageFont.core = original_core + + +def test_default_font() -> None: # Arrange txt = 'This is a "better than nothing" default font.' im = Image.new(mode="RGB", size=(300, 100)) @@ -25,12 +35,12 @@ def test_default_font(): assert_image_equal_tofile(im, "Tests/images/default_font.png") -def test_size_without_freetype(): +def test_size_without_freetype() -> None: with pytest.raises(ImportError): ImageFont.load_default(size=14) -def test_unicode(): +def test_unicode() -> None: # should not segfault, should return UnicodeDecodeError # issue #2826 font = ImageFont.load_default() @@ -38,9 +48,31 @@ def test_unicode(): font.getbbox("’") -def test_textbbox(): +def test_textbbox() -> None: im = Image.new("RGB", (200, 200)) d = ImageDraw.Draw(im) default_font = ImageFont.load_default() assert d.textlength("test", font=default_font) == 24 assert d.textbbox((0, 0), "test", font=default_font) == (0, 0, 24, 11) + + +def test_decompression_bomb() -> None: + glyph = struct.pack(">hhhhhhhhhh", 1, 0, 0, 0, 256, 256, 0, 0, 256, 256) + fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) + + font = ImageFont.ImageFont() + font._load_pilfont_data(fp, Image.new("L", (256, 256))) + with pytest.raises(Image.DecompressionBombError): + font.getmask("A" * 1_000_000) + + +@pytest.mark.timeout(4) +def test_oom() -> None: + glyph = struct.pack( + ">hhhhhhhhhh", 1, 0, -32767, -32767, 32767, 32767, -32767, -32767, 32767, 32767 + ) + fp = BytesIO(b"PILfont\n\nDATA\n" + glyph * 256) + + font = ImageFont.ImageFont() + font._load_pilfont_data(fp, Image.new("L", (1, 1))) + font.getmask("A" * 1_000_000) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index b7683ec18..e23adeb70 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,4 +1,5 @@ from __future__ import annotations + import os import shutil import subprocess @@ -19,7 +20,7 @@ class TestImageGrab: @pytest.mark.skipif( sys.platform not in ("win32", "darwin"), reason="requires Windows or macOS" ) - def test_grab(self): + def test_grab(self) -> None: ImageGrab.grab() ImageGrab.grab(include_layered_windows=True) ImageGrab.grab(all_screens=True) @@ -28,7 +29,7 @@ class TestImageGrab: assert im.size == (40, 60) @skip_unless_feature("xcb") - def test_grab_x11(self): + def test_grab_x11(self) -> None: try: if sys.platform not in ("win32", "darwin"): ImageGrab.grab() @@ -38,7 +39,7 @@ class TestImageGrab: pytest.skip(str(e)) @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") - def test_grab_no_xcb(self): + def test_grab_no_xcb(self) -> None: if sys.platform not in ("win32", "darwin") and not shutil.which( "gnome-screenshot" ): @@ -51,12 +52,12 @@ class TestImageGrab: assert str(e.value).startswith("Pillow was built without XCB support") @skip_unless_feature("xcb") - def test_grab_invalid_xdisplay(self): + def test_grab_invalid_xdisplay(self) -> None: with pytest.raises(OSError) as e: ImageGrab.grab(xdisplay="error.test:0.0") assert str(e.value).startswith("X connection failed") - def test_grabclipboard(self): + def test_grabclipboard(self) -> None: if sys.platform == "darwin": subprocess.call(["screencapture", "-cx"]) elif sys.platform == "win32": @@ -81,8 +82,9 @@ $bmp = New-Object Drawing.Bitmap 200, 200 ImageGrab.grabclipboard() @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") - def test_grabclipboard_file(self): + def test_grabclipboard_file(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') p.communicate() @@ -91,8 +93,9 @@ $bmp = New-Object Drawing.Bitmap 200, 200 assert os.path.samefile(im[0], "Tests/images/hopper.gif") @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") - def test_grabclipboard_png(self): + def test_grabclipboard_png(self) -> None: p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) + assert p.stdin is not None p.stdin.write( rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") $ms = new-object System.IO.MemoryStream(, $bytes) @@ -112,9 +115,21 @@ $ms = new-object System.IO.MemoryStream(, $bytes) reason="Linux with wl-clipboard only", ) @pytest.mark.parametrize("ext", ("gif", "png", "ico")) - def test_grabclipboard_wl_clipboard(self, ext): + def test_grabclipboard_wl_clipboard(self, ext: str) -> None: image_path = "Tests/images/hopper." + ext with open(image_path, "rb") as fp: subprocess.call(["wl-copy"], stdin=fp) im = ImageGrab.grabclipboard() assert_image_equal_tofile(im, image_path) + + @pytest.mark.skipif( + ( + sys.platform != "linux" + or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy")) + ), + reason="Linux with wl-clipboard only", + ) + @pytest.mark.parametrize("arg", ("text", "--clear")) + def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None: + subprocess.call(["wl-copy", arg]) + assert ImageGrab.grabclipboard() is None diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 22de86c7c..a21e2307d 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,15 +1,16 @@ from __future__ import annotations + import pytest from PIL import Image, ImageMath -def pixel(im): - if hasattr(im, "im"): - return f"{im.mode} {repr(im.getpixel((0, 0)))}" +def pixel(im: Image.Image | int) -> str | int: if isinstance(im, int): return int(im) # hack to deal with booleans + return f"{im.mode} {repr(im.getpixel((0, 0)))}" + A = Image.new("L", (1, 1), 1) B = Image.new("L", (1, 1), 2) @@ -23,7 +24,7 @@ B2 = B.resize((2, 2)) images = {"A": A, "B": B, "F": F, "I": I} -def test_sanity(): +def test_sanity() -> None: assert ImageMath.eval("1") == 1 assert ImageMath.eval("1+A", A=2) == 3 assert pixel(ImageMath.eval("A+B", A=A, B=B)) == "I 3" @@ -32,7 +33,7 @@ def test_sanity(): assert pixel(ImageMath.eval("int(float(A)+B)", images)) == "I 3" -def test_ops(): +def test_ops() -> None: assert pixel(ImageMath.eval("-A", images)) == "I -1" assert pixel(ImageMath.eval("+B", images)) == "L 2" @@ -59,41 +60,51 @@ def test_ops(): "(lambda: (lambda: exec('pass'))())()", ), ) -def test_prevent_exec(expression): +def test_prevent_exec(expression: str) -> None: with pytest.raises(ValueError): ImageMath.eval(expression) -def test_logical(): +def test_prevent_double_underscores() -> None: + with pytest.raises(ValueError): + ImageMath.eval("1", {"__": None}) + + +def test_prevent_builtins() -> None: + with pytest.raises(ValueError): + ImageMath.eval("(lambda: exec('exit()'))()", {"exec": None}) + + +def test_logical() -> None: assert pixel(ImageMath.eval("not A", images)) == 0 assert pixel(ImageMath.eval("A and B", images)) == "L 2" assert pixel(ImageMath.eval("A or B", images)) == "L 1" -def test_convert(): +def test_convert() -> None: assert pixel(ImageMath.eval("convert(A+B, 'L')", images)) == "L 3" assert pixel(ImageMath.eval("convert(A+B, '1')", images)) == "1 0" assert pixel(ImageMath.eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)" -def test_compare(): +def test_compare() -> None: assert pixel(ImageMath.eval("min(A, B)", images)) == "I 1" assert pixel(ImageMath.eval("max(A, B)", images)) == "I 2" assert pixel(ImageMath.eval("A == 1", images)) == "I 1" assert pixel(ImageMath.eval("A == 2", images)) == "I 0" -def test_one_image_larger(): +def test_one_image_larger() -> None: assert pixel(ImageMath.eval("A+B", A=A2, B=B)) == "I 3" assert pixel(ImageMath.eval("A+B", A=A, B=B2)) == "I 3" -def test_abs(): +def test_abs() -> None: assert pixel(ImageMath.eval("abs(A)", A=A)) == "I 1" assert pixel(ImageMath.eval("abs(B)", B=B)) == "I 2" -def test_binary_mod(): +def test_binary_mod() -> None: assert pixel(ImageMath.eval("A%A", A=A)) == "I 0" assert pixel(ImageMath.eval("B%B", B=B)) == "I 0" assert pixel(ImageMath.eval("A%B", A=A, B=B)) == "I 1" @@ -102,90 +113,90 @@ def test_binary_mod(): assert pixel(ImageMath.eval("Z%B", B=B, Z=Z)) == "I 0" -def test_bitwise_invert(): +def test_bitwise_invert() -> None: assert pixel(ImageMath.eval("~Z", Z=Z)) == "I -1" assert pixel(ImageMath.eval("~A", A=A)) == "I -2" assert pixel(ImageMath.eval("~B", B=B)) == "I -3" -def test_bitwise_and(): +def test_bitwise_and() -> None: assert pixel(ImageMath.eval("Z&Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z&A", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("A&Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("A&A", A=A, Z=Z)) == "I 1" -def test_bitwise_or(): +def test_bitwise_or() -> None: assert pixel(ImageMath.eval("Z|Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z|A", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A|Z", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A|A", A=A, Z=Z)) == "I 1" -def test_bitwise_xor(): +def test_bitwise_xor() -> None: assert pixel(ImageMath.eval("Z^Z", A=A, Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z^A", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A^Z", A=A, Z=Z)) == "I 1" assert pixel(ImageMath.eval("A^A", A=A, Z=Z)) == "I 0" -def test_bitwise_leftshift(): +def test_bitwise_leftshift() -> None: assert pixel(ImageMath.eval("Z<<0", Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z<<1", Z=Z)) == "I 0" assert pixel(ImageMath.eval("A<<0", A=A)) == "I 1" assert pixel(ImageMath.eval("A<<1", A=A)) == "I 2" -def test_bitwise_rightshift(): +def test_bitwise_rightshift() -> None: assert pixel(ImageMath.eval("Z>>0", Z=Z)) == "I 0" assert pixel(ImageMath.eval("Z>>1", Z=Z)) == "I 0" assert pixel(ImageMath.eval("A>>0", A=A)) == "I 1" assert pixel(ImageMath.eval("A>>1", A=A)) == "I 0" -def test_logical_eq(): +def test_logical_eq() -> None: assert pixel(ImageMath.eval("A==A", A=A)) == "I 1" assert pixel(ImageMath.eval("B==B", B=B)) == "I 1" assert pixel(ImageMath.eval("A==B", A=A, B=B)) == "I 0" assert pixel(ImageMath.eval("B==A", A=A, B=B)) == "I 0" -def test_logical_ne(): +def test_logical_ne() -> None: assert pixel(ImageMath.eval("A!=A", A=A)) == "I 0" assert pixel(ImageMath.eval("B!=B", B=B)) == "I 0" assert pixel(ImageMath.eval("A!=B", A=A, B=B)) == "I 1" assert pixel(ImageMath.eval("B!=A", A=A, B=B)) == "I 1" -def test_logical_lt(): +def test_logical_lt() -> None: assert pixel(ImageMath.eval("A None: assert pixel(ImageMath.eval("A<=A", A=A)) == "I 1" assert pixel(ImageMath.eval("B<=B", B=B)) == "I 1" assert pixel(ImageMath.eval("A<=B", A=A, B=B)) == "I 1" assert pixel(ImageMath.eval("B<=A", A=A, B=B)) == "I 0" -def test_logical_gt(): +def test_logical_gt() -> None: assert pixel(ImageMath.eval("A>A", A=A)) == "I 0" assert pixel(ImageMath.eval("B>B", B=B)) == "I 0" assert pixel(ImageMath.eval("A>B", A=A, B=B)) == "I 0" assert pixel(ImageMath.eval("B>A", A=A, B=B)) == "I 1" -def test_logical_ge(): +def test_logical_ge() -> None: assert pixel(ImageMath.eval("A>=A", A=A)) == "I 1" assert pixel(ImageMath.eval("B>=B", B=B)) == "I 1" assert pixel(ImageMath.eval("A>=B", A=A, B=B)) == "I 0" assert pixel(ImageMath.eval("B>=A", A=A, B=B)) == "I 1" -def test_logical_equal(): +def test_logical_equal() -> None: assert pixel(ImageMath.eval("equal(A, A)", A=A)) == "I 1" assert pixel(ImageMath.eval("equal(B, B)", B=B)) == "I 1" assert pixel(ImageMath.eval("equal(Z, Z)", Z=Z)) == "I 1" @@ -194,7 +205,7 @@ def test_logical_equal(): assert pixel(ImageMath.eval("equal(A, Z)", A=A, Z=Z)) == "I 0" -def test_logical_not_equal(): +def test_logical_not_equal() -> None: assert pixel(ImageMath.eval("notequal(A, A)", A=A)) == "I 0" assert pixel(ImageMath.eval("notequal(B, B)", B=B)) == "I 0" assert pixel(ImageMath.eval("notequal(Z, Z)", Z=Z)) == "I 0" diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index ec55aadf9..46b473d7a 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,5 +1,8 @@ # Test the ImageMorphology functionality from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image, ImageMorph, _imagingmorph @@ -7,7 +10,7 @@ from PIL import Image, ImageMorph, _imagingmorph from .helper import assert_image_equal_tofile, hopper -def string_to_img(image_string): +def string_to_img(image_string: str) -> Image.Image: """Turn a string image representation into a binary image""" rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)] height = len(rows) @@ -35,7 +38,7 @@ A = string_to_img( ) -def img_to_string(im): +def img_to_string(im: Image.Image) -> str: """Turn a (small) binary image into a string representation""" chars = ".1" width, height = im.size @@ -45,31 +48,22 @@ def img_to_string(im): ) -def img_string_normalize(im): +def img_string_normalize(im: str) -> str: return img_to_string(string_to_img(im)) -def assert_img_equal_img_string(a, b_string): +def assert_img_equal_img_string(a: Image.Image, b_string: str) -> None: assert img_to_string(a) == img_string_normalize(b_string) -def test_str_to_img(): +def test_str_to_img() -> None: assert_image_equal_tofile(A, "Tests/images/morph_a.png") -def create_lut(): - for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): - lb = ImageMorph.LutBuilder(op_name=op) - lut = lb.build_lut() - with open(f"Tests/images/{op}.lut", "wb") as f: - f.write(lut) - - -# create_lut() @pytest.mark.parametrize( "op", ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge") ) -def test_lut(op): +def test_lut(op: str) -> None: lb = ImageMorph.LutBuilder(op_name=op) assert lb.get_lut() is None @@ -78,7 +72,7 @@ def test_lut(op): assert lut == bytearray(f.read()) -def test_no_operator_loaded(): +def test_no_operator_loaded() -> None: mop = ImageMorph.MorphOp() with pytest.raises(Exception) as e: mop.apply(None) @@ -92,7 +86,7 @@ def test_no_operator_loaded(): # Test the named patterns -def test_erosion8(): +def test_erosion8() -> None: # erosion8 mop = ImageMorph.MorphOp(op_name="erosion8") count, Aout = mop.apply(A) @@ -111,7 +105,7 @@ def test_erosion8(): ) -def test_dialation8(): +def test_dialation8() -> None: # dialation8 mop = ImageMorph.MorphOp(op_name="dilation8") count, Aout = mop.apply(A) @@ -130,7 +124,7 @@ def test_dialation8(): ) -def test_erosion4(): +def test_erosion4() -> None: # erosion4 mop = ImageMorph.MorphOp(op_name="dilation4") count, Aout = mop.apply(A) @@ -149,7 +143,7 @@ def test_erosion4(): ) -def test_edge(): +def test_edge() -> None: # edge mop = ImageMorph.MorphOp(op_name="edge") count, Aout = mop.apply(A) @@ -168,7 +162,7 @@ def test_edge(): ) -def test_corner(): +def test_corner() -> None: # Create a corner detector pattern mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) count, Aout = mop.apply(A) @@ -196,7 +190,7 @@ def test_corner(): assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) -def test_mirroring(): +def test_mirroring() -> None: # Test 'M' for mirroring mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"]) count, Aout = mop.apply(A) @@ -215,7 +209,7 @@ def test_mirroring(): ) -def test_negate(): +def test_negate() -> None: # Test 'N' for negate mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"]) count, Aout = mop.apply(A) @@ -234,7 +228,7 @@ def test_negate(): ) -def test_incorrect_mode(): +def test_incorrect_mode() -> None: im = hopper("RGB") mop = ImageMorph.MorphOp(op_name="erosion8") @@ -249,7 +243,7 @@ def test_incorrect_mode(): assert str(e.value) == "Image mode must be L" -def test_add_patterns(): +def test_add_patterns() -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") assert lb.patterns == ["1:(... ... ...)->0", "4:(00. 01. ...)->1"] @@ -267,12 +261,12 @@ def test_add_patterns(): ] -def test_unknown_pattern(): +def test_unknown_pattern() -> None: with pytest.raises(Exception): ImageMorph.LutBuilder(op_name="unknown") -def test_pattern_syntax_error(): +def test_pattern_syntax_error() -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") new_patterns = ["a pattern with a syntax error"] @@ -284,7 +278,7 @@ def test_pattern_syntax_error(): assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' -def test_load_invalid_mrl(): +def test_load_invalid_mrl() -> None: # Arrange invalid_mrl = "Tests/images/hopper.png" mop = ImageMorph.MorphOp() @@ -295,7 +289,7 @@ def test_load_invalid_mrl(): assert str(e.value) == "Wrong size operator file!" -def test_roundtrip_mrl(tmp_path): +def test_roundtrip_mrl(tmp_path: Path) -> None: # Arrange tempfile = str(tmp_path / "temp.mrl") mop = ImageMorph.MorphOp(op_name="corner") @@ -309,7 +303,7 @@ def test_roundtrip_mrl(tmp_path): assert mop.lut == initial_lut -def test_set_lut(): +def test_set_lut() -> None: # Arrange lb = ImageMorph.LutBuilder(op_name="corner") lut = lb.build_lut() @@ -322,7 +316,7 @@ def test_set_lut(): assert mop.lut == lut -def test_wrong_mode(): +def test_wrong_mode() -> None: lut = ImageMorph.LutBuilder(op_name="corner").build_lut() imrgb = Image.new("RGB", (10, 10)) iml = Image.new("L", (10, 10)) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 7980bead0..b320e79c1 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageDraw, ImageOps, ImageStat, features @@ -13,7 +14,7 @@ from .helper import ( class Deformer: - def getmesh(self, im): + def getmesh(self, im: Image.Image) -> list[tuple[tuple[int, ...], tuple[int, ...]]]: x, y = im.size return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] @@ -21,7 +22,7 @@ class Deformer: deformer = Deformer() -def test_sanity(): +def test_sanity() -> None: ImageOps.autocontrast(hopper("L")) ImageOps.autocontrast(hopper("RGB")) @@ -83,7 +84,7 @@ def test_sanity(): ImageOps.exif_transpose(hopper("RGB")) -def test_1pxfit(): +def test_1pxfit() -> None: # Division by zero in equalize if image is 1 pixel high newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35)) assert newimg.size == (35, 35) @@ -95,7 +96,7 @@ def test_1pxfit(): assert newimg.size == (35, 35) -def test_fit_same_ratio(): +def test_fit_same_ratio() -> None: # The ratio for this image is 1000.0 / 755 = 1.3245033112582782 # If the ratios are not acknowledged to be the same, # and Pillow attempts to adjust the width to @@ -107,13 +108,13 @@ def test_fit_same_ratio(): @pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) -def test_contain(new_size): +def test_contain(new_size: tuple[int, int]) -> None: im = hopper() new_im = ImageOps.contain(im, new_size) assert new_im.size == (256, 256) -def test_contain_round(): +def test_contain_round() -> None: im = Image.new("1", (43, 63), 1) new_im = ImageOps.contain(im, (5, 7)) assert new_im.width == 5 @@ -131,13 +132,13 @@ def test_contain_round(): ("hopper.png", (256, 256)), # square ), ) -def test_cover(image_name, expected_size): +def test_cover(image_name: str, expected_size: tuple[int, int]) -> None: with Image.open("Tests/images/" + image_name) as im: new_im = ImageOps.cover(im, (256, 256)) assert new_im.size == expected_size -def test_pad(): +def test_pad() -> None: # Same ratio im = hopper() new_size = (im.width * 2, im.height * 2) @@ -157,7 +158,7 @@ def test_pad(): ) -def test_pad_round(): +def test_pad_round() -> None: im = Image.new("1", (1, 1), 1) new_im = ImageOps.pad(im, (4, 1)) assert new_im.load()[2, 0] == 1 @@ -167,7 +168,7 @@ def test_pad_round(): @pytest.mark.parametrize("mode", ("P", "PA")) -def test_palette(mode): +def test_palette(mode: str) -> None: im = hopper(mode) # Expand @@ -181,7 +182,7 @@ def test_palette(mode): ) -def test_pil163(): +def test_pil163() -> None: # Division by zero in equalize if < 255 pixels in image (@PIL163) i = hopper("RGB").resize((15, 16)) @@ -191,7 +192,7 @@ def test_pil163(): ImageOps.equalize(i.convert("RGB")) -def test_scale(): +def test_scale() -> None: # Test the scaling function i = hopper("L").resize((50, 50)) @@ -209,7 +210,7 @@ def test_scale(): @pytest.mark.parametrize("border", (10, (1, 2, 3, 4))) -def test_expand_palette(border): +def test_expand_palette(border: int | tuple[int, int, int, int]) -> None: with Image.open("Tests/images/p_16.tga") as im: im_expanded = ImageOps.expand(im, border, (255, 0, 0)) @@ -235,7 +236,7 @@ def test_expand_palette(border): assert_image_equal(im_cropped, im) -def test_colorize_2color(): +def test_colorize_2color() -> None: # Test the colorizing function with 2-color functionality # Open test image (256px by 10px, black to white) @@ -269,7 +270,7 @@ def test_colorize_2color(): ) -def test_colorize_2color_offset(): +def test_colorize_2color_offset() -> None: # Test the colorizing function with 2-color functionality and offset # Open test image (256px by 10px, black to white) @@ -305,7 +306,7 @@ def test_colorize_2color_offset(): ) -def test_colorize_3color_offset(): +def test_colorize_3color_offset() -> None: # Test the colorizing function with 3-color functionality and offset # Open test image (256px by 10px, black to white) @@ -358,14 +359,14 @@ def test_colorize_3color_offset(): ) -def test_exif_transpose(): +def test_exif_transpose() -> None: exts = [".jpg"] if features.check("webp") and features.check("webp_anim"): exts.append(".webp") for ext in exts: with Image.open("Tests/images/hopper" + ext) as base_im: - def check(orientation_im): + def check(orientation_im: Image.Image) -> None: for im in [ orientation_im, orientation_im.copy(), @@ -422,7 +423,7 @@ def test_exif_transpose(): assert 0x0112 not in transposed_im.getexif() -def test_exif_transpose_in_place(): +def test_exif_transpose_in_place() -> None: with Image.open("Tests/images/orientation_rectangle.jpg") as im: assert im.size == (2, 1) assert im.getexif()[0x0112] == 8 @@ -434,24 +435,24 @@ def test_exif_transpose_in_place(): assert_image_equal(im, expected) -def test_autocontrast_unsupported_mode(): +def test_autocontrast_unsupported_mode() -> None: im = Image.new("RGBA", (1, 1)) with pytest.raises(OSError): ImageOps.autocontrast(im) -def test_autocontrast_cutoff(): +def test_autocontrast_cutoff() -> None: # Test the cutoff argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: - def autocontrast(cutoff): + def autocontrast(cutoff: int | tuple[int, int]): return ImageOps.autocontrast(img, cutoff).histogram() assert autocontrast(10) == autocontrast((10, 10)) assert autocontrast(10) != autocontrast((1, 10)) -def test_autocontrast_mask_toy_input(): +def test_autocontrast_mask_toy_input() -> None: # Test the mask argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: rect_mask = Image.new("L", img.size, 0) @@ -470,7 +471,7 @@ def test_autocontrast_mask_toy_input(): assert ImageStat.Stat(result_nomask).median == [128] -def test_autocontrast_mask_real_input(): +def test_autocontrast_mask_real_input() -> None: # Test the autocontrast with a rectangular mask with Image.open("Tests/images/iptc.jpg") as img: rect_mask = Image.new("L", img.size, 0) @@ -485,20 +486,20 @@ def test_autocontrast_mask_real_input(): assert result_nomask != result assert_tuple_approx_equal( ImageStat.Stat(result, mask=rect_mask).median, - [195, 202, 184], + (195, 202, 184), threshold=2, msg="autocontrast with mask pixel incorrect", ) assert_tuple_approx_equal( ImageStat.Stat(result_nomask).median, - [119, 106, 79], + (119, 106, 79), threshold=2, msg="autocontrast without mask pixel incorrect", ) -def test_autocontrast_preserve_tone(): - def autocontrast(mode, preserve_tone): +def test_autocontrast_preserve_tone() -> None: + def autocontrast(mode: str, preserve_tone: bool) -> Image.Image: im = hopper(mode) return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() @@ -506,7 +507,7 @@ def test_autocontrast_preserve_tone(): assert autocontrast("L", True) == autocontrast("L", False) -def test_autocontrast_preserve_gradient(): +def test_autocontrast_preserve_gradient() -> None: gradient = Image.linear_gradient("L") # test with a grayscale gradient that extends to 0,255. @@ -532,7 +533,7 @@ def test_autocontrast_preserve_gradient(): @pytest.mark.parametrize( "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) ) -def test_autocontrast_preserve_one_color(color): +def test_autocontrast_preserve_one_color(color: tuple[int, int, int]) -> None: img = Image.new("RGB", (10, 10), color) # single color images shouldn't change diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 84d3a6950..519d79105 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,11 +1,14 @@ from __future__ import annotations + +from typing import Generator + import pytest from PIL import Image, ImageFilter @pytest.fixture -def test_images(): +def test_images() -> Generator[dict[str, Image.Image], None, None]: ims = { "im": Image.open("Tests/images/hopper.ppm"), "snakes": Image.open("Tests/images/color_snakes.png"), @@ -17,7 +20,7 @@ def test_images(): im.close() -def test_filter_api(test_images): +def test_filter_api(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] test_filter = ImageFilter.GaussianBlur(2.0) @@ -31,7 +34,7 @@ def test_filter_api(test_images): assert i.size == (128, 128) -def test_usm_formats(test_images): +def test_usm_formats(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] usm = ImageFilter.UnsharpMask @@ -49,7 +52,7 @@ def test_usm_formats(test_images): im.convert("YCbCr").filter(usm) -def test_blur_formats(test_images): +def test_blur_formats(test_images: dict[str, Image.Image]) -> None: im = test_images["im"] blur = ImageFilter.GaussianBlur @@ -67,7 +70,7 @@ def test_blur_formats(test_images): im.convert("YCbCr").filter(blur) -def test_usm_accuracy(test_images): +def test_usm_accuracy(test_images: dict[str, Image.Image]) -> None: snakes = test_images["snakes"] src = snakes.convert("RGB") @@ -76,7 +79,7 @@ def test_usm_accuracy(test_images): assert i.tobytes() == src.tobytes() -def test_blur_accuracy(test_images): +def test_blur_accuracy(test_images: dict[str, Image.Image]) -> None: snakes = test_images["snakes"] i = snakes.filter(ImageFilter.GaussianBlur(0.4)) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index e5b59b74a..8e2db15aa 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image, ImagePalette @@ -6,19 +9,19 @@ from PIL import Image, ImagePalette from .helper import assert_image_equal, assert_image_equal_tofile -def test_sanity(): +def test_sanity() -> None: palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) assert len(palette.colors) == 256 -def test_reload(): +def test_reload() -> None: 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(): +def test_getcolor() -> None: palette = ImagePalette.ImagePalette() assert len(palette.palette) == 0 assert len(palette.colors) == 0 @@ -45,7 +48,7 @@ def test_getcolor(): palette.getcolor("unknown") -def test_getcolor_rgba_color_rgb_palette(): +def test_getcolor_rgba_color_rgb_palette() -> None: palette = ImagePalette.ImagePalette("RGB") # Opaque RGBA colors are converted @@ -64,7 +67,7 @@ def test_getcolor_rgba_color_rgb_palette(): (255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)), ], ) -def test_getcolor_not_special(index, palette): +def test_getcolor_not_special(index: int, palette: ImagePalette.ImagePalette) -> None: im = Image.new("P", (1, 1)) # Do not use transparency index as a new color @@ -78,7 +81,7 @@ def test_getcolor_not_special(index, palette): assert index2 not in (index, index1) -def test_file(tmp_path): +def test_file(tmp_path: Path) -> None: palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) f = str(tmp_path / "temp.lut") @@ -96,7 +99,7 @@ def test_file(tmp_path): assert p.palette == palette.tobytes() -def test_make_linear_lut(): +def test_make_linear_lut() -> None: # Arrange black = 0 white = 255 @@ -112,7 +115,7 @@ def test_make_linear_lut(): assert lut[i] == i -def test_make_linear_lut_not_yet_implemented(): +def test_make_linear_lut_not_yet_implemented() -> None: # Update after FIXME # Arrange black = 1 @@ -123,7 +126,7 @@ def test_make_linear_lut_not_yet_implemented(): ImagePalette.make_linear_lut(black, white) -def test_make_gamma_lut(): +def test_make_gamma_lut() -> None: # Arrange exp = 5 @@ -141,7 +144,7 @@ def test_make_gamma_lut(): assert lut[255] == 255 -def test_rawmode_valueerrors(tmp_path): +def test_rawmode_valueerrors(tmp_path: Path) -> None: # Arrange palette = ImagePalette.raw("RGB", list(range(256)) * 3) @@ -155,7 +158,7 @@ def test_rawmode_valueerrors(tmp_path): palette.save(f) -def test_getdata(): +def test_getdata() -> None: # Arrange data_in = list(range(256)) * 3 palette = ImagePalette.ImagePalette("RGB", data_in) @@ -167,7 +170,7 @@ def test_getdata(): assert mode == "RGB" -def test_rawmode_getdata(): +def test_rawmode_getdata() -> None: # Arrange data_in = list(range(256)) * 3 palette = ImagePalette.raw("RGB", data_in) @@ -180,7 +183,7 @@ def test_rawmode_getdata(): assert data_in == data_out -def test_2bit_palette(tmp_path): +def test_2bit_palette(tmp_path: Path) -> None: # issue #2258, 2 bit palettes are corrupted. outfile = str(tmp_path / "temp.png") @@ -192,6 +195,6 @@ def test_2bit_palette(tmp_path): assert_image_equal_tofile(img, outfile) -def test_invalid_palette(): +def test_invalid_palette() -> None: with pytest.raises(OSError): ImagePalette.load("Tests/images/hopper.jpg") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index ac3ea3281..9487560af 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,14 +1,16 @@ from __future__ import annotations + import array import math import struct +from typing import Sequence import pytest from PIL import Image, ImagePath -def test_path(): +def test_path() -> None: p = ImagePath.Path(list(range(10))) # sequence interface @@ -56,7 +58,9 @@ def test_path(): ImagePath.Path((0, 1)), ), ) -def test_path_constructors(coords): +def test_path_constructors( + coords: Sequence[float] | array.array[float] | ImagePath.Path, +) -> None: # Arrange / Act p = ImagePath.Path(coords) @@ -74,7 +78,9 @@ def test_path_constructors(coords): [[0.0, 1.0]], ), ) -def test_invalid_path_constructors(coords): +def test_invalid_path_constructors( + coords: tuple[str, str] | Sequence[Sequence[int]] +) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -92,7 +98,7 @@ def test_invalid_path_constructors(coords): [0, 1, 2], ), ) -def test_path_odd_number_of_coordinates(coords): +def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) @@ -110,7 +116,9 @@ def test_path_odd_number_of_coordinates(coords): (1, (0.0, 0.0, 0.0, 0.0)), ], ) -def test_getbbox(coords, expected): +def test_getbbox( + coords: int | list[int], expected: tuple[float, float, float, float] +) -> None: # Arrange p = ImagePath.Path(coords) @@ -118,7 +126,7 @@ def test_getbbox(coords, expected): assert p.getbbox() == expected -def test_getbbox_no_args(): +def test_getbbox_no_args() -> None: # Arrange p = ImagePath.Path([0, 1, 2, 3]) @@ -134,7 +142,7 @@ def test_getbbox_no_args(): (list(range(6)), [(0.0, 3.0), (4.0, 9.0), (8.0, 15.0)]), ], ) -def test_map(coords, expected): +def test_map(coords: int | list[int], expected: list[tuple[float, float]]) -> None: # Arrange p = ImagePath.Path(coords) @@ -146,7 +154,7 @@ def test_map(coords, expected): assert list(p) == expected -def test_transform(): +def test_transform() -> None: # Arrange p = ImagePath.Path([0, 1, 2, 3]) theta = math.pi / 15 @@ -164,7 +172,7 @@ def test_transform(): ] -def test_transform_with_wrap(): +def test_transform_with_wrap() -> None: # Arrange p = ImagePath.Path([0, 1, 2, 3]) theta = math.pi / 15 @@ -183,7 +191,7 @@ def test_transform_with_wrap(): ] -def test_overflow_segfault(): +def test_overflow_segfault() -> None: # Some Pythons fail getting the argument as an integer, and it falls # through to the sequence. Seeing this on 32-bit Windows. with pytest.raises((TypeError, MemoryError)): @@ -197,12 +205,12 @@ def test_overflow_segfault(): class Evil: - def __init__(self): + def __init__(self) -> None: self.corrupt = Image.core.path(0x4000000000000000) - def __getitem__(self, i): + def __getitem__(self, i: int) -> bytes: x = self.corrupt[i] return struct.pack("dd", x[0], x[1]) - def __setitem__(self, i, x): + def __setitem__(self, i: int, x: bytes) -> None: self.corrupt[i] = struct.unpack("dd", x) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 41d247f42..88ad1f9ee 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest @@ -15,7 +16,7 @@ if ImageQt.qt_is_installed: from PIL.ImageQt import qRgba -def test_rgb(): +def test_rgb() -> None: # from https://doc.qt.io/archives/qt-4.8/qcolor.html # typedef QRgb # An ARGB quadruplet on the format #AARRGGBB, @@ -27,7 +28,7 @@ def test_rgb(): assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) - def checkrgb(r, g, b): + def checkrgb(r: int, g: int, b: int) -> None: val = ImageQt.rgb(r, g, b) val = val % 2**24 # drop the alpha assert val >> 16 == r @@ -40,7 +41,7 @@ def test_rgb(): checkrgb(0, 0, 255) -def test_image(): +def test_image() -> None: 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+ @@ -54,6 +55,6 @@ def test_image(): assert_image_similar(roundtripped_im, im, 1) -def test_closed_file(): +def test_closed_file() -> None: with warnings.catch_warnings(): ImageQt.ImageQt("Tests/images/hopper.gif") diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 6d71e4d87..7280dded0 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image, ImageSequence, TiffImagePlugin @@ -6,7 +9,7 @@ from PIL import Image, ImageSequence, TiffImagePlugin from .helper import assert_image_equal, hopper, skip_unless_feature -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: test_file = str(tmp_path / "temp.im") im = hopper("RGB") @@ -26,7 +29,7 @@ def test_sanity(tmp_path): ImageSequence.Iterator(0) -def test_iterator(): +def test_iterator() -> None: with Image.open("Tests/images/multipage.tiff") as im: i = ImageSequence.Iterator(im) for index in range(0, im.n_frames): @@ -37,14 +40,14 @@ def test_iterator(): next(i) -def test_iterator_min_frame(): +def test_iterator_min_frame() -> None: with Image.open("Tests/images/hopper.psd") as im: i = ImageSequence.Iterator(im) for index in range(1, im.n_frames): assert i[index] == next(i) -def _test_multipage_tiff(): +def _test_multipage_tiff() -> None: with Image.open("Tests/images/multipage.tiff") as im: for index, frame in enumerate(ImageSequence.Iterator(im)): frame.load() @@ -52,18 +55,18 @@ def _test_multipage_tiff(): frame.convert("RGB") -def test_tiff(): +def test_tiff() -> None: _test_multipage_tiff() @skip_unless_feature("libtiff") -def test_libtiff(): +def test_libtiff() -> None: TiffImagePlugin.READ_LIBTIFF = True _test_multipage_tiff() TiffImagePlugin.READ_LIBTIFF = False -def test_consecutive(): +def test_consecutive() -> None: with Image.open("Tests/images/multipage.tiff") as im: first_frame = None for frame in ImageSequence.Iterator(im): @@ -74,7 +77,7 @@ def test_consecutive(): break -def test_palette_mmap(): +def test_palette_mmap() -> None: # Using mmap in ImageFile can require to reload the palette. with Image.open("Tests/images/multipage-mmap.tiff") as im: color1 = im.getpalette()[:3] @@ -83,7 +86,7 @@ def test_palette_mmap(): assert color1 == color2 -def test_all_frames(): +def test_all_frames() -> None: # Test a single image with Image.open("Tests/images/iss634.gif") as im: ims = ImageSequence.all_frames(im) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 761d28d30..8d741d94a 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from typing import Any + import pytest from PIL import Image, ImageShow @@ -6,12 +9,12 @@ from PIL import Image, ImageShow from .helper import hopper, is_win32, on_ci -def test_sanity(): +def test_sanity() -> None: dir(Image) dir(ImageShow) -def test_register(): +def test_register() -> None: # Test registering a viewer that is not a class ImageShow.register("not a class") @@ -23,9 +26,9 @@ def test_register(): "order", [-1, 0], ) -def test_viewer_show(order): +def test_viewer_show(order: int) -> None: class TestViewer(ImageShow.Viewer): - def show_image(self, image, **options): + def show_image(self, image: Image.Image, **options: Any) -> bool: self.methodCalled = True return True @@ -47,12 +50,12 @@ def test_viewer_show(order): reason="Only run on CIs; hangs on Windows CIs", ) @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) -def test_show(mode): +def test_show(mode: str) -> None: im = hopper(mode) assert ImageShow.show(im) -def test_show_without_viewers(): +def test_show_without_viewers() -> None: viewers = ImageShow._viewers ImageShow._viewers = [] @@ -62,7 +65,7 @@ def test_show_without_viewers(): ImageShow._viewers = viewers -def test_viewer(): +def test_viewer() -> None: viewer = ImageShow.Viewer() assert viewer.get_format(None) is None @@ -72,14 +75,14 @@ def test_viewer(): @pytest.mark.parametrize("viewer", ImageShow._viewers) -def test_viewers(viewer): +def test_viewers(viewer: ImageShow.Viewer) -> None: try: viewer.get_command("test.jpg") except NotImplementedError: pass -def test_ipythonviewer(): +def test_ipythonviewer() -> None: pytest.importorskip("IPython", reason="IPython not installed") for viewer in ImageShow._viewers: if isinstance(viewer, ImageShow.IPythonViewer): diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 7b56b89cc..b1c1306c1 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image, ImageStat @@ -6,7 +7,7 @@ from PIL import Image, ImageStat from .helper import hopper -def test_sanity(): +def test_sanity() -> None: im = hopper() st = ImageStat.Stat(im) @@ -30,7 +31,7 @@ def test_sanity(): ImageStat.Stat(1) -def test_hopper(): +def test_hopper() -> None: im = hopper() st = ImageStat.Stat(im) @@ -43,7 +44,7 @@ def test_hopper(): assert st.sum[2] == 1563008 -def test_constant(): +def test_constant() -> None: im = Image.new("L", (128, 128), 128) st = ImageStat.Stat(im) diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index bb20fbb6f..b607b8c43 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image @@ -22,7 +23,7 @@ TK_MODES = ("1", "L", "P", "RGB", "RGBA") pytestmark = pytest.mark.skipif(not HAS_TK, reason="Tk not installed") -def setup_module(): +def setup_module() -> None: try: # setup tk tk.Frame() @@ -33,7 +34,7 @@ def setup_module(): pytest.skip(f"TCL Error: {v}") -def test_kw(): +def test_kw() -> None: TEST_JPG = "Tests/images/hopper.jpg" TEST_PNG = "Tests/images/hopper.png" with Image.open(TEST_JPG) as im1: @@ -56,7 +57,7 @@ def test_kw(): @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage(mode): +def test_photoimage(mode: str) -> None: # test as image: im = hopper(mode) @@ -70,7 +71,7 @@ def test_photoimage(mode): assert_image_equal(reloaded, im.convert("RGBA")) -def test_photoimage_apply_transparency(): +def test_photoimage_apply_transparency() -> None: with Image.open("Tests/images/pil123p.png") as im: im_tk = ImageTk.PhotoImage(im) reloaded = ImageTk.getimage(im_tk) @@ -78,7 +79,7 @@ def test_photoimage_apply_transparency(): @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage_blank(mode): +def test_photoimage_blank(mode: str) -> None: # test a image using mode/size: im_tk = ImageTk.PhotoImage(mode, (100, 100)) @@ -90,7 +91,7 @@ def test_photoimage_blank(mode): assert_image_equal(reloaded.convert(mode), im) -def test_bitmapimage(): +def test_bitmapimage() -> None: im = hopper("1") # this should not crash diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 6927eedcf..b43c31b52 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import ImageWin @@ -7,10 +8,10 @@ from .helper import hopper, is_win32 class TestImageWin: - def test_sanity(self): + def test_sanity(self) -> None: dir(ImageWin) - def test_hdc(self): + def test_hdc(self) -> None: # Arrange dc = 50 @@ -21,7 +22,7 @@ class TestImageWin: # Assert assert dc2 == 50 - def test_hwnd(self): + def test_hwnd(self) -> None: # Arrange wnd = 50 @@ -35,7 +36,7 @@ class TestImageWin: @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestImageWinDib: - def test_dib_image(self): + def test_dib_image(self) -> None: # Arrange im = hopper() @@ -45,7 +46,7 @@ class TestImageWinDib: # Assert assert dib.size == im.size - def test_dib_mode_string(self): + def test_dib_mode_string(self) -> None: # Arrange mode = "RGBA" size = (128, 128) @@ -56,7 +57,7 @@ class TestImageWinDib: # Assert assert dib.size == (128, 128) - def test_dib_paste(self): + def test_dib_paste(self) -> None: # Arrange im = hopper() @@ -70,7 +71,7 @@ class TestImageWinDib: # Assert assert dib.size == (128, 128) - def test_dib_paste_bbox(self): + def test_dib_paste_bbox(self) -> None: # Arrange im = hopper() bbox = (0, 0, 10, 10) @@ -85,7 +86,7 @@ class TestImageWinDib: # Assert assert dib.size == (128, 128) - def test_dib_frombytes_tobytes_roundtrip(self): + def test_dib_frombytes_tobytes_roundtrip(self) -> None: # Arrange # Make two different DIB images im = hopper() diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index bd154335a..f59ee7284 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -1,5 +1,7 @@ from __future__ import annotations + from io import BytesIO +from pathlib import Path from PIL import Image, ImageWin @@ -68,7 +70,7 @@ if is_win32(): ] CreateDIBSection.restype = ctypes.wintypes.HBITMAP - def serialize_dib(bi, pixels): + def serialize_dib(bi, pixels) -> bytearray: bf = BITMAPFILEHEADER() bf.bfType = 0x4D42 bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize @@ -82,7 +84,7 @@ if is_win32(): memcpy(bp + bf.bfOffBits, pixels, bi.biSizeImage) return bytearray(buf) - def test_pointer(tmp_path): + def test_pointer(tmp_path: Path) -> None: im = hopper() (width, height) = im.size opath = str(tmp_path / "temp.png") diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 92cad4ac1..31548bbc9 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -1,10 +1,11 @@ from __future__ import annotations + import pytest from PIL import Image -def test_setmode(): +def test_setmode() -> None: im = Image.new("L", (1, 1), 255) im.im.setmode("1") assert im.im.getpixel((0, 0)) == 255 diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 1293f7628..629a6dc7a 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest @@ -9,7 +10,13 @@ X = 255 class TestLibPack: - def assert_pack(self, mode, rawmode, data, *pixels): + def assert_pack( + self, + mode: str, + rawmode: str, + data: int | bytes, + *pixels: int | float | tuple[int, ...], + ) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ @@ -23,7 +30,7 @@ class TestLibPack: assert data == im.tobytes("raw", rawmode) - def test_1(self): + def test_1(self) -> None: self.assert_pack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) self.assert_pack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) self.assert_pack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) @@ -36,29 +43,29 @@ class TestLibPack: self.assert_pack("1", "L", b"\xff\x00\x00\xff\x00\x00", X, 0, 0, X, 0, 0) - def test_L(self): + def test_L(self) -> None: self.assert_pack("L", "L", 1, 1, 2, 3, 4) self.assert_pack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) self.assert_pack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) - def test_LA(self): + def test_LA(self) -> None: self.assert_pack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) self.assert_pack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_La(self): + def test_La(self) -> None: self.assert_pack("La", "La", 2, (1, 2), (3, 4), (5, 6)) - def test_P(self): + def test_P(self) -> None: self.assert_pack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 255, 0, 0) self.assert_pack("P", "P;2", b"\xe4", 3, 2, 1, 0) self.assert_pack("P", "P;4", b"\x02\xef", 0, 2, 14, 15) self.assert_pack("P", "P", 1, 1, 2, 3, 4) - def test_PA(self): + def test_PA(self) -> None: self.assert_pack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) self.assert_pack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_RGB(self): + def test_RGB(self) -> None: self.assert_pack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_pack( "RGB", "RGBX", b"\x01\x02\x03\xff\x05\x06\x07\xff", (1, 2, 3), (5, 6, 7) @@ -78,7 +85,7 @@ class TestLibPack: self.assert_pack("RGB", "G", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("RGB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - def test_RGBA(self): + def test_RGBA(self) -> None: self.assert_pack("RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) @@ -100,12 +107,12 @@ class TestLibPack: self.assert_pack("RGBA", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) self.assert_pack("RGBA", "A", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - def test_RGBa(self): + def test_RGBa(self) -> None: self.assert_pack("RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack("RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) self.assert_pack("RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) - def test_RGBX(self): + def test_RGBX(self) -> None: self.assert_pack("RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12) @@ -133,7 +140,7 @@ class TestLibPack: self.assert_pack("RGBX", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) self.assert_pack("RGBX", "X", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - def test_CMYK(self): + def test_CMYK(self) -> None: self.assert_pack("CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) self.assert_pack( "CMYK", @@ -148,7 +155,7 @@ class TestLibPack: ) self.assert_pack("CMYK", "K", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) - def test_YCbCr(self): + def test_YCbCr(self) -> None: self.assert_pack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_pack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_pack( @@ -171,19 +178,19 @@ class TestLibPack: self.assert_pack("YCbCr", "Cb", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) self.assert_pack("YCbCr", "Cr", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) - def test_LAB(self): + def test_LAB(self) -> None: self.assert_pack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) self.assert_pack("LAB", "L", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) self.assert_pack("LAB", "A", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("LAB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - def test_HSV(self): + def test_HSV(self) -> None: self.assert_pack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_pack("HSV", "H", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) self.assert_pack("HSV", "S", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("HSV", "V", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) - def test_I(self): + def test_I(self) -> None: self.assert_pack("I", "I;16B", 2, 0x0102, 0x0304) self.assert_pack( "I", "I;32S", b"\x83\x00\x00\x01\x01\x00\x00\x83", 0x01000083, -2097151999 @@ -208,10 +215,10 @@ class TestLibPack: 0x01000083, ) - def test_I16(self): + def test_I16(self) -> None: self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) - def test_F_float(self): + def test_F_float(self) -> None: self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) if sys.byteorder == "little": @@ -227,7 +234,13 @@ class TestLibPack: class TestLibUnpack: - def assert_unpack(self, mode, rawmode, data, *pixels): + def assert_unpack( + self, + mode: str, + rawmode: str, + data: int | bytes, + *pixels: int | float | tuple[int, ...], + ) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ @@ -240,7 +253,7 @@ class TestLibUnpack: for x, pixel in enumerate(pixels): assert pixel == im.getpixel((x, 0)) - def test_1(self): + def test_1(self) -> None: self.assert_unpack("1", "1", b"\x01", 0, 0, 0, 0, 0, 0, 0, X) self.assert_unpack("1", "1;I", b"\x01", X, X, X, X, X, X, X, 0) self.assert_unpack("1", "1;R", b"\x01", X, 0, 0, 0, 0, 0, 0, 0) @@ -253,7 +266,7 @@ class TestLibUnpack: self.assert_unpack("1", "1;8", b"\x00\x01\x02\xff", 0, X, X, X) - def test_L(self): + def test_L(self) -> None: self.assert_unpack("L", "L;2", b"\xe4", 255, 170, 85, 0) self.assert_unpack("L", "L;2I", b"\xe4", 0, 85, 170, 255) self.assert_unpack("L", "L;2R", b"\xe4", 0, 170, 85, 255) @@ -272,14 +285,14 @@ class TestLibUnpack: self.assert_unpack("L", "L;16", b"\x00\xc6\x00\xaf", 198, 175) self.assert_unpack("L", "L;16B", b"\xc6\x00\xaf\x00", 198, 175) - def test_LA(self): + def test_LA(self) -> None: self.assert_unpack("LA", "LA", 2, (1, 2), (3, 4), (5, 6)) self.assert_unpack("LA", "LA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_La(self): + def test_La(self) -> None: self.assert_unpack("La", "La", 2, (1, 2), (3, 4), (5, 6)) - def test_P(self): + def test_P(self) -> None: self.assert_unpack("P", "P;1", b"\xe4", 1, 1, 1, 0, 0, 1, 0, 0) self.assert_unpack("P", "P;2", b"\xe4", 3, 2, 1, 0) # erroneous? @@ -290,11 +303,11 @@ class TestLibUnpack: self.assert_unpack("P", "P", 1, 1, 2, 3, 4) self.assert_unpack("P", "P;R", 1, 128, 64, 192, 32) - def test_PA(self): + def test_PA(self) -> None: self.assert_unpack("PA", "PA", 2, (1, 2), (3, 4), (5, 6)) self.assert_unpack("PA", "PA;L", 2, (1, 4), (2, 5), (3, 6)) - def test_RGB(self): + def test_RGB(self) -> None: self.assert_unpack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_unpack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_unpack("RGB", "RGB;R", 3, (128, 64, 192), (32, 160, 96)) @@ -345,14 +358,14 @@ class TestLibUnpack: "RGB", "CMYK", 4, (250, 249, 248), (242, 241, 240), (234, 233, 233) ) - def test_BGR(self): + def test_BGR(self) -> None: self.assert_unpack("BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)) self.assert_unpack( "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0) ) self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - def test_RGBA(self): + def test_RGBA(self) -> None: self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) self.assert_unpack( "RGBA", "LA;16B", 4, (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11) @@ -521,7 +534,7 @@ class TestLibUnpack: "RGBA", "A;16N", 2, (0, 0, 0, 1), (0, 0, 0, 3), (0, 0, 0, 5) ) - def test_RGBa(self): + def test_RGBa(self) -> None: self.assert_unpack( "RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) ) @@ -535,7 +548,7 @@ class TestLibUnpack: "RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9) ) - def test_RGBX(self): + def test_RGBX(self) -> None: self.assert_unpack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) self.assert_unpack("RGBX", "RGB;L", 3, (1, 4, 7, X), (2, 5, 8, X), (3, 6, 9, X)) self.assert_unpack("RGBX", "RGB;16B", 6, (1, 3, 5, X), (7, 9, 11, X)) @@ -580,7 +593,7 @@ class TestLibUnpack: self.assert_unpack("RGBX", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) self.assert_unpack("RGBX", "X", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) - def test_CMYK(self): + def test_CMYK(self) -> None: self.assert_unpack( "CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) ) @@ -618,25 +631,25 @@ class TestLibUnpack: "CMYK", "K;I", 1, (0, 0, 0, 254), (0, 0, 0, 253), (0, 0, 0, 252) ) - def test_YCbCr(self): + def test_YCbCr(self) -> None: self.assert_unpack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_unpack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_unpack("YCbCr", "YCbCrK", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) self.assert_unpack("YCbCr", "YCbCrX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) - def test_LAB(self): + def test_LAB(self) -> None: self.assert_unpack("LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) self.assert_unpack("LAB", "L", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) self.assert_unpack("LAB", "A", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) self.assert_unpack("LAB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) - def test_HSV(self): + def test_HSV(self) -> None: self.assert_unpack("HSV", "HSV", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) self.assert_unpack("HSV", "H", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) self.assert_unpack("HSV", "S", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) self.assert_unpack("HSV", "V", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) - def test_I(self): + def test_I(self) -> None: self.assert_unpack("I", "I;8", 1, 0x01, 0x02, 0x03, 0x04) self.assert_unpack("I", "I;8S", b"\x01\x83", 1, -125) self.assert_unpack("I", "I;16", 2, 0x0201, 0x0403) @@ -677,7 +690,7 @@ class TestLibUnpack: 0x01000083, ) - def test_F_int(self): + def test_F_int(self) -> None: self.assert_unpack("F", "F;8", 1, 0x01, 0x02, 0x03, 0x04) self.assert_unpack("F", "F;8S", b"\x01\x83", 1, -125) self.assert_unpack("F", "F;16", 2, 0x0201, 0x0403) @@ -716,7 +729,7 @@ class TestLibUnpack: 16777348, ) - def test_F_float(self): + def test_F_float(self) -> None: self.assert_unpack( "F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34 ) @@ -767,7 +780,7 @@ class TestLibUnpack: -1234.5, ) - def test_I16(self): + def test_I16(self) -> None: self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) @@ -784,7 +797,7 @@ class TestLibUnpack: self.assert_unpack("I;16L", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) - def test_CMYK16(self): + def test_CMYK16(self) -> None: self.assert_unpack("CMYK", "CMYK;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) self.assert_unpack("CMYK", "CMYK;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) if sys.byteorder == "little": @@ -792,7 +805,7 @@ class TestLibUnpack: else: self.assert_unpack("CMYK", "CMYK;16N", 8, (1, 3, 5, 7), (9, 11, 13, 15)) - def test_value_error(self): + def test_value_error(self) -> None: with pytest.raises(ValueError): self.assert_unpack("L", "L", 0, 0) with pytest.raises(ValueError): diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 49b052fa4..1c8b84a2b 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,4 +1,5 @@ from __future__ import annotations + import locale import pytest @@ -23,7 +24,7 @@ from PIL import Image path = "Tests/images/hopper.jpg" -def test_sanity(): +def test_sanity() -> None: with Image.open(path): pass try: diff --git a/Tests/test_main.py b/Tests/test_main.py index a84e61a7b..46259f1dc 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -1,10 +1,11 @@ from __future__ import annotations + import os import subprocess import sys -def test_main(): +def test_main() -> None: out = subprocess.check_output([sys.executable, "-m", "PIL"]).decode("utf-8") lines = out.splitlines() assert lines[0] == "-" * 68 diff --git a/Tests/test_map.py b/Tests/test_map.py index 76444f33d..93140f6e5 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,4 +1,5 @@ from __future__ import annotations + import sys import pytest @@ -6,7 +7,7 @@ import pytest from PIL import Image -def test_overflow(): +def test_overflow() -> None: # There is the potential to overflow comparisons in map.c # if there are > SIZE_MAX bytes in the image or if # the file encodes an offset that makes @@ -24,7 +25,7 @@ def test_overflow(): Image.MAX_IMAGE_PIXELS = max_pixels -def test_tobytes(): +def test_tobytes() -> None: # Note that this image triggers the decompression bomb warning: max_pixels = Image.MAX_IMAGE_PIXELS Image.MAX_IMAGE_PIXELS = None @@ -38,7 +39,7 @@ def test_tobytes(): @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") -def test_ysize(): +def test_ysize() -> None: numpy = pytest.importorskip("numpy", reason="NumPy not installed") # Should not raise 'Integer overflow in ysize' diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 3e17d8dcc..903f7e0c6 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import Image @@ -8,7 +11,7 @@ from .helper import hopper original = hopper().resize((32, 32)).convert("I") -def verify(im1): +def verify(im1: Image.Image) -> None: im2 = original.copy() assert im1.size == im2.size pix1 = im1.load() @@ -24,7 +27,7 @@ def verify(im1): @pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I")) -def test_basic(tmp_path, mode): +def test_basic(tmp_path: Path, mode: str) -> None: # PIL 1.1 has limited support for 16-bit image data. Check that # create/copy/transform and save works as expected. @@ -74,8 +77,8 @@ def test_basic(tmp_path, mode): assert im_in.getpixel((0, 0)) == min(512, maximum) -def test_tobytes(): - def tobytes(mode): +def test_tobytes() -> None: + def tobytes(mode: str) -> Image.Image: return Image.new(mode, (1, 1), 1).tobytes() order = 1 if Image._ENDIAN == "<" else -1 @@ -86,7 +89,7 @@ def test_tobytes(): assert tobytes("I") == b"\x01\x00\x00\x00"[::order] -def test_convert(): +def test_convert() -> None: im = original.copy() for mode in ("I;16", "I;16B", "I;16N"): diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 6f0e99b3f..9f4e6534e 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,4 +1,5 @@ from __future__ import annotations + import warnings import pytest @@ -12,8 +13,8 @@ numpy = pytest.importorskip("numpy", reason="NumPy not installed") TEST_IMAGE_SIZE = (10, 10) -def test_numpy_to_image(): - def to_image(dtype, bands=1, boolean=0): +def test_numpy_to_image() -> None: + def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image: if bands == 1: if boolean: data = [0, 255] * 50 @@ -81,7 +82,7 @@ def test_numpy_to_image(): # Based on an erring example at # https://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function -def test_3d_array(): +def test_3d_array() -> None: size = (5, TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1]) a = numpy.ones(size, dtype=numpy.uint8) assert_image(Image.fromarray(a[1, :, :]), "L", TEST_IMAGE_SIZE) @@ -93,12 +94,12 @@ def test_3d_array(): assert_image(Image.fromarray(a[:, :, 1]), "L", TEST_IMAGE_SIZE) -def test_1d_array(): +def test_1d_array() -> None: a = numpy.ones(5, dtype=numpy.uint8) assert_image(Image.fromarray(a), "L", (1, 5)) -def _test_img_equals_nparray(img, np): +def _test_img_equals_nparray(img: Image.Image, np) -> None: assert len(np.shape) >= 2 np_size = np.shape[1], np.shape[0] assert img.size == np_size @@ -108,14 +109,14 @@ def _test_img_equals_nparray(img, np): assert_deep_equal(px[x, y], np[y, x]) -def test_16bit(): +def test_16bit() -> None: with Image.open("Tests/images/16bit.cropped.tif") as img: np_img = numpy.array(img) _test_img_equals_nparray(img, np_img) assert np_img.dtype == numpy.dtype(" None: # Test that 1-bit arrays convert to numpy and back # See: https://github.com/python-pillow/Pillow/issues/350 arr = numpy.array([[1, 0, 0, 1, 0], [0, 1, 0, 0, 0]], "u1") @@ -125,7 +126,7 @@ def test_1bit(): numpy.testing.assert_array_equal(arr, arr_back) -def test_save_tiff_uint16(): +def test_save_tiff_uint16() -> None: # Tests that we're getting the pixel value in the right byte order. pixel_value = 0x1234 a = numpy.array( @@ -156,7 +157,7 @@ def test_save_tiff_uint16(): ("HSV", numpy.uint8), ), ) -def test_to_array(mode, dtype): +def test_to_array(mode: str, dtype) -> None: img = hopper(mode) # Resize to non-square @@ -168,7 +169,7 @@ def test_to_array(mode, dtype): assert np_img.dtype == dtype -def test_point_lut(): +def test_point_lut() -> None: # See https://github.com/python-pillow/Pillow/issues/439 data = list(range(256)) * 3 @@ -179,7 +180,7 @@ def test_point_lut(): im.point(lut) -def test_putdata(): +def test_putdata() -> None: # Shouldn't segfault # See https://github.com/python-pillow/Pillow/issues/1008 @@ -206,12 +207,12 @@ def test_putdata(): numpy.float64, ), ) -def test_roundtrip_eye(dtype): +def test_roundtrip_eye(dtype) -> None: arr = numpy.eye(10, dtype=dtype) numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr))) -def test_zero_size(): +def test_zero_size() -> None: # Shouldn't cause floating point exception # See https://github.com/python-pillow/Pillow/issues/2259 @@ -221,13 +222,13 @@ def test_zero_size(): @skip_unless_feature("libtiff") -def test_load_first(): +def test_load_first() -> None: with Image.open("Tests/images/g4_orientation_5.tif") as im: a = numpy.array(im) assert a.shape == (88, 590) -def test_bool(): +def test_bool() -> None: # https://github.com/python-pillow/Pillow/issues/2044 a = numpy.zeros((10, 2), dtype=bool) a[0][0] = True @@ -236,7 +237,7 @@ def test_bool(): assert im2.getdata()[0] == 255 -def test_no_resource_warning_for_numpy_array(): +def test_no_resource_warning_for_numpy_array() -> None: # https://github.com/python-pillow/Pillow/issues/835 # Arrange from numpy import array diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index aeeafb6f1..f6b12cb20 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -1,4 +1,5 @@ from __future__ import annotations + import time import pytest @@ -18,14 +19,14 @@ from PIL.PdfParser import ( ) -def test_text_encode_decode(): +def test_text_encode_decode() -> None: assert encode_text("abc") == b"\xFE\xFF\x00a\x00b\x00c" assert decode_text(b"\xFE\xFF\x00a\x00b\x00c") == "abc" assert decode_text(b"abc") == "abc" assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD" -def test_indirect_refs(): +def test_indirect_refs() -> None: assert IndirectReference(1, 2) == IndirectReference(1, 2) assert IndirectReference(1, 2) != IndirectReference(1, 3) assert IndirectReference(1, 2) != IndirectObjectDef(1, 2) @@ -36,7 +37,7 @@ def test_indirect_refs(): assert IndirectObjectDef(1, 2) != (1, 2) -def test_parsing(): +def test_parsing() -> None: assert PdfParser.interpret_name(b"Name#23Hash") == b"Name#Hash" assert PdfParser.interpret_name(b"Name#23Hash", as_text=True) == "Name#Hash" assert PdfParser.get_value(b"1 2 R ", 0) == (IndirectReference(1, 2), 5) @@ -94,7 +95,7 @@ def test_parsing(): assert time.strftime("%Y%m%d%H%M%S", getattr(d, name)) == value -def test_pdf_repr(): +def test_pdf_repr() -> None: assert bytes(IndirectReference(1, 2)) == b"1 2 R" assert bytes(IndirectObjectDef(*IndirectReference(1, 2))) == b"1 2 obj" assert bytes(PdfName(b"Name#Hash")) == b"/Name#23Hash" @@ -120,7 +121,7 @@ def test_pdf_repr(): assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" -def test_duplicate_xref_entry(): +def test_duplicate_xref_entry() -> None: pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf") assert pdf.xref_table.existing_entries[6][0] == 1197 pdf.close() diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index eb687b57b..ed415953f 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -1,5 +1,7 @@ from __future__ import annotations + import pickle +from pathlib import Path import pytest @@ -11,7 +13,9 @@ FONT_SIZE = 20 FONT_PATH = "Tests/fonts/DejaVuSans/DejaVuSans.ttf" -def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode): +def helper_pickle_file( + tmp_path: Path, protocol: int, test_file: str, mode: str | None +) -> None: # Arrange with Image.open(test_file) as im: filename = str(tmp_path / "temp.pkl") @@ -28,7 +32,7 @@ def helper_pickle_file(tmp_path, pickle, protocol, test_file, mode): assert im == loaded_im -def helper_pickle_string(pickle, protocol, test_file, mode): +def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> None: with Image.open(test_file) as im: if mode: im = im.convert(mode) @@ -62,13 +66,15 @@ def helper_pickle_string(pickle, protocol, test_file, mode): ], ) @pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1)) -def test_pickle_image(tmp_path, test_file, test_mode, protocol): +def test_pickle_image( + tmp_path: Path, test_file: str, test_mode: str | None, protocol: int +) -> None: # Act / Assert - helper_pickle_string(pickle, protocol, test_file, test_mode) - helper_pickle_file(tmp_path, pickle, protocol, test_file, test_mode) + helper_pickle_string(protocol, test_file, test_mode) + helper_pickle_file(tmp_path, protocol, test_file, test_mode) -def test_pickle_la_mode_with_palette(tmp_path): +def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: # Arrange filename = str(tmp_path / "temp.pkl") with Image.open("Tests/images/hopper.jpg") as im: @@ -87,7 +93,7 @@ def test_pickle_la_mode_with_palette(tmp_path): @skip_unless_feature("webp") -def test_pickle_tell(): +def test_pickle_tell() -> None: # Arrange with Image.open("Tests/images/hopper.webp") as image: # Act: roundtrip @@ -97,7 +103,9 @@ def test_pickle_tell(): assert unpickled_image.tell() == 0 -def helper_assert_pickled_font_images(font1, font2): +def helper_assert_pickled_font_images( + font1: ImageFont.FreeTypeFont, font2: ImageFont.FreeTypeFont +) -> None: # Arrange im1 = Image.new(mode="RGBA", size=(300, 100)) im2 = Image.new(mode="RGBA", size=(300, 100)) @@ -115,7 +123,7 @@ def helper_assert_pickled_font_images(font1, font2): @skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) -def test_pickle_font_string(protocol): +def test_pickle_font_string(protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -129,7 +137,7 @@ def test_pickle_font_string(protocol): @skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) -def test_pickle_font_file(tmp_path, protocol): +def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) filename = str(tmp_path / "temp.pkl") diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 77c7952e9..64dfb2c95 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -1,14 +1,16 @@ from __future__ import annotations + import os import sys from io import BytesIO +from pathlib import Path import pytest from PIL import Image, PSDraw -def _create_document(ps): +def _create_document(ps: PSDraw.PSDraw) -> None: title = "hopper" box = (1 * 72, 2 * 72, 7 * 72, 10 * 72) # in points @@ -30,7 +32,7 @@ def _create_document(ps): ps.end_document() -def test_draw_postscript(tmp_path): +def test_draw_postscript(tmp_path: Path) -> None: # Based on Pillow tutorial, but there is no textsize: # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript @@ -48,7 +50,7 @@ def test_draw_postscript(tmp_path): @pytest.mark.parametrize("buffer", (True, False)) -def test_stdout(buffer): +def test_stdout(buffer: bool) -> None: # Temporarily redirect stdout old_stdout = sys.stdout diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 08133b6c3..c2f7fe22e 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import __version__ @@ -6,7 +7,7 @@ from PIL import __version__ pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") -def test_pyroma(): +def test_pyroma() -> None: # Arrange data = pyroma.projectdata.get_data(".") diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 49ca01677..3cd323553 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,7 +1,10 @@ from __future__ import annotations + +from pathlib import Path + import pytest -from PIL import ImageQt +from PIL import Image, ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -18,7 +21,7 @@ if ImageQt.qt_is_installed: from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget class Example(QWidget): - def __init__(self): + def __init__(self) -> None: super().__init__() img = hopper().resize((1000, 1000)) @@ -34,14 +37,14 @@ if ImageQt.qt_is_installed: lbl.setPixmap(pixmap1.copy()) -def roundtrip(expected): +def roundtrip(expected: Image.Image) -> None: result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb assert_image_similar(result, expected.convert("RGB"), 1) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") -def test_sanity(tmp_path): +def test_sanity(tmp_path: Path) -> None: # Segfault test app = QApplication([]) ex = Example() diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 396bd9080..6110be707 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,4 +1,7 @@ from __future__ import annotations + +from pathlib import Path + import pytest from PIL import ImageQt @@ -14,7 +17,7 @@ if ImageQt.qt_is_installed: @pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) -def test_sanity(mode, tmp_path): +def test_sanity(mode: str, tmp_path: Path) -> None: src = hopper(mode) data = ImageQt.toqimage(src) diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index 37d72d451..3ce31cd2d 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -1,4 +1,5 @@ from __future__ import annotations + import pytest from PIL import Image @@ -20,7 +21,7 @@ from PIL import Image "Tests/images/crash-db8bfa78b19721225425530c5946217720d7df4e.sgi", ], ) -def test_crashes(test_file): +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: with pytest.raises(OSError): diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index d93b03904..2a072fd44 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -1,5 +1,8 @@ from __future__ import annotations + import shutil +from pathlib import Path +from typing import Callable import pytest @@ -15,7 +18,12 @@ test_filenames = ("temp_';", 'temp_";', "temp_'\"|", "temp_'\"||", "temp_'\"&&") @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") class TestShellInjection: - def assert_save_filename_check(self, tmp_path, src_img, save_func): + def assert_save_filename_check( + self, + tmp_path: Path, + src_img: Image.Image, + save_func: Callable[[Image.Image, int, str], None], + ) -> None: for filename in test_filenames: dest_file = str(tmp_path / filename) save_func(src_img, 0, dest_file) @@ -24,7 +32,7 @@ class TestShellInjection: im.load() @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") - def test_load_djpeg_filename(self, tmp_path): + def test_load_djpeg_filename(self, tmp_path: Path) -> None: for filename in test_filenames: src_file = str(tmp_path / filename) shutil.copy(TEST_JPG, src_file) @@ -33,18 +41,18 @@ class TestShellInjection: im.load_djpeg() @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") - def test_save_cjpeg_filename(self, tmp_path): + def test_save_cjpeg_filename(self, tmp_path: Path) -> None: with Image.open(TEST_JPG) as im: self.assert_save_filename_check(tmp_path, im, JpegImagePlugin._save_cjpeg) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") - def test_save_netpbm_filename_bmp_mode(self, tmp_path): + def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: im = im.convert("RGB") self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") - def test_save_netpbm_filename_l_mode(self, tmp_path): + def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: im = im.convert("L") self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index 64e781cba..f51e8b3a8 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -42,7 +42,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): +def test_tiff_crashes(test_file: str) -> None: try: with Image.open(test_file) as im: im.load() diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index e7b41fb47..f6adae3e6 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,5 +1,7 @@ from __future__ import annotations + from fractions import Fraction +from pathlib import Path from PIL import Image, TiffImagePlugin, features from PIL.TiffImagePlugin import IFDRational @@ -7,14 +9,14 @@ from PIL.TiffImagePlugin import IFDRational from .helper import hopper -def _test_equal(num, denom, target): +def _test_equal(num, denom, target) -> None: t = IFDRational(num, denom) assert target == t assert t == target -def test_sanity(): +def test_sanity() -> None: _test_equal(1, 1, 1) _test_equal(1, 1, Fraction(1, 1)) @@ -30,13 +32,13 @@ def test_sanity(): _test_equal(7, 5, 1.4) -def test_ranges(): +def test_ranges() -> None: for num in range(1, 10): for denom in range(1, 10): assert IFDRational(num, denom) == IFDRational(num, denom) -def test_nonetype(): +def test_nonetype() -> None: # Fails if the _delegate function doesn't return a valid function xres = IFDRational(72) @@ -50,10 +52,10 @@ def test_nonetype(): assert xres and yres -def test_ifd_rational_save(tmp_path): - methods = (True, False) - if not features.check("libtiff"): - methods = (False,) +def test_ifd_rational_save(tmp_path: Path) -> None: + methods = [True] + if features.check("libtiff"): + methods.append(False) for libtiff in methods: TiffImagePlugin.WRITE_LIBTIFF = libtiff diff --git a/Tests/test_uploader.py b/Tests/test_uploader.py index 6b693f7cd..d55ceb4be 100644 --- a/Tests/test_uploader.py +++ b/Tests/test_uploader.py @@ -1,14 +1,15 @@ from __future__ import annotations + from .helper import assert_image_equal, assert_image_similar, hopper -def check_upload_equal(): +def check_upload_equal() -> None: result = hopper("P").convert("RGB") target = hopper("RGB") assert_image_equal(result, target) -def check_upload_similar(): +def check_upload_similar() -> None: result = hopper("P").convert("RGB") target = hopper("RGB") assert_image_similar(result, target, 0) diff --git a/Tests/test_util.py b/Tests/test_util.py index 1457d85f7..197ef79ee 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,26 +1,16 @@ from __future__ import annotations + +from pathlib import Path, PurePath + import pytest from PIL import _util -def test_is_path(): - # Arrange - fp = "filename.ext" - - # Act - it_is = _util.is_path(fp) - - # Assert - assert it_is - - -def test_path_obj_is_path(): - # Arrange - from pathlib import Path - - test_path = Path("filename.ext") - +@pytest.mark.parametrize( + "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] +) +def test_is_path(test_path: str | Path | PurePath) -> None: # Act it_is = _util.is_path(test_path) @@ -28,7 +18,7 @@ def test_path_obj_is_path(): assert it_is -def test_is_not_path(tmp_path): +def test_is_not_path(tmp_path: Path) -> None: # Arrange with (tmp_path / "temp.ext").open("w") as fp: pass @@ -40,7 +30,7 @@ def test_is_not_path(tmp_path): assert not it_is_not -def test_is_directory(): +def test_is_directory() -> None: # Arrange directory = "Tests" @@ -51,7 +41,7 @@ def test_is_directory(): assert it_is -def test_is_not_directory(): +def test_is_not_directory() -> None: # Arrange text = "abc" @@ -62,11 +52,11 @@ def test_is_not_directory(): assert not it_is_not -def test_deferred_error(): +def test_deferred_error() -> None: # Arrange # Act - thing = _util.DeferredError(ValueError("Some error text")) + thing = _util.DeferredError.new(ValueError("Some error text")) # Assert with pytest.raises(ValueError): diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 28ebc7d79..626fe427c 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -1,4 +1,5 @@ from __future__ import annotations + from io import BytesIO from PIL import Image @@ -13,11 +14,11 @@ class TestWebPLeaks(PillowLeakTestCase): mem_limit = 3 * 1024 # kb iterations = 100 - def test_leak_load(self): + def test_leak_load(self) -> None: with open(test_file, "rb") as f: im_data = f.read() - def core(): + def core() -> None: with Image.open(BytesIO(im_data)) as im: im.load() diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index a318bfafd..04bfbc755 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -5,7 +5,7 @@ archive=$1 url=$2 if [ ! -f $archive.tar.gz ]; then - wget -O $archive.tar.gz $url + wget --no-verbose -O $archive.tar.gz $url fi rmdir $archive diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index b7cebbdbf..3adae91a5 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,15 +1,39 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.2.2 +archive_name=libimagequant +archive_version=4.2.2 -./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz +archive=$archive_name-$archive_version -pushd $archive/imagequant-sys +if [[ "$GHA_LIBIMAGEQUANT_CACHE_HIT" == "true" ]]; then -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/ + # Copy cached files into place + sudo cp ~/cache-$archive_name/libimagequant.so* /usr/lib/ + sudo cp ~/cache-$archive_name/libimagequant.h /usr/include/ -popd +else + + # Build from source + ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz + + pushd $archive/imagequant-sys + + cargo install cargo-c + cargo cinstall --prefix=/usr --destdir=. + + # Copy into place + sudo cp usr/lib/libimagequant.so* /usr/lib/ + sudo cp usr/include/libimagequant.h /usr/include/ + + if [ -n "$GITHUB_ACTIONS" ]; then + # Copy to cache + rm -rf ~/cache-$archive_name + mkdir ~/cache-$archive_name + cp usr/lib/libimagequant.so* ~/cache-$archive_name/ + cp usr/include/libimagequant.h ~/cache-$archive_name/ + fi + + popd + +fi diff --git a/docs/COPYING b/docs/COPYING index bc44ba388..73af6d99c 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-2023 by Jeffrey A. Clark (Alex) and contributors + Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/PIL.rst b/docs/PIL.rst index fa036b9cc..bdbf1373d 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -69,10 +69,10 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`~PIL.ImageTransform` Module ---------------------------------- +:mod:`~PIL.ImageMode` Module +---------------------------- -.. automodule:: PIL.ImageTransform +.. automodule:: PIL.ImageMode :members: :undoc-members: :show-inheritance: diff --git a/docs/about.rst b/docs/about.rst index 872ac0ea6..cdb32ca5d 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -6,15 +6,14 @@ Goals The fork author's goal is to foster and support active development of PIL through: -- Continuous integration testing via `GitHub Actions`_, `AppVeyor`_ and `Travis CI`_ +- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_ - Publicized development activity on `GitHub`_ - Regular releases to the `Python Package Index`_ .. _GitHub Actions: https://github.com/python-pillow/Pillow/actions .. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow -.. _Travis CI: https://app.travis-ci.com/github/python-pillow/Pillow .. _GitHub: https://github.com/python-pillow/Pillow -.. _Python Package Index: https://pypi.org/project/Pillow/ +.. _Python Package Index: https://pypi.org/project/pillow/ License ------- diff --git a/docs/conf.py b/docs/conf.py index 9974b0f2a..97289c91d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,7 +54,7 @@ master_doc = "index" # General information about the project. project = "Pillow (PIL Fork)" copyright = ( - "1995-2011 Fredrik Lundh, 2010-2023 Jeffrey A. Clark (Alex) and contributors" + "1995-2011 Fredrik Lundh, 2010-2024 Jeffrey A. Clark (Alex) and contributors" ) author = "Fredrik Lundh, Jeffrey A. Clark (Alex), contributors" @@ -233,7 +233,7 @@ htmlhelp_basename = "PillowPILForkdoc" # -- Options for LaTeX output --------------------------------------------- -latex_elements = { +latex_elements: dict[str, str] = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). @@ -326,7 +326,7 @@ linkcheck_allowed_redirects = { r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", - r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", + r"https://tidelift.com/badges/package/pypi/pillow?.*": r"https://img.shields.io/badge/.*", r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", } diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 75c0b73eb..205fcb9ab 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -44,6 +44,54 @@ ImageFile.raise_oserror error codes returned by a codec's ``decode()`` method, which ImageFile already does automatically. +IptcImageFile helper functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.2.0 + +The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant +``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow +12.0.0 (2025-10-15). These are undocumented helper functions intended +for internal use, so there is no replacement. They can each be replaced +by a single line of code using builtin functions in Python. + +ImageCms constants and versions() function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 10.3.0 + +A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. +This includes a table of flags based on LittleCMS version 1 which has been +replaced with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== + Removed features ---------------- @@ -107,7 +155,7 @@ Constants .. versionremoved:: 10.0.0 A number of constants have been removed. -Instead, ``enum.IntEnum`` classes have been added. +Instead, :py:class:`enum.IntEnum` classes have been added. .. note:: @@ -327,8 +375,8 @@ ImageCms.CmsProfile attributes .. deprecated:: 3.2.0 .. versionremoved:: 8.0.0 -Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6.0.0, -they issued a :py:exc:`DeprecationWarning`: +Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed. +From 6.0.0, they issued a :py:exc:`DeprecationWarning`: ======================== =================================================== Removed Use instead diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index e98bb8680..2a2a0ba29 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -9,6 +9,7 @@ The contents of this file are hereby released in the public domain (CC0) Full text of the CC0 license: https://creativecommons.org/publicdomain/zero/1.0/ """ + from __future__ import annotations import struct diff --git a/docs/example/anchors.py b/docs/example/anchors.py index 3a0e40b84..b5d76b4fe 100644 --- a/docs/example/anchors.py +++ b/docs/example/anchors.py @@ -5,7 +5,7 @@ from PIL import Image, ImageDraw, ImageFont font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 16) -def test(anchor): +def test(anchor: str) -> Image.Image: im = Image.new("RGBA", (200, 100), "white") d = ImageDraw.Draw(im) d.line(((100, 0), (100, 100)), "gray") diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 9cd65fd48..569ccb769 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -487,6 +487,16 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **exif** If present, the image will be stored with the provided raw EXIF data. +**keep_rgb** + By default, libjpeg converts images with an RGB color space to YCbCr. + If this option is present and true, those images will be stored as RGB + instead. + + When this option is enabled, attempting to chroma-subsample RGB images + with the ``subsampling`` option will raise an :py:exc:`OSError`. + + .. versionadded:: 10.2.0 + **subsampling** If present, sets the subsampling for the encoder. @@ -552,12 +562,13 @@ JPEG 2000 .. versionadded:: 2.4.0 -Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB`` or -``RGBA`` data. It can also read files containing ``YCbCr`` data, which it -converts on read into ``RGB`` or ``RGBA`` depending on whether or not there is -an alpha channel. Pillow supports JPEG 2000 raw codestreams (``.j2k`` files), -as well as boxed JPEG 2000 files (``.j2p`` or ``.jpx`` files). Pillow does -*not* support files whose components have different sampling frequencies. +Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``, +``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to +``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel. +Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``, +``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports +JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files +(``.jp2`` or ``.jpx`` files). When loading, if you set the ``mode`` on the image prior to the :py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to @@ -685,6 +696,25 @@ PCX Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` data. +PFM +^^^ + +.. versionadded:: 10.3.0 + +Pillow reads and writes grayscale (Pf format) Portable FloatMap (PFM) files +containing ``F`` data. + +Color (PF format) PFM files are not supported. + +Opening +~~~~~~~ + +The :py:func:`~PIL.Image.open` function sets the following +:py:attr:`~PIL.Image.Image.info` properties: + +**scale** + The absolute value of the number stored in the *Scale Factor / Endianness* line. + PNG ^^^ diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index d79f2465f..523e2ad74 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -542,7 +542,7 @@ Reading from URL from PIL import Image from urllib.request import urlopen - url = "https://python-pillow.org/images/pillow-logo.png" + url = "https://python-pillow.org/assets/images/pillow-logo.png" img = Image.open(urlopen(url)) diff --git a/docs/index.rst b/docs/index.rst index 4f577fe9c..bf2feea9a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,10 +41,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more `_ and by direct URL access -eg. https://pypi.org/project/Pillow/1.0/. +`_ and by direct URL access +eg. https://pypi.org/project/pillow/1.0/. diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst index 464ab77ea..06965ead3 100644 --- a/docs/reference/ExifTags.rst +++ b/docs/reference/ExifTags.rst @@ -4,8 +4,9 @@ :py:mod:`~PIL.ExifTags` Module ============================== -The :py:mod:`~PIL.ExifTags` module exposes several ``enum.IntEnum`` classes -which provide constants and clear-text names for various well-known EXIF tags. +The :py:mod:`~PIL.ExifTags` module exposes several :py:class:`enum.IntEnum` +classes which provide constants and clear-text names for various well-known +EXIF tags. .. py:data:: Base diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 9b9b5e7b2..c4484cbe2 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -8,9 +8,34 @@ The :py:mod:`~PIL.ImageCms` module provides color profile management support using the LittleCMS2 color management engine, based on Kevin Cazabon's PyCMS library. +.. autoclass:: ImageCmsProfile + :members: + :special-members: __init__ .. autoclass:: ImageCmsTransform + :members: + :undoc-members: + :show-inheritance: .. autoexception:: PyCMSError +Constants +--------- + +.. autoclass:: Intent + :members: + :member-order: bysource + :undoc-members: + :show-inheritance: +.. autoclass:: Direction + :members: + :member-order: bysource + :undoc-members: + :show-inheritance: +.. autoclass:: Flags + :members: + :member-order: bysource + :undoc-members: + :show-inheritance: + Functions --------- @@ -37,13 +62,15 @@ CmsProfile ---------- The ICC color profiles are wrapped in an instance of the class -:py:class:`CmsProfile`. The specification ICC.1:2010 contains more +:py:class:`~core.CmsProfile`. The specification ICC.1:2010 contains more information about the meaning of the values in ICC profiles. For convenience, all XYZ-values are also given as xyY-values (so they can be easily displayed in a chromaticity diagram, for example). +.. py:currentmodule:: PIL.ImageCms.core .. py:class:: CmsProfile + :canonical: PIL._imagingcms.CmsProfile .. py:attribute:: creation_date :type: Optional[datetime.datetime] diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 0b94032d5..db2987eb0 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -11,9 +11,9 @@ or the clipboard to a PIL image memory. .. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None) - Take a snapshot of the screen. The pixels inside the bounding box are - returned as an "RGBA" on macOS, or an "RGB" image otherwise. - If the bounding box is omitted, the entire screen is copied. + Take a snapshot of the screen. The pixels inside the bounding box are returned as + an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, + the entire screen is copied, and on macOS, it will be at 2x if on a Retina screen. On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is @@ -22,7 +22,10 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) :param bbox: What region to copy. Default is the entire screen. - Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used. + On macOS, this is not increased to 2x for Retina screens, so the full + width of a Retina screen would be 1440, not 2880. + On Windows, the top-left point may be negative if ``all_screens=True`` + is used. :param include_layered_windows: Includes layered windows. Windows OS only. .. versionadded:: 6.1.0 diff --git a/docs/reference/ImageTransform.rst b/docs/reference/ImageTransform.rst new file mode 100644 index 000000000..5b0a5ce49 --- /dev/null +++ b/docs/reference/ImageTransform.rst @@ -0,0 +1,40 @@ + +.. py:module:: PIL.ImageTransform +.. py:currentmodule:: PIL.ImageTransform + +:py:mod:`~PIL.ImageTransform` Module +==================================== + +The :py:mod:`~PIL.ImageTransform` module contains implementations of +:py:class:`~PIL.Image.ImageTransformHandler` for some of the builtin +:py:class:`.Image.Transform` methods. + +.. autoclass:: Transform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: AffineTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: PerspectiveTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: ExtentTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: QuadTransform + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: MeshTransform + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 5d6affa94..82c75e373 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -25,6 +25,7 @@ Reference ImageShow ImageStat ImageTk + ImageTransform ImageWin ExifTags TiffTags diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 2e2d3322f..99a18e9ea 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -1,5 +1,5 @@ -Internal Reference Docs -======================= +Internal Reference +================== .. toctree:: :maxdepth: 2 diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 363a67d9b..899e4966f 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -25,6 +25,27 @@ Internal Modules :undoc-members: :show-inheritance: +:mod:`~PIL._typing` Module +-------------------------- + +.. module:: PIL._typing + +Provides a convenient way to import type hints that are not available +on some Python versions. + +.. py:class:: StrOrBytesPath + + Typing alias. + +.. py:class:: SupportsRead + + An object that supports the read method. + +.. py:data:: TypeGuard + :value: typing.TypeGuard + + See :py:obj:`typing.TypeGuard`. + :mod:`~PIL._util` Module ------------------------ diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index f31941c9a..730c8da5b 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -3,7 +3,7 @@ File Handling in Pillow ======================= -When opening a file as an image, Pillow requires a filename, ``pathlib.Path`` +When opening a file as an image, Pillow requires a filename, ``os.PathLike`` object, or a file-like object. Pillow uses the filename or ``Path`` to open a file, so for the rest of this article, they will all be treated as a file-like object. diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index a3f238119..705ca0415 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -43,7 +43,7 @@ Constants ^^^^^^^^^ A number of constants have been removed. -Instead, ``enum.IntEnum`` classes have been added. +Instead, :py:class:`enum.IntEnum` classes have been added. ===================================================== ============================================================ Removed Use instead diff --git a/docs/releasenotes/10.2.0.rst b/docs/releasenotes/10.2.0.rst index 9883f10ba..c3947f64c 100644 --- a/docs/releasenotes/10.2.0.rst +++ b/docs/releasenotes/10.2.0.rst @@ -1,14 +1,6 @@ 10.2.0 ------ -Backwards Incompatible Changes -============================== - -TODO -^^^^ - -TODO - Deprecations ============ @@ -20,10 +12,14 @@ ImageFile.raise_oserror error codes returned by a codec's ``decode()`` method, which ImageFile already does automatically. -TODO -^^^^ +IptcImageFile helper functions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +The functions ``IptcImageFile.dump`` and ``IptcImageFile.i``, and the constant +``IptcImageFile.PAD`` have been deprecated and will be removed in Pillow +12.0.0 (2025-10-15). These are undocumented helper functions intended +for internal use, so there is no replacement. They can each be replaced +by a single line of code using builtin functions in Python. API Changes =========== @@ -46,6 +42,14 @@ Added DdsImagePlugin enums :py:class:`~PIL.DdsImagePlugin.DXGI_FORMAT` and :py:class:`~PIL.DdsImagePlugin.D3DFMT` enums have been added to :py:class:`PIL.DdsImagePlugin`. +JPEG RGB color space +^^^^^^^^^^^^^^^^^^^^ + +When saving JPEG files, ``keep_rgb`` can now be set to ``True``. This will store RGB +images in the RGB color space instead of being converted to YCbCr automatically by +libjpeg. When this option is enabled, attempting to chroma-subsample RGB images with +the ``subsampling`` option will raise an :py:exc:`OSError`. + JPEG restart marker interval ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -62,10 +66,34 @@ output only the quantization and Huffman tables for the image. Security ======== -TODO -^^^^ +ImageFont.getmask: Applied ImageFont.MAX_STRING_LENGTH +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +To protect against potential DOS attacks when using arbitrary strings as text input, +Pillow will now raise a :py:exc:`ValueError` if the number of characters passed into +:py:meth:`PIL.ImageFont.ImageFont.getmask` is over a certain limit, +:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. + +This threshold can be changed by setting :py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It +can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. + +A decompression bomb check has also been added to +:py:meth:`PIL.ImageFont.ImageFont.getmask`. + +ImageFont.getmask: Trim glyph size +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To protect against potential DOS attacks when using PIL fonts, +:py:class:`PIL.ImageFont.ImageFont` now trims the size of individual glyphs so that +they do not extend beyond the bitmap image. + +ImageMath.eval: Restricted environment keys +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cve:`2023-50447`: If an attacker has control over the keys passed to the +``environment`` argument of :py:meth:`PIL.ImageMath.eval`, they may be able to execute +arbitrary code. To prevent this, keys matching the names of builtins and keys +containing double underscores will now raise a :py:exc:`ValueError`. Other Changes ============= @@ -78,16 +106,56 @@ Support has been added to read the BC4U format of DDS images. Support has also been added to read DX10 BC1 and BC4, whether UNORM or TYPELESS. +Support arbitrary masks for uncompressed RGB DDS images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +All masks are now supported when reading DDS images with uncompressed RGB data, +allowing for bit counts other than 24 and 32. + +Saving TIFF tag RowsPerStrip +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving TIFF images, the TIFF tag RowsPerStrip can now be one of the tags set by +the user, rather than always being calculated by Pillow. + +Optimized ImageColor.getrgb and getcolor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The color calculations of :py:attr:`~PIL.ImageColor.getrgb` and +:py:attr:`~PIL.ImageColor.getcolor` are now cached using +:py:func:`functools.lru_cache`. Cached calls of ``getrgb`` are 3.1 - 91.4 times +as fast and ``getcolor`` are 5.1 - 19.6 times as fast. + +Optimized ImageMode.getmode +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The lookups made by :py:attr:`~PIL.ImageMode.getmode` are now cached using +:py:func:`functools.lru_cache` instead of a custom cache. Cached calls are 1.2 times as +fast. + Optimized ImageStat.Stat count and extrema ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Calculating the :py:attr:`~PIL.ImageStat.Stat.count` and :py:attr:`~PIL.ImageStat.Stat.extrema` statistics is now faster. After the -histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3x as fast -on average and ``st.extrema`` is 12x as fast on average. +histogram is created in ``st = ImageStat.Stat(im)``, ``st.count`` is 3 times as fast on +average and ``st.extrema`` is 12 times as fast on average. Encoder errors now report error detail as string ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :py:exc:`OSError` exceptions from image encoders now include a textual description of the error instead of a numeric error code. + +Type hints +^^^^^^^^^^ + +Work has begun to add type annotations to Pillow, including: + +* :py:mod:`~PIL.ContainerIO` +* :py:mod:`~PIL.FontFile`, :py:mod:`~PIL.BdfFontFile` and :py:mod:`~PIL.PcfFontFile` +* :py:mod:`~PIL.ImageChops` +* :py:mod:`~PIL.ImageMode` +* :py:mod:`~PIL.ImageSequence` +* :py:mod:`~PIL.ImageTransform` +* :py:mod:`~PIL.TarIO` diff --git a/docs/releasenotes/10.3.0.rst b/docs/releasenotes/10.3.0.rst new file mode 100644 index 000000000..af31cdb74 --- /dev/null +++ b/docs/releasenotes/10.3.0.rst @@ -0,0 +1,87 @@ +10.3.0 +------ + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +Deprecations +============ + +ImageCms constants and versions() function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A number of constants and a function in :py:mod:`.ImageCms` have been deprecated. +This includes a table of flags based on LittleCMS version 1 which has been replaced +with a new class :py:class:`.ImageCms.Flags` based on LittleCMS 2 flags. + +============================================ ==================================================== +Deprecated Use instead +============================================ ==================================================== +``ImageCms.DESCRIPTION`` No replacement +``ImageCms.VERSION`` ``PIL.__version__`` +``ImageCms.FLAGS["MATRIXINPUT"]`` :py:attr:`.ImageCms.Flags.CLUT_POST_LINEARIZATION` +``ImageCms.FLAGS["MATRIXOUTPUT"]`` :py:attr:`.ImageCms.Flags.FORCE_CLUT` +``ImageCms.FLAGS["MATRIXONLY"]`` No replacement +``ImageCms.FLAGS["NOWHITEONWHITEFIXUP"]`` :py:attr:`.ImageCms.Flags.NOWHITEONWHITEFIXUP` +``ImageCms.FLAGS["NOPRELINEARIZATION"]`` :py:attr:`.ImageCms.Flags.CLUT_PRE_LINEARIZATION` +``ImageCms.FLAGS["GUESSDEVICECLASS"]`` :py:attr:`.ImageCms.Flags.GUESSDEVICECLASS` +``ImageCms.FLAGS["NOTCACHE"]`` :py:attr:`.ImageCms.Flags.NOCACHE` +``ImageCms.FLAGS["NOTPRECALC"]`` :py:attr:`.ImageCms.Flags.NOOPTIMIZE` +``ImageCms.FLAGS["NULLTRANSFORM"]`` :py:attr:`.ImageCms.Flags.NULLTRANSFORM` +``ImageCms.FLAGS["HIGHRESPRECALC"]`` :py:attr:`.ImageCms.Flags.HIGHRESPRECALC` +``ImageCms.FLAGS["LOWRESPRECALC"]`` :py:attr:`.ImageCms.Flags.LOWRESPRECALC` +``ImageCms.FLAGS["GAMUTCHECK"]`` :py:attr:`.ImageCms.Flags.GAMUTCHECK` +``ImageCms.FLAGS["WHITEBLACKCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["BLACKPOINTCOMPENSATION"]`` :py:attr:`.ImageCms.Flags.BLACKPOINTCOMPENSATION` +``ImageCms.FLAGS["SOFTPROOFING"]`` :py:attr:`.ImageCms.Flags.SOFTPROOFING` +``ImageCms.FLAGS["PRESERVEBLACK"]`` :py:attr:`.ImageCms.Flags.NONEGATIVES` +``ImageCms.FLAGS["NODEFAULTRESOURCEDEF"]`` :py:attr:`.ImageCms.Flags.NODEFAULTRESOURCEDEF` +``ImageCms.FLAGS["GRIDPOINTS"]`` :py:attr:`.ImageCms.Flags.GRIDPOINTS()` +``ImageCms.versions()`` :py:func:`PIL.features.version_module` with + ``feature="littlecms2"``, :py:data:`sys.version` or + :py:data:`sys.version_info`, and ``PIL.__version__`` +============================================ ==================================================== + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Added PerspectiveTransform +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:class:`~PIL.ImageTransform.PerspectiveTransform` has been added, meaning +that all of the :py:data:`~PIL.Image.Transform` values now have a corresponding +subclass of :py:class:`~PIL.ImageTransform.Transform`. + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +Portable FloatMap (PFM) images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for reading and writing grayscale (Pf format) +Portable FloatMap (PFM) files containing ``F`` data. + +Release GIL when fetching WebP frames +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Python's Global Interpreter Lock is now released when fetching WebP frames from +the libwebp decoder. diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 2bf299dd3..1fc245c9a 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -30,7 +30,7 @@ Image.fromstring, im.fromstring and im.tostring ImageCms.CmsProfile attributes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed: +Some attributes in :py:class:`PIL.ImageCms.core.CmsProfile` have been removed: ======================== =================================================== Removed Use instead diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 02da702a7..6400218f4 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -51,7 +51,7 @@ 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. +(2023-07-01). Instead, :py:class:`enum.IntEnum` classes have been added. .. note:: diff --git a/docs/releasenotes/9.3.0.rst b/docs/releasenotes/9.3.0.rst index fde2faae3..16075ce95 100644 --- a/docs/releasenotes/9.3.0.rst +++ b/docs/releasenotes/9.3.0.rst @@ -33,8 +33,9 @@ Added ExifTags enums ^^^^^^^^^^^^^^^^^^^^ The data from :py:data:`~PIL.ExifTags.TAGS` and -:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as ``enum.IntEnum`` -classes: :py:data:`~PIL.ExifTags.Base` and :py:data:`~PIL.ExifTags.GPS`. +:py:data:`~PIL.ExifTags.GPSTAGS` is now also exposed as +:py:class:`enum.IntEnum` classes: :py:data:`~PIL.ExifTags.Base` and +:py:data:`~PIL.ExifTags.GPS`. Security diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index d8034853c..e86f8082b 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 10.3.0 10.2.0 10.1.0 10.0.1 diff --git a/pyproject.toml b/pyproject.toml index 193e8c9b2..58c2464bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ classifiers = [ "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", "Topic :: Multimedia :: Graphics :: Graphics Conversion", "Topic :: Multimedia :: Graphics :: Viewers", + "Typing :: Typed", ] dynamic = [ "version", @@ -65,6 +66,9 @@ tests = [ "pytest-cov", "pytest-timeout", ] +typing = [ + 'typing-extensions; python_version < "3.10"', +] xmp = [ "defusedxml", ] @@ -93,7 +97,7 @@ config-settings = "raqm=enable raqm=vendor fribidi=vendor imagequant=disable" test-command = "cd {project} && .github/workflows/wheels-test.sh" test-extras = "tests" -[tool.ruff] +[tool.ruff.lint] select = [ "C4", # flake8-comprehensions "E", # pycodestyle errors @@ -101,24 +105,25 @@ select = [ "F", # pyflakes errors "I", # isort "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging "PGH", # pygrep-hooks "RUF100", # unused noqa (yesqa) "UP", # pyupgrade "W", # pycodestyle warnings "YTT", # flake8-2020 - # "LOG", # TODO: enable flake8-logging when it's not in preview anymore ] -extend-ignore = [ +ignore = [ "E203", # Whitespace before ':' "E221", # Multiple spaces before operator "E226", # Missing whitespace around arithmetic operator "E241", # Multiple spaces after ',' ] -[tool.ruff.per-file-ignores] -"Tests/*.py" = ["I001"] +[tool.ruff.lint.per-file-ignores] +"Tests/oss-fuzz/fuzz_font.py" = ["I002"] +"Tests/oss-fuzz/fuzz_pillow.py" = ["I002"] -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["PIL"] required-imports = ["from __future__ import annotations"] @@ -136,23 +141,3 @@ follow_imports = "silent" warn_redundant_casts = true warn_unreachable = true warn_unused_ignores = true -exclude = [ - '^src/PIL/_tkinter_finder.py$', - '^src/PIL/DdsImagePlugin.py$', - '^src/PIL/FpxImagePlugin.py$', - '^src/PIL/Image.py$', - '^src/PIL/ImageCms.py$', - '^src/PIL/ImageFile.py$', - '^src/PIL/ImageFont.py$', - '^src/PIL/ImageMath.py$', - '^src/PIL/ImageMorph.py$', - '^src/PIL/ImageQt.py$', - '^src/PIL/ImageShow.py$', - '^src/PIL/ImImagePlugin.py$', - '^src/PIL/MicImagePlugin.py$', - '^src/PIL/PdfParser.py$', - '^src/PIL/PyAccess.py$', - '^src/PIL/TiffImagePlugin.py$', - '^src/PIL/TiffTags.py$', - '^src/PIL/WebPImagePlugin.py$', -] diff --git a/selftest.py b/selftest.py index 600fd6496..ed5252c44 100755 --- a/selftest.py +++ b/selftest.py @@ -15,7 +15,7 @@ except AttributeError: pass -def testimage(): +def testimage() -> None: """ PIL lets you create in-memory images with various pixel types: diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 07163d001..0ad9204d0 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 # > pyroma . # ------------------------------ # Checking . diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index b12ddc2d4..e3eda4fe9 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -22,6 +22,8 @@ Parse X Bitmap Distribution Format (BDF) """ from __future__ import annotations +from typing import BinaryIO + from . import FontFile, Image bdf_slant = { @@ -36,7 +38,17 @@ bdf_slant = { bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"} -def bdf_char(f): +def bdf_char( + f: BinaryIO, +) -> ( + tuple[ + str, + int, + tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]], + Image.Image, + ] + | None +): # skip to STARTCHAR while True: s = f.readline() @@ -56,13 +68,12 @@ def bdf_char(f): props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii") # load bitmap - bitmap = [] + bitmap = bytearray() while True: s = f.readline() if not s or s[:7] == b"ENDCHAR": break - bitmap.append(s[:-1]) - bitmap = b"".join(bitmap) + bitmap += s[:-1] # The word BBX # followed by the width in x (BBw), height in y (BBh), @@ -92,7 +103,7 @@ def bdf_char(f): class BdfFontFile(FontFile.FontFile): """Font file plugin for the X11 BDF format.""" - def __init__(self, fp): + def __init__(self, fp: BinaryIO): super().__init__() s = fp.readline() diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index b8f38b78a..f0fbc8cc2 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -28,6 +28,7 @@ BLP files come in many different flavours: - DXT3 compression is used if alpha_encoding == 1. - DXT5 compression is used if alpha_encoding == 7. """ + from __future__ import annotations import os diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 387a4c182..0035296a4 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -16,15 +16,16 @@ from __future__ import annotations import io +from typing import IO, AnyStr, Generic, Literal -class ContainerIO: +class ContainerIO(Generic[AnyStr]): """ A file object that provides read access to a part of an existing file (for example a TAR file). """ - def __init__(self, file, offset, length): + def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None: """ Create file object. @@ -32,7 +33,7 @@ class ContainerIO: :param offset: Start of region, in bytes. :param length: Size of region, in bytes. """ - self.fh = file + self.fh: IO[AnyStr] = file self.pos = 0 self.offset = offset self.length = length @@ -41,10 +42,10 @@ class ContainerIO: ## # Always false. - def isatty(self): + def isatty(self) -> bool: return False - def seek(self, offset, mode=io.SEEK_SET): + def seek(self, offset: int, mode: Literal[0, 1, 2] = io.SEEK_SET) -> None: """ Move file pointer. @@ -63,7 +64,7 @@ class ContainerIO: self.pos = max(0, min(self.pos, self.length)) self.fh.seek(self.offset + self.pos) - def tell(self): + def tell(self) -> int: """ Get current file pointer. @@ -71,7 +72,7 @@ class ContainerIO: """ return self.pos - def read(self, n=0): + def read(self, n: int = 0) -> AnyStr: """ Read data. @@ -84,17 +85,17 @@ class ContainerIO: else: n = self.length - self.pos if not n: # EOF - return b"" if "b" in self.fh.mode else "" + return b"" if "b" in self.fh.mode else "" # type: ignore[return-value] self.pos = self.pos + n return self.fh.read(n) - def readline(self): + def readline(self) -> AnyStr: """ Read a line of text. :returns: An 8-bit string. """ - s = b"" if "b" in self.fh.mode else "" + s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment] newline_character = b"\n" if "b" in self.fh.mode else "\n" while True: c = self.read(1) @@ -105,7 +106,7 @@ class ContainerIO: break return s - def readlines(self): + def readlines(self) -> list[AnyStr]: """ Read multiple lines of text. diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 5b6ac2ead..be17f4223 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -9,6 +9,7 @@ The contents of this file are hereby released in the public domain (CC0) Full text of the CC0 license: https://creativecommons.org/publicdomain/zero/1.0/ """ + from __future__ import annotations import io @@ -18,6 +19,7 @@ from enum import IntEnum, IntFlag from . import Image, ImageFile, ImagePalette from ._binary import i32le as i32 +from ._binary import o8 from ._binary import o32le as o32 # Magic ("DDS ") @@ -268,13 +270,17 @@ class D3DFMT(IntEnum): # Backward compatibility layer module = sys.modules[__name__] for item in DDSD: + assert item.name is not None setattr(module, "DDSD_" + item.name, item.value) -for item in DDSCAPS: - setattr(module, "DDSCAPS_" + item.name, item.value) -for item in DDSCAPS2: - setattr(module, "DDSCAPS2_" + item.name, item.value) -for item in DDPF: - setattr(module, "DDPF_" + item.name, item.value) +for item1 in DDSCAPS: + assert item1.name is not None + setattr(module, "DDSCAPS_" + item1.name, item1.value) +for item2 in DDSCAPS2: + assert item2.name is not None + setattr(module, "DDSCAPS2_" + item2.name, item2.value) +for item3 in DDPF: + assert item3.name is not None + setattr(module, "DDPF_" + item3.name, item3.value) DDS_FOURCC = DDPF.FOURCC DDS_RGB = DDPF.RGB @@ -341,6 +347,7 @@ class DdsImageFile(ImageFile.ImageFile): flags, height, width = struct.unpack("<3I", header.read(12)) self._size = (width, height) + extents = (0, 0) + self.size pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) struct.unpack("<11I", header.read(44)) # reserved @@ -351,22 +358,16 @@ class DdsImageFile(ImageFile.ImageFile): rawmode = None if pfflags & DDPF.RGB: # Texture contains uncompressed RGB data - masks = struct.unpack("<4I", header.read(16)) - masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)} - if bitcount == 24: - self._mode = "RGB" - rawmode = masks[0x000000FF] + masks[0x0000FF00] + masks[0x00FF0000] - elif bitcount == 32 and pfflags & DDPF.ALPHAPIXELS: + if pfflags & DDPF.ALPHAPIXELS: self._mode = "RGBA" - rawmode = ( - masks[0x000000FF] - + masks[0x0000FF00] - + masks[0x00FF0000] - + masks[0xFF000000] - ) + mask_count = 4 else: - msg = f"Unsupported bitcount {bitcount} for {pfflags}" - raise OSError(msg) + self._mode = "RGB" + mask_count = 3 + + masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4)) + self.tile = [("dds_rgb", extents, 0, (bitcount, masks))] + return elif pfflags & DDPF.LUMINANCE: if bitcount == 8: self._mode = "L" @@ -464,7 +465,6 @@ class DdsImageFile(ImageFile.ImageFile): msg = f"Unknown pixel format flags {pfflags}" raise NotImplementedError(msg) - extents = (0, 0) + self.size if n: self.tile = [ ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format)) @@ -476,6 +476,39 @@ class DdsImageFile(ImageFile.ImageFile): pass +class DdsRgbDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def decode(self, buffer): + bitcount, masks = self.args + + # Some masks will be padded with zeros, e.g. R 0b11 G 0b1100 + # Calculate how many zeros each mask is padded with + mask_offsets = [] + # And the maximum value of each channel without the padding + mask_totals = [] + for mask in masks: + offset = 0 + if mask != 0: + while mask >> (offset + 1) << (offset + 1) == mask: + offset += 1 + mask_offsets.append(offset) + mask_totals.append(mask >> offset) + + data = bytearray() + bytecount = bitcount // 8 + while len(data) < self.state.xsize * self.state.ysize * len(masks): + value = int.from_bytes(self.fd.read(bytecount), "little") + for i, mask in enumerate(masks): + masked_value = value & mask + # Remove the zero padding, and scale it to 8 bits + data += o8( + int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) + ) + self.set_as_raw(bytes(data)) + return -1, 0 + + def _save(im, fp, filename): if im.mode not in ("RGB", "RGBA", "L", "LA"): msg = f"cannot write mode {im.mode} as DDS" @@ -533,5 +566,6 @@ def _accept(prefix): Image.register_open(DdsImageFile.format, DdsImageFile, _accept) +Image.register_decoder("dds_rgb", DdsRgbDecoder) Image.register_save(DdsImageFile.format, _save) Image.register_extension(DdsImageFile.format, ".dds") diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 7dce2d60f..e69890bab 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -15,7 +15,7 @@ import math from . import Image, ImageFile -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:6] == b"SIMPLE" @@ -23,8 +23,10 @@ class FitsImageFile(ImageFile.ImageFile): format = "FITS" format_description = "FITS" - def _open(self): - headers = {} + def _open(self) -> None: + assert self.fp is not None + + headers: dict[bytes, bytes] = {} while True: header = self.fp.read(80) if not header: diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 9769761fc..f9e4c731c 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -77,6 +77,7 @@ class FliImageFile(ImageFile.ImageFile): if i16(s, 4) == 0xF100: # prefix chunk; ignore it self.__offset = self.__offset + i32(s) + self.fp.seek(self.__offset) s = self.fp.read(16) if i16(s, 4) == 0xF1FA: diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 9621770e2..1e0c1c166 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -16,13 +16,16 @@ from __future__ import annotations import os +from typing import BinaryIO from . import Image, _binary WIDTH = 800 -def puti16(fp, values): +def puti16( + fp: BinaryIO, values: tuple[int, int, int, int, int, int, int, int, int, int] +) -> None: """Write network order (big-endian) 16-bit sequence""" for v in values: if v < 0: @@ -33,16 +36,32 @@ def puti16(fp, values): class FontFile: """Base class for raster font file handlers.""" - bitmap = None + bitmap: Image.Image | None = None - def __init__(self): - self.info = {} - self.glyph = [None] * 256 + def __init__(self) -> None: + self.info: dict[bytes, bytes | int] = {} + self.glyph: list[ + tuple[ + tuple[int, int], + tuple[int, int, int, int], + tuple[int, int, int, int], + Image.Image, + ] + | None + ] = [None] * 256 - def __getitem__(self, ix): + def __getitem__(self, ix: int) -> ( + tuple[ + tuple[int, int], + tuple[int, int, int, int], + tuple[int, int, int, int], + Image.Image, + ] + | None + ): return self.glyph[ix] - def compile(self): + def compile(self) -> None: """Create metrics and bitmap""" if self.bitmap: @@ -51,7 +70,7 @@ class FontFile: # create bitmap large enough to hold all data h = w = maxwidth = 0 lines = 1 - for glyph in self: + for glyph in self.glyph: if glyph: d, dst, src, im = glyph h = max(h, src[3] - src[1]) @@ -65,13 +84,16 @@ class FontFile: ysize = lines * h if xsize == 0 and ysize == 0: - return "" + return self.ysize = h # paste glyphs into bitmap self.bitmap = Image.new("1", (xsize, ysize)) - self.metrics = [None] * 256 + self.metrics: list[ + tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]] + | None + ] = [None] * 256 x = y = 0 for i in range(256): glyph = self[i] @@ -88,12 +110,15 @@ class FontFile: self.bitmap.paste(im.crop(src), s) self.metrics[i] = d, dst, s - def save(self, filename): + def save(self, filename: str) -> None: """Save font""" self.compile() # font data + if not self.bitmap: + msg = "No bitmap created" + raise ValueError(msg) self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG") # font metrics @@ -104,6 +129,6 @@ class FontFile: for id in range(256): m = self.metrics[id] if not m: - puti16(fp, [0] * 10) + puti16(fp, (0,) * 10) else: puti16(fp, m[0] + m[1] + m[2]) diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index d5513a56a..b4488e6ee 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -50,6 +50,7 @@ bytes for that mipmap level. Note: All data is stored in little-Endian (Intel) byte order. """ + from __future__ import annotations import struct diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index d84876eb6..88b87a22c 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -27,9 +27,12 @@ """ from __future__ import annotations +from typing import IO + from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i16be as i16 from ._binary import i32be as i32 +from ._typing import StrOrBytesPath class GdImageFile(ImageFile.ImageFile): @@ -43,8 +46,10 @@ class GdImageFile(ImageFile.ImageFile): format = "GD" format_description = "GD uncompressed images" - def _open(self): + def _open(self) -> None: # Header + assert self.fp is not None + s = self.fp.read(1037) if i16(s) not in [65534, 65535]: @@ -76,7 +81,7 @@ class GdImageFile(ImageFile.ImageFile): ] -def open(fp, mode="r"): +def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile: """ Load texture from a GD image file. diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 57d87078b..dc842d7a3 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -641,9 +641,9 @@ def _write_multiple_frames(im, fp, palette): if encoderinfo.get("optimize") and im_frame.mode != "1": if "transparency" not in encoderinfo: try: - encoderinfo[ - "transparency" - ] = im_frame.palette._new_color_index(im_frame) + encoderinfo["transparency"] = ( + im_frame.palette._new_color_index(im_frame) + ) except ValueError: pass if "transparency" in encoderinfo: diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 97d726a8a..4613e40b6 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -93,8 +93,8 @@ for i in ["16", "16L", "16B"]: for i in ["32S"]: OPEN[f"L {i} image"] = ("I", f"I;{i}") OPEN[f"L*{i} image"] = ("I", f"I;{i}") -for i in range(2, 33): - OPEN[f"L*{i} image"] = ("F", f"F;{i}") +for j in range(2, 33): + OPEN[f"L*{j} image"] = ("F", f"F;{j}") # -------------------------------------------------------------------- diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1cb484b85..ba81a22c7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -26,6 +26,7 @@ from __future__ import annotations +import abc import atexit import builtins import io @@ -39,12 +40,8 @@ import tempfile import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum -from pathlib import Path - -try: - from defusedxml import ElementTree -except ImportError: - ElementTree = None +from types import ModuleType +from typing import IO, TYPE_CHECKING, Any # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -60,6 +57,12 @@ from . import ( from ._binary import i32le, o32be, o32le from ._util import DeferredError, is_path +ElementTree: ModuleType | None +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + logger = logging.getLogger(__name__) @@ -92,7 +95,7 @@ try: raise ImportError(msg) except ImportError as v: - core = DeferredError(ImportError("The _imaging C module is not installed.")) + core = DeferredError.new(ImportError("The _imaging C module is not installed.")) # Explanations for ways that we know we might have an import error if str(v).startswith("Module use of python"): # The _imaging C module is present, but not compiled for @@ -110,6 +113,7 @@ except ImportError as v: USE_CFFI_ACCESS = False +cffi: ModuleType | None try: import cffi except ImportError: @@ -211,14 +215,22 @@ if hasattr(core, "DEFAULT_STRATEGY"): # -------------------------------------------------------------------- # Registries -ID = [] -OPEN = {} -MIME = {} -SAVE = {} -SAVE_ALL = {} -EXTENSION = {} -DECODERS = {} -ENCODERS = {} +if TYPE_CHECKING: + from . import ImageFile +ID: list[str] = [] +OPEN: dict[ + str, + tuple[ + Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + Callable[[bytes], bool] | None, + ], +] = {} +MIME: dict[str, str] = {} +SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +EXTENSION: dict[str, str] = {} +DECODERS: dict[str, object] = {} +ENCODERS: dict[str, object] = {} # -------------------------------------------------------------------- # Modes @@ -242,7 +254,7 @@ MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", " _MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") -def getmodebase(mode): +def getmodebase(mode: str) -> str: """ Gets the "base" mode for given mode. This function returns "L" for images that contain grayscale data, and "RGB" for images that @@ -282,7 +294,7 @@ def getmodebandnames(mode): return ImageMode.getmode(mode).bands -def getmodebands(mode): +def getmodebands(mode: str) -> int: """ Gets the number of individual bands for this mode. @@ -530,15 +542,19 @@ class Image: def __enter__(self): return self + def _close_fp(self): + if getattr(self, "_fp", False): + if self._fp != self.fp: + self._fp.close() + self._fp = DeferredError(ValueError("Operation on closed image")) + if self.fp: + self.fp.close() + def __exit__(self, *args): - if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False): - if getattr(self, "_fp", False): - if self._fp != self.fp: - self._fp.close() - self._fp = DeferredError(ValueError("Operation on closed image")) - if self.fp: - self.fp.close() - self.fp = None + if hasattr(self, "fp"): + if getattr(self, "_exclusive_fp", False): + self._close_fp() + self.fp = None def close(self): """ @@ -554,12 +570,7 @@ class Image: """ if hasattr(self, "fp"): try: - if getattr(self, "_fp", False): - if self._fp != self.fp: - self._fp.close() - self._fp = DeferredError(ValueError("Operation on closed image")) - if self.fp: - self.fp.close() + self._close_fp() self.fp = None except Exception as msg: logger.debug("Error closing: %s", msg) @@ -572,7 +583,7 @@ class Image: # object is gone. self.im = DeferredError(ValueError("Operation on closed image")) - def _copy(self): + def _copy(self) -> None: self.load() self.im = self.im.copy() self.pyaccess = None @@ -584,7 +595,9 @@ class Image: else: self.load() - def _dump(self, file=None, format=None, **options): + def _dump( + self, file: str | None = None, format: str | None = None, **options + ) -> str: suffix = "" if format: suffix = "." + format @@ -709,7 +722,7 @@ class Image: self.putpalette(palette) self.frombytes(data) - def tobytes(self, encoder_name="raw", *args): + def tobytes(self, encoder_name: str = "raw", *args) -> bytes: """ Return image as a bytes object. @@ -787,7 +800,7 @@ class Image: ] ) - def frombytes(self, data, decoder_name="raw", *args): + def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None: """ Loads this image with pixel data from a bytes object. @@ -874,7 +887,7 @@ class Image: def convert( self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256 - ): + ) -> Image: """ Returns a converted copy of this image. For the "P" mode, this method translates pixels through the palette. If mode is @@ -1181,7 +1194,7 @@ class Image: return im - def copy(self): + def copy(self) -> Image: """ Copies this image. Use this method if you wish to paste things into an image, but still retain the original. @@ -1194,7 +1207,7 @@ class Image: __copy__ = copy - def crop(self, box=None): + def crop(self, box=None) -> Image: """ Returns a rectangular region from this image. The box is a 4-tuple defining the left, upper, right, and lower pixel @@ -1296,7 +1309,7 @@ class Image: ] return merge(self.mode, ims) - def getbands(self): + def getbands(self) -> tuple[str, ...]: """ Returns a tuple containing the name of each band in this image. For example, ``getbands`` on an RGB image returns ("R", "G", "B"). @@ -1306,7 +1319,7 @@ class Image: """ return ImageMode.getmode(self.mode).bands - def getbbox(self, *, alpha_only=True): + def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]: """ Calculates the bounding box of the non-zero regions in the image. @@ -1417,7 +1430,7 @@ class Image: root = ElementTree.fromstring(xmp_tags) return {get_name(root.tag): get_value(root)} - def getexif(self): + def getexif(self) -> Exif: """ Gets EXIF data from the image. @@ -1425,7 +1438,6 @@ class Image: """ if self._exif is None: self._exif = Exif() - self._exif._loaded = False elif self._exif._loaded: return self._exif self._exif._loaded = True @@ -1512,7 +1524,7 @@ class Image: self.load() return self.im.ptr - def getpalette(self, rawmode="RGB"): + def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: """ Returns the image palette as a list. @@ -1602,7 +1614,7 @@ class Image: x, y = self.im.getprojection() return list(x), list(y) - def histogram(self, mask=None, extrema=None): + def histogram(self, mask=None, extrema=None) -> list[int]: """ Returns a histogram for the image. The histogram is returned as a list of pixel counts, one for each pixel value in the source @@ -1659,7 +1671,7 @@ class Image: return self.im.entropy(extrema) return self.im.entropy() - def paste(self, im, box=None, mask=None): + def paste(self, im, box=None, mask=None) -> None: """ Pastes another image into this image. The box argument is either a 2-tuple giving the upper left corner, a 4-tuple defining the @@ -1791,7 +1803,7 @@ class Image: result = alpha_composite(background, overlay) self.paste(result, box) - def point(self, lut, mode=None): + def point(self, lut, mode: str | None = None) -> Image: """ Maps this image through a lookup table or function. @@ -1915,7 +1927,7 @@ class Image: self.im.putdata(data, scale, offset) - def putpalette(self, data, rawmode="RGB"): + def putpalette(self, data, rawmode="RGB") -> None: """ Attaches a palette to this image. The image must be a "P", "PA", "L" or "LA" image. @@ -2095,7 +2107,7 @@ class Image: min(self.size[1], math.ceil(box[3] + support_y)), ) - def resize(self, size, resample=None, box=None, reducing_gap=None): + def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image: """ Returns a resized copy of this image. @@ -2187,10 +2199,11 @@ class Image: if factor_x > 1 or factor_y > 1: reduce_box = self._get_safe_box(size, resample, box) factor = (factor_x, factor_y) - if callable(self.reduce): - self = self.reduce(factor, box=reduce_box) - else: - self = Image.reduce(self, factor, box=reduce_box) + self = ( + self.reduce(factor, box=reduce_box) + if callable(self.reduce) + else Image.reduce(self, factor, box=reduce_box) + ) box = ( (box[0] - reduce_box[0]) / factor_x, (box[1] - reduce_box[1]) / factor_y, @@ -2352,7 +2365,7 @@ class Image: (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor ) - def save(self, fp, format=None, **params): + def save(self, fp, format=None, **params) -> None: """ Saves this image under the given filename. If no format is specified, the format to use is determined from the filename @@ -2369,7 +2382,7 @@ class Image: implement the ``seek``, ``tell``, and ``write`` methods, and be opened in binary mode. - :param fp: A filename (string), pathlib.Path object or file object. + :param fp: A filename (string), os.PathLike object or file object. :param format: Optional format override. If omitted, the format to use is determined from the filename extension. If a file object was used instead of a filename, this @@ -2382,13 +2395,10 @@ class Image: may have been created, and may contain partial data. """ - filename = "" + filename: str | bytes = "" open_fp = False - if isinstance(fp, Path): - filename = str(fp) - open_fp = True - elif is_path(fp): - filename = fp + if is_path(fp): + filename = os.path.realpath(os.fspath(fp)) open_fp = True elif fp == sys.stdout: try: @@ -2397,7 +2407,7 @@ class Image: pass if not filename and hasattr(fp, "name") and is_path(fp.name): # only set the name for metadata purposes - filename = fp.name + filename = os.path.realpath(os.fspath(fp.name)) # may mutate self! self._ensure_mutable() @@ -2408,7 +2418,8 @@ class Image: preinit() - ext = os.path.splitext(filename)[1].lower() + filename_ext = os.path.splitext(filename)[1].lower() + ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext if not format: if ext not in EXTENSION: @@ -2450,7 +2461,7 @@ class Image: if open_fp: fp.close() - def seek(self, frame): + def seek(self, frame) -> None: """ Seeks to the given frame in this sequence file. If you seek beyond the end of the sequence, the method raises an @@ -2494,7 +2505,7 @@ class Image: _show(self, title=title) - def split(self): + def split(self) -> tuple[Image, ...]: """ Split this image into individual bands. This method returns a tuple of individual image bands from an image. For example, @@ -2510,10 +2521,8 @@ class Image: self.load() if self.im.bands == 1: - ims = [self.copy()] - else: - ims = map(self._new, self.im.split()) - return tuple(ims) + return (self.copy(),) + return tuple(map(self._new, self.im.split())) def getchannel(self, channel): """ @@ -2537,7 +2546,7 @@ class Image: return self._new(self.im.getband(channel)) - def tell(self): + def tell(self) -> int: """ Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`. @@ -2644,7 +2653,7 @@ class Image: resample=Resampling.NEAREST, fill=1, fillcolor=None, - ): + ) -> Image: """ Transforms this image. This method creates a new image with the given size, and the same mode as the original, and copies data @@ -2667,6 +2676,10 @@ class Image: def transform(self, size, data, resample, fill=1): # Return result + Implementations of :py:class:`~PIL.Image.ImageTransformHandler` + for some of the :py:class:`Transform` methods are provided + in :py:mod:`~PIL.ImageTransform`. + It may also be an object with a ``method.getdata`` method that returns a tuple supplying new ``method`` and ``data`` values:: @@ -2805,7 +2818,7 @@ class Image: self.im.transform2(box, image.im, method, data, resample, fill) - def transpose(self, method): + def transpose(self, method: Transpose) -> Image: """ Transpose image (flip or rotate in 90 degree steps) @@ -2857,7 +2870,9 @@ class ImagePointHandler: (for use with :py:meth:`~PIL.Image.Image.point`) """ - pass + @abc.abstractmethod + def point(self, im: Image) -> Image: + pass class ImageTransformHandler: @@ -2866,7 +2881,14 @@ class ImageTransformHandler: (for use with :py:meth:`~PIL.Image.Image.transform`) """ - pass + @abc.abstractmethod + def transform( + self, + size: tuple[int, int], + image: Image, + **options: dict[str, str | int | tuple[int, ...] | list[int]], + ) -> Image: + pass # -------------------------------------------------------------------- @@ -2903,7 +2925,7 @@ def _check_size(size): return True -def new(mode, size, color=0): +def new(mode, size, color=0) -> Image: """ Creates a new image with the given mode and size. @@ -2942,7 +2964,7 @@ def new(mode, size, color=0): return im._new(core.fill(mode, size, color)) -def frombytes(mode, size, data, decoder_name="raw", *args): +def frombytes(mode, size, data, decoder_name="raw", *args) -> Image: """ Creates a copy of an image memory from pixel data in a buffer. @@ -3191,7 +3213,7 @@ def _decompression_bomb_check(size): ) -def open(fp, mode="r", formats=None): +def open(fp, mode="r", formats=None) -> Image: """ Opens and identifies the given image file. @@ -3201,7 +3223,7 @@ def open(fp, mode="r", formats=None): :py:meth:`~PIL.Image.Image.load` method). See :py:func:`~PIL.Image.new`. See :ref:`file-handling`. - :param fp: A filename (string), pathlib.Path object or a file object. + :param fp: A filename (string), os.PathLike object or a file object. The file object must implement ``file.read``, ``file.seek``, and ``file.tell`` methods, and be opened in binary mode. The file object will also seek to zero @@ -3238,11 +3260,9 @@ def open(fp, mode="r", formats=None): raise TypeError(msg) exclusive_fp = False - filename = "" - if isinstance(fp, Path): - filename = str(fp.resolve()) - elif is_path(fp): - filename = fp + filename: str | bytes = "" + if is_path(fp): + filename = os.path.realpath(os.fspath(fp)) if filename: fp = builtins.open(filename, "rb") @@ -3416,7 +3436,11 @@ def merge(mode, bands): # Plugin registry -def register_open(id, factory, accept=None): +def register_open( + id, + factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + accept: Callable[[bytes], bool] | None = None, +) -> None: """ Register an image file plugin. This function should not be used in application code. @@ -3432,7 +3456,7 @@ def register_open(id, factory, accept=None): OPEN[id] = factory, accept -def register_mime(id, mimetype): +def register_mime(id: str, mimetype: str) -> None: """ Registers an image MIME type by populating ``Image.MIME``. This function should not be used in application code. @@ -3447,7 +3471,7 @@ def register_mime(id, mimetype): MIME[id.upper()] = mimetype -def register_save(id, driver): +def register_save(id: str, driver) -> None: """ Registers an image save function. This function should not be used in application code. @@ -3470,7 +3494,7 @@ def register_save_all(id, driver): SAVE_ALL[id.upper()] = driver -def register_extension(id, extension): +def register_extension(id, extension) -> None: """ Registers an image extension. This function should not be used in application code. @@ -3481,7 +3505,7 @@ def register_extension(id, extension): EXTENSION[extension.lower()] = id.upper() -def register_extensions(id, extensions): +def register_extensions(id, extensions) -> None: """ Registers image extensions. This function should not be used in application code. @@ -3502,7 +3526,7 @@ def registered_extensions(): return EXTENSION -def register_decoder(name, decoder): +def register_decoder(name: str, decoder) -> None: """ Registers an image decoder. This function should not be used in application code. @@ -3626,7 +3650,13 @@ _apply_env_variables() atexit.register(core.clear_cache) -class Exif(MutableMapping): +if TYPE_CHECKING: + _ExifBase = MutableMapping[int, Any] +else: + _ExifBase = MutableMapping + + +class Exif(_ExifBase): """ This class provides read and write access to EXIF image data:: @@ -3662,6 +3692,7 @@ class Exif(MutableMapping): endian = None bigtiff = False + _loaded = False def __init__(self): self._data = {} @@ -3777,7 +3808,7 @@ class Exif(MutableMapping): return merged_dict - def tobytes(self, offset=8): + def tobytes(self, offset: int = 8) -> bytes: from . import TiffImagePlugin head = self._get_head() @@ -3932,7 +3963,7 @@ class Exif(MutableMapping): del self._info[tag] self._data[tag] = value - def __delitem__(self, tag): + def __delitem__(self, tag: int) -> None: if self._info is not None and tag in self._info: del self._info[tag] else: diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 9d27f2513..2b0ed6c9d 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -4,6 +4,9 @@ # Optional color management support, based on Kevin Cazabon's PyCMS # library. +# Originally released under LGPL. Graciously donated to PIL in +# March 2009, for distribution under the standard PIL license + # History: # 2009-03-08 fl Added to PIL. @@ -16,10 +19,14 @@ # below for the original description. from __future__ import annotations +import operator import sys -from enum import IntEnum +from enum import IntEnum, IntFlag +from functools import reduce +from typing import Any from . import Image +from ._deprecate import deprecate try: from . import _imagingcms @@ -28,9 +35,9 @@ except ImportError as ex: # anything in core. from ._util import DeferredError - _imagingcms = DeferredError(ex) + _imagingcms = DeferredError.new(ex) -DESCRIPTION = """ +_DESCRIPTION = """ pyCMS a Python / PIL interface to the littleCMS ICC Color Management System @@ -93,7 +100,22 @@ pyCMS """ -VERSION = "1.0.0 pil" +_VERSION = "1.0.0 pil" + + +def __getattr__(name: str) -> Any: + if name == "DESCRIPTION": + deprecate("PIL.ImageCms.DESCRIPTION", 12) + return _DESCRIPTION + elif name == "VERSION": + deprecate("PIL.ImageCms.VERSION", 12) + return _VERSION + elif name == "FLAGS": + deprecate("PIL.ImageCms.FLAGS", 12, "PIL.ImageCms.Flags") + return _FLAGS + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) + # --------------------------------------------------------------------. @@ -119,7 +141,70 @@ class Direction(IntEnum): # # flags -FLAGS = { + +class Flags(IntFlag): + """Flags and documentation are taken from ``lcms2.h``.""" + + NONE = 0 + NOCACHE = 0x0040 + """Inhibit 1-pixel cache""" + NOOPTIMIZE = 0x0100 + """Inhibit optimizations""" + NULLTRANSFORM = 0x0200 + """Don't transform anyway""" + GAMUTCHECK = 0x1000 + """Out of Gamut alarm""" + SOFTPROOFING = 0x4000 + """Do softproofing""" + BLACKPOINTCOMPENSATION = 0x2000 + NOWHITEONWHITEFIXUP = 0x0004 + """Don't fix scum dot""" + HIGHRESPRECALC = 0x0400 + """Use more memory to give better accuracy""" + LOWRESPRECALC = 0x0800 + """Use less memory to minimize resources""" + # this should be 8BITS_DEVICELINK, but that is not a valid name in Python: + USE_8BITS_DEVICELINK = 0x0008 + """Create 8 bits devicelinks""" + GUESSDEVICECLASS = 0x0020 + """Guess device class (for ``transform2devicelink``)""" + KEEP_SEQUENCE = 0x0080 + """Keep profile sequence for devicelink creation""" + FORCE_CLUT = 0x0002 + """Force CLUT optimization""" + CLUT_POST_LINEARIZATION = 0x0001 + """create postlinearization tables if possible""" + CLUT_PRE_LINEARIZATION = 0x0010 + """create prelinearization tables if possible""" + NONEGATIVES = 0x8000 + """Prevent negative numbers in floating point transforms""" + COPY_ALPHA = 0x04000000 + """Alpha channels are copied on ``cmsDoTransform()``""" + NODEFAULTRESOURCEDEF = 0x01000000 + + _GRIDPOINTS_1 = 1 << 16 + _GRIDPOINTS_2 = 2 << 16 + _GRIDPOINTS_4 = 4 << 16 + _GRIDPOINTS_8 = 8 << 16 + _GRIDPOINTS_16 = 16 << 16 + _GRIDPOINTS_32 = 32 << 16 + _GRIDPOINTS_64 = 64 << 16 + _GRIDPOINTS_128 = 128 << 16 + + @staticmethod + def GRIDPOINTS(n: int) -> Flags: + """ + Fine-tune control over number of gridpoints + + :param n: :py:class:`int` in range ``0 <= n <= 255`` + """ + return Flags.NONE | ((n & 0xFF) << 16) + + +_MAX_FLAG = reduce(operator.or_, Flags) + + +_FLAGS = { "MATRIXINPUT": 1, "MATRIXOUTPUT": 2, "MATRIXONLY": (1 | 2), @@ -142,11 +227,6 @@ FLAGS = { "GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints } -_MAX_FLAG = 0 -for flag in FLAGS.values(): - if isinstance(flag, int): - _MAX_FLAG = _MAX_FLAG | flag - # --------------------------------------------------------------------. # Experimental PIL-level API @@ -201,7 +281,6 @@ class ImageCmsProfile: class ImageCmsTransform(Image.ImagePointHandler): - """ Transform. This can be used with the procedural API, or with the standard :py:func:`~PIL.Image.Image.point` method. @@ -218,7 +297,7 @@ class ImageCmsTransform(Image.ImagePointHandler): intent=Intent.PERCEPTUAL, proof=None, proof_intent=Intent.ABSOLUTE_COLORIMETRIC, - flags=0, + flags=Flags.NONE, ): if proof is None: self.transform = core.buildTransform( @@ -289,7 +368,6 @@ def get_display_profile(handle=None): class PyCMSError(Exception): - """(pyCMS) Exception class. This is used for all errors in the pyCMS API.""" @@ -303,7 +381,7 @@ def profileToProfile( renderingIntent=Intent.PERCEPTUAL, outputMode=None, inPlace=False, - flags=0, + flags=Flags.NONE, ): """ (pyCMS) Applies an ICC transformation to a given image, mapping from @@ -420,7 +498,7 @@ def buildTransform( inMode, outMode, renderingIntent=Intent.PERCEPTUAL, - flags=0, + flags=Flags.NONE, ): """ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the @@ -482,7 +560,7 @@ def buildTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" raise PyCMSError(msg) try: @@ -505,7 +583,7 @@ def buildProofTransform( outMode, renderingIntent=Intent.PERCEPTUAL, proofRenderingIntent=Intent.ABSOLUTE_COLORIMETRIC, - flags=FLAGS["SOFTPROOFING"], + flags=Flags.SOFTPROOFING, ): """ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the @@ -586,7 +664,7 @@ def buildProofTransform( raise PyCMSError(msg) if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - msg = "flags must be an integer between 0 and %s" + _MAX_FLAG + msg = f"flags must be an integer between 0 and {_MAX_FLAG}" raise PyCMSError(msg) try: @@ -1004,4 +1082,9 @@ def versions(): (pyCMS) Fetches versions. """ - return VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__ + deprecate( + "PIL.ImageCms.versions()", + 12, + '(PIL.features.version("littlecms2"), sys.version, PIL.__version__)', + ) + return _VERSION, core.littlecms_version, sys.version.split()[0], Image.__version__ diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index bfad27c82..5fb80b753 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -19,10 +19,12 @@ from __future__ import annotations import re +from functools import lru_cache from . import Image +@lru_cache def getrgb(color): """ Convert a color string to an RGB or RGBA tuple. If the string cannot be @@ -121,7 +123,8 @@ def getrgb(color): raise ValueError(msg) -def getcolor(color, mode): +@lru_cache +def getcolor(color, mode: str) -> tuple[int, ...]: """ Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 8611dcc36..d4e000087 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -33,8 +33,11 @@ from __future__ import annotations import math import numbers +import struct +from typing import Sequence, cast from . import Image, ImageColor +from ._typing import Coords """ A simple 2D drawing interface for PIL images. @@ -47,7 +50,7 @@ directly. class ImageDraw: font = None - def __init__(self, im, mode=None): + def __init__(self, im: Image.Image, mode: str | None = None) -> None: """ Create a drawing instance. @@ -114,7 +117,7 @@ class ImageDraw: self.font = ImageFont.load_default() return self.font - def _getfont(self, font_size): + def _getfont(self, font_size: float | None): if font_size is not None: from . import ImageFont @@ -123,7 +126,7 @@ class ImageDraw: font = self.getfont() return font - def _getink(self, ink, fill=None): + def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: if ink is None and fill is None: if self.fill: fill = self.ink @@ -144,13 +147,13 @@ class ImageDraw: fill = self.draw.draw_ink(fill) return ink, fill - def arc(self, xy, start, end, fill=None, width=1): + def arc(self, xy: Coords, start, end, fill=None, width=1) -> None: """Draw an arc.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_arc(xy, start, end, ink, width) - def bitmap(self, xy, bitmap, fill=None): + def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None: """Draw a bitmap.""" bitmap.load() ink, fill = self._getink(fill) @@ -159,7 +162,7 @@ class ImageDraw: if ink is not None: self.draw.draw_bitmap(xy, bitmap.im, ink) - def chord(self, xy, start, end, fill=None, outline=None, width=1): + def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None: """Draw a chord.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -167,7 +170,7 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_chord(xy, start, end, ink, 0, width) - def ellipse(self, xy, fill=None, outline=None, width=1): + def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw an ellipse.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -175,20 +178,29 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_ellipse(xy, ink, 0, width) - def line(self, xy, fill=None, width=0, joint=None): + def line(self, xy: Coords, fill=None, width=0, joint=None) -> None: """Draw a line, or a connected sequence of line segments.""" ink = self._getink(fill)[0] if ink is not None: self.draw.draw_lines(xy, ink, width) if joint == "curve" and width > 4: - if not isinstance(xy[0], (list, tuple)): - xy = [tuple(xy[i : i + 2]) for i in range(0, len(xy), 2)] - for i in range(1, len(xy) - 1): - point = xy[i] + points: Sequence[Sequence[float]] + if isinstance(xy[0], (list, tuple)): + points = cast(Sequence[Sequence[float]], xy) + else: + points = [ + cast(Sequence[float], tuple(xy[i : i + 2])) + for i in range(0, len(xy), 2) + ] + for i in range(1, len(points) - 1): + point = points[i] angles = [ math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) % 360 - for start, end in ((xy[i - 1], point), (point, xy[i + 1])) + for start, end in ( + (points[i - 1], point), + (point, points[i + 1]), + ) ] if angles[0] == angles[1]: # This is a straight line, so no joint is required @@ -235,7 +247,7 @@ class ImageDraw: ] self.line(gap_coords, fill, width=3) - def shape(self, shape, fill=None, outline=None): + def shape(self, shape, fill=None, outline=None) -> None: """(Experimental) Draw a shape.""" shape.close() ink, fill = self._getink(outline, fill) @@ -244,7 +256,9 @@ class ImageDraw: if ink is not None and ink != fill: self.draw.draw_outline(shape, ink, 0) - def pieslice(self, xy, start, end, fill=None, outline=None, width=1): + def pieslice( + self, xy: Coords, start, end, fill=None, outline=None, width=1 + ) -> None: """Draw a pieslice.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -252,13 +266,13 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_pieslice(xy, start, end, ink, 0, width) - def point(self, xy, fill=None): + def point(self, xy: Coords, fill=None) -> None: """Draw one or more individual pixels.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_points(xy, ink) - def polygon(self, xy, fill=None, outline=None, width=1): + def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw a polygon.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -266,7 +280,7 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: if width == 1: self.draw.draw_polygon(xy, ink, 0, width) - else: + elif self.im is not None: # To avoid expanding the polygon outwards, # use the fill as a mask mask = Image.new("1", self.im.size) @@ -290,12 +304,12 @@ class ImageDraw: def regular_polygon( self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 - ): + ) -> None: """Draw a regular polygon.""" xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) self.polygon(xy, fill, outline, width) - def rectangle(self, xy, fill=None, outline=None, width=1): + def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw a rectangle.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -304,13 +318,13 @@ class ImageDraw: self.draw.draw_rectangle(xy, ink, 0, width) def rounded_rectangle( - self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None - ): + self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None + ) -> None: """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): - (x0, y0), (x1, y1) = xy + (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy) else: - x0, y0, x1, y1 = xy + x0, y0, x1, y1 = cast(Sequence[float], xy) if x1 < x0: msg = "x1 must be greater than or equal to x0" raise ValueError(msg) @@ -345,7 +359,8 @@ class ImageDraw: r = d // 2 ink, fill = self._getink(outline, fill) - def draw_corners(pieslice): + def draw_corners(pieslice) -> None: + parts: tuple[tuple[tuple[float, float, float, float], int, int], ...] if full_x: # Draw top and bottom halves parts = ( @@ -360,17 +375,18 @@ class ImageDraw: ) else: # Draw four separate corners - parts = [] - for i, part in enumerate( - ( - ((x0, y0, x0 + d, y0 + d), 180, 270), - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), + parts = tuple( + part + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) ) - ): - if corners[i]: - parts.append(part) + if corners[i] + ) for part in parts: if pieslice: self.draw.draw_pieslice(*(part + (fill, 1))) @@ -430,12 +446,12 @@ class ImageDraw: right[3] -= r + 1 self.draw.draw_rectangle(right, ink, 1) - def _multiline_check(self, text): + def _multiline_check(self, text) -> bool: split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text - def _multiline_split(self, text): + def _multiline_split(self, text) -> list[str | bytes]: split_character = "\n" if isinstance(text, str) else b"\n" return text.split(split_character) @@ -464,7 +480,7 @@ class ImageDraw: embedded_color=False, *args, **kwargs, - ): + ) -> None: """Draw text.""" if embedded_color and self.mode not in ("RGB", "RGBA"): msg = "Embedded color supported only in RGB and RGBA modes" @@ -496,7 +512,7 @@ class ImageDraw: return fill return ink - def draw_text(ink, stroke_width=0, stroke_offset=None): + def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: mode = self.fontmode if stroke_width == 0 and embedded_color: mode = "RGBA" @@ -519,7 +535,7 @@ class ImageDraw: *args, **kwargs, ) - coord = coord[0] + offset[0], coord[1] + offset[1] + coord = [coord[0] + offset[0], coord[1] + offset[1]] except AttributeError: try: mask = font.getmask( @@ -538,14 +554,18 @@ class ImageDraw: except TypeError: mask = font.getmask(text) if stroke_offset: - coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] + coord = [coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]] if mode == "RGBA": # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A # extract mask and set text alpha color, mask = mask, mask.getband(3) - color.fillband(3, (ink >> 24) & 0xFF) + ink_alpha = struct.pack("i", ink)[3] + color.fillband(3, ink_alpha) x, y = coord - self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) + if self.im is not None: + self.im.paste( + color, (x, y, x + mask.size[0], y + mask.size[1]), mask + ) else: self.draw.draw_bitmap(coord, mask, ink) @@ -582,7 +602,7 @@ class ImageDraw: embedded_color=False, *, font_size=None, - ): + ) -> None: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" raise ValueError(msg) @@ -691,7 +711,7 @@ class ImageDraw: embedded_color=False, *, font_size=None, - ): + ) -> tuple[int, int, int, int]: """Get the bounding box of a given string, in pixels.""" if embedded_color and self.mode not in ("RGB", "RGBA"): msg = "Embedded color supported only in RGB and RGBA modes" @@ -736,7 +756,7 @@ class ImageDraw: embedded_color=False, *, font_size=None, - ): + ) -> tuple[int, int, int, int]: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" raise ValueError(msg) @@ -775,7 +795,7 @@ class ImageDraw: elif anchor[1] == "d": top -= (len(lines) - 1) * line_spacing - bbox = None + bbox: tuple[int, int, int, int] | None = None for idx, line in enumerate(lines): left = xy[0] @@ -826,7 +846,7 @@ class ImageDraw: return bbox -def Draw(im, mode=None): +def Draw(im, mode: str | None = None) -> ImageDraw: """ A simple 2D drawing interface for PIL images. @@ -874,7 +894,7 @@ def getdraw(im=None, hints=None): return im, handler -def floodfill(image, xy, value, border=None, thresh=0): +def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None: """ (experimental) Fills a bounded region with a given color. @@ -930,7 +950,9 @@ def floodfill(image, xy, value, border=None, thresh=0): edge = new_edge -def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): +def _compute_regular_polygon_vertices( + bounding_circle, n_sides, rotation +) -> list[tuple[float, float]]: """ Generate a list of vertices for a 2D regular polygon. @@ -980,7 +1002,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): # 1.2 Check `bounding_circle` has an appropriate value if not isinstance(bounding_circle, (list, tuple)): - msg = "bounding_circle should be a tuple" + msg = "bounding_circle should be a sequence" raise TypeError(msg) if len(bounding_circle) == 3: @@ -1012,7 +1034,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): raise ValueError(msg) # 2. Define Helper Functions - def _apply_rotation(point, degrees, centroid): + def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]: return ( round( point[0] * math.cos(math.radians(360 - degrees)) @@ -1028,11 +1050,11 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): ), ) - def _compute_polygon_vertex(centroid, polygon_radius, angle): + def _compute_polygon_vertex(angle: float) -> tuple[int, int]: start_point = [polygon_radius, 0] - return _apply_rotation(start_point, angle, centroid) + return _apply_rotation(start_point, angle) - def _get_angles(n_sides, rotation): + def _get_angles(n_sides: int, rotation: float) -> list[float]: angles = [] degrees = 360 / n_sides # Start with the bottom left polygon vertex @@ -1048,12 +1070,10 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): angles = _get_angles(n_sides, rotation) # 4. Compute Vertices - return [ - _compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles - ] + return [_compute_polygon_vertex(angle) for angle in angles] -def _color_diff(color1, color2): +def _color_diff(color1, color2: float | tuple[int, ...]) -> float: """ Uses 1-norm distance to calculate difference between two values. """ diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index ae4e23db1..e929b665e 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -32,7 +32,7 @@ import io import itertools import struct import sys -from typing import NamedTuple +from typing import IO, Any, NamedTuple from . import Image from ._deprecate import deprecate @@ -91,10 +91,10 @@ def _tilesort(t): class _Tile(NamedTuple): - encoder_name: str + codec_name: str extents: tuple[int, int, int, int] offset: int - args: tuple | str | None + args: tuple[Any, ...] | str | None # @@ -514,7 +514,7 @@ class Parser: # -------------------------------------------------------------------- -def _save(im, fp, tile, bufsize=0): +def _save(im, fp, tile, bufsize=0) -> None: """Helper to save image based on tile list :param im: Image object. @@ -616,6 +616,8 @@ class PyCodecState: class PyCodec: + fd: IO[bytes] | None + def __init__(self, mode, *args): self.im = None self.state = PyCodecState() @@ -713,7 +715,7 @@ class PyDecoder(PyCodec): msg = "unavailable in base decoder" raise NotImplementedError(msg) - def set_as_raw(self, data, rawmode=None): + def set_as_raw(self, data: bytes, rawmode=None) -> None: """ Convenience method to set the internal image from a stream of raw data diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 021b40c0e..035b83c4d 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -396,7 +396,7 @@ class Color3DLUT(MultibandFilter): if hasattr(table, "shape"): try: import numpy - except ImportError: # pragma: no cover + except ImportError: pass if numpy and isinstance(table, numpy.ndarray): diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 6db7cc4ec..256c581df 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -33,10 +33,10 @@ import sys import warnings from enum import IntEnum from io import BytesIO -from pathlib import Path -from typing import IO +from typing import BinaryIO from . import Image +from ._typing import StrOrBytesPath from ._util import is_directory, is_path @@ -53,7 +53,7 @@ try: except ImportError as ex: from ._util import DeferredError - core = DeferredError(ex) + core = DeferredError.new(ex) def _string_length_check(text): @@ -149,6 +149,8 @@ class ImageFont: :return: An internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module. """ + _string_length_check(text) + Image._decompression_bomb_check(self.font.getsize(text)) return self.font.getmask(text, mode) def getbbox(self, text, *args, **kwargs): @@ -191,7 +193,7 @@ class FreeTypeFont: def __init__( self, - font: bytes | str | Path | IO | None = None, + font: StrOrBytesPath | BinaryIO | None = None, size: float = 10, index: int = 0, encoding: str = "", @@ -228,8 +230,7 @@ class FreeTypeFont: ) if is_path(font): - if isinstance(font, Path): - font = str(font) + font = os.path.realpath(os.fspath(font)) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: @@ -582,22 +583,13 @@ class FreeTypeFont: _string_length_check(text) if start is None: start = (0, 0) - im = None - size = None def fill(width, height): - nonlocal im, size - size = (width, height) - if Image.MAX_IMAGE_PIXELS is not None: - pixels = max(1, width) * max(1, height) - if pixels > 2 * Image.MAX_IMAGE_PIXELS: - return + Image._decompression_bomb_check(size) + return Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size) - return im - - offset = self.font.render( + return self.font.render( text, fill, mode, @@ -610,8 +602,6 @@ class FreeTypeFont: start[0], start[1], ) - Image._decompression_bomb_check(size) - return im, offset def font_variant( self, font=None, size=None, index=None, encoding=None, layout_engine=None @@ -881,7 +871,7 @@ def load_path(filename): raise OSError(msg) -def load_default(size=None): +def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: """If FreeType support is available, load a version of Aileron Regular, https://dotcolon.net/font/aileron, with a more limited character set. diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index a4993d3d4..3f3be706d 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -149,18 +149,7 @@ def grabclipboard(): session_type = None if shutil.which("wl-paste") and session_type in ("wayland", None): - output = subprocess.check_output(["wl-paste", "-l"]).decode() - mimetypes = output.splitlines() - if "image/png" in mimetypes: - mimetype = "image/png" - elif mimetypes: - mimetype = mimetypes[0] - else: - mimetype = None - - args = ["wl-paste"] - if mimetype: - args.extend(["-t", mimetype]) + args = ["wl-paste", "-t", "image"] elif shutil.which("xclip") and session_type in ("x11", None): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: @@ -168,10 +157,29 @@ def grabclipboard(): raise NotImplementedError(msg) p = subprocess.run(args, capture_output=True) - err = p.stderr - if err: - msg = f"{args[0]} error: {err.strip().decode()}" + if p.returncode != 0: + err = p.stderr + for silent_error in [ + # wl-paste, when the clipboard is empty + b"Nothing is copied", + # Ubuntu/Debian wl-paste, when the clipboard is empty + b"No selection", + # Ubuntu/Debian wl-paste, when an image isn't available + b"No suitable type of content copied", + # wl-paste or Ubuntu/Debian xclip, when an image isn't available + b" not available", + # xclip, when an image isn't available + b"cannot convert ", + # xclip, when the clipboard isn't initialized + b"xclip: Error: There is no owner for the ", + ]: + if silent_error in err: + return None + msg = f"{args[0]} error" + if err: + msg += f": {err.strip().decode()}" raise ChildProcessError(msg) + data = io.BytesIO(p.stdout) im = Image.open(data) im.load() diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index 7ca512e75..a7652f237 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -17,6 +17,8 @@ from __future__ import annotations import builtins +from types import CodeType +from typing import Any from . import Image, _imagingmath @@ -24,10 +26,10 @@ from . import Image, _imagingmath class _Operand: """Wraps an image operand, providing standard operators""" - def __init__(self, im): + def __init__(self, im: Image.Image): self.im = im - def __fixup(self, im1): + def __fixup(self, im1: _Operand | float) -> Image.Image: # convert image to suitable mode if isinstance(im1, _Operand): # argument was an image. @@ -45,122 +47,131 @@ class _Operand: else: return Image.new("F", self.im.size, im1) - def apply(self, op, im1, im2=None, mode=None): - im1 = self.__fixup(im1) + def apply( + self, + op: str, + im1: _Operand | float, + im2: _Operand | float | None = None, + mode: str | None = None, + ) -> _Operand: + im_1 = self.__fixup(im1) if im2 is None: # unary operation - out = Image.new(mode or im1.mode, im1.size, None) - im1.load() + out = Image.new(mode or im_1.mode, im_1.size, None) + im_1.load() try: - op = getattr(_imagingmath, op + "_" + im1.mode) + op = getattr(_imagingmath, op + "_" + im_1.mode) except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.unop(op, out.im.id, im1.im.id) + _imagingmath.unop(op, out.im.id, im_1.im.id) else: # binary operation - im2 = self.__fixup(im2) - if im1.mode != im2.mode: + im_2 = self.__fixup(im2) + if im_1.mode != im_2.mode: # convert both arguments to floating point - if im1.mode != "F": - im1 = im1.convert("F") - if im2.mode != "F": - im2 = im2.convert("F") - if im1.size != im2.size: + if im_1.mode != "F": + im_1 = im_1.convert("F") + if im_2.mode != "F": + im_2 = im_2.convert("F") + if im_1.size != im_2.size: # crop both arguments to a common size - size = (min(im1.size[0], im2.size[0]), min(im1.size[1], im2.size[1])) - if im1.size != size: - im1 = im1.crop((0, 0) + size) - if im2.size != size: - im2 = im2.crop((0, 0) + size) - out = Image.new(mode or im1.mode, im1.size, None) - im1.load() - im2.load() + size = ( + min(im_1.size[0], im_2.size[0]), + min(im_1.size[1], im_2.size[1]), + ) + if im_1.size != size: + im_1 = im_1.crop((0, 0) + size) + if im_2.size != size: + im_2 = im_2.crop((0, 0) + size) + out = Image.new(mode or im_1.mode, im_1.size, None) + im_1.load() + im_2.load() try: - op = getattr(_imagingmath, op + "_" + im1.mode) + op = getattr(_imagingmath, op + "_" + im_1.mode) except AttributeError as e: msg = f"bad operand type for '{op}'" raise TypeError(msg) from e - _imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id) + _imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id) return _Operand(out) # unary operators - def __bool__(self): + def __bool__(self) -> bool: # an image is "true" if it contains at least one non-zero pixel return self.im.getbbox() is not None - def __abs__(self): + def __abs__(self) -> _Operand: return self.apply("abs", self) - def __pos__(self): + def __pos__(self) -> _Operand: return self - def __neg__(self): + def __neg__(self) -> _Operand: return self.apply("neg", self) # binary operators - def __add__(self, other): + def __add__(self, other: _Operand | float) -> _Operand: return self.apply("add", self, other) - def __radd__(self, other): + def __radd__(self, other: _Operand | float) -> _Operand: return self.apply("add", other, self) - def __sub__(self, other): + def __sub__(self, other: _Operand | float) -> _Operand: return self.apply("sub", self, other) - def __rsub__(self, other): + def __rsub__(self, other: _Operand | float) -> _Operand: return self.apply("sub", other, self) - def __mul__(self, other): + def __mul__(self, other: _Operand | float) -> _Operand: return self.apply("mul", self, other) - def __rmul__(self, other): + def __rmul__(self, other: _Operand | float) -> _Operand: return self.apply("mul", other, self) - def __truediv__(self, other): + def __truediv__(self, other: _Operand | float) -> _Operand: return self.apply("div", self, other) - def __rtruediv__(self, other): + def __rtruediv__(self, other: _Operand | float) -> _Operand: return self.apply("div", other, self) - def __mod__(self, other): + def __mod__(self, other: _Operand | float) -> _Operand: return self.apply("mod", self, other) - def __rmod__(self, other): + def __rmod__(self, other: _Operand | float) -> _Operand: return self.apply("mod", other, self) - def __pow__(self, other): + def __pow__(self, other: _Operand | float) -> _Operand: return self.apply("pow", self, other) - def __rpow__(self, other): + def __rpow__(self, other: _Operand | float) -> _Operand: return self.apply("pow", other, self) # bitwise - def __invert__(self): + def __invert__(self) -> _Operand: return self.apply("invert", self) - def __and__(self, other): + def __and__(self, other: _Operand | float) -> _Operand: return self.apply("and", self, other) - def __rand__(self, other): + def __rand__(self, other: _Operand | float) -> _Operand: return self.apply("and", other, self) - def __or__(self, other): + def __or__(self, other: _Operand | float) -> _Operand: return self.apply("or", self, other) - def __ror__(self, other): + def __ror__(self, other: _Operand | float) -> _Operand: return self.apply("or", other, self) - def __xor__(self, other): + def __xor__(self, other: _Operand | float) -> _Operand: return self.apply("xor", self, other) - def __rxor__(self, other): + def __rxor__(self, other: _Operand | float) -> _Operand: return self.apply("xor", other, self) - def __lshift__(self, other): + def __lshift__(self, other: _Operand | float) -> _Operand: return self.apply("lshift", self, other) - def __rshift__(self, other): + def __rshift__(self, other: _Operand | float) -> _Operand: return self.apply("rshift", self, other) # logical @@ -170,56 +181,61 @@ class _Operand: def __ne__(self, other): return self.apply("ne", self, other) - def __lt__(self, other): + def __lt__(self, other: _Operand | float) -> _Operand: return self.apply("lt", self, other) - def __le__(self, other): + def __le__(self, other: _Operand | float) -> _Operand: return self.apply("le", self, other) - def __gt__(self, other): + def __gt__(self, other: _Operand | float) -> _Operand: return self.apply("gt", self, other) - def __ge__(self, other): + def __ge__(self, other: _Operand | float) -> _Operand: return self.apply("ge", self, other) # conversions -def imagemath_int(self): +def imagemath_int(self: _Operand) -> _Operand: return _Operand(self.im.convert("I")) -def imagemath_float(self): +def imagemath_float(self: _Operand) -> _Operand: return _Operand(self.im.convert("F")) # logical -def imagemath_equal(self, other): +def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("eq", self, other, mode="I") -def imagemath_notequal(self, other): +def imagemath_notequal(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("ne", self, other, mode="I") -def imagemath_min(self, other): +def imagemath_min(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("min", self, other) -def imagemath_max(self, other): +def imagemath_max(self: _Operand, other: _Operand | float | None) -> _Operand: return self.apply("max", self, other) -def imagemath_convert(self, mode): +def imagemath_convert(self: _Operand, mode: str) -> _Operand: return _Operand(self.im.convert(mode)) -ops = {} -for k, v in list(globals().items()): - if k[:10] == "imagemath_": - ops[k[10:]] = v +ops = { + "int": imagemath_int, + "float": imagemath_float, + "equal": imagemath_equal, + "notequal": imagemath_notequal, + "min": imagemath_min, + "max": imagemath_max, + "convert": imagemath_convert, +} -def eval(expression, _dict={}, **kw): +def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any: """ Evaluates an image expression. @@ -233,7 +249,12 @@ def eval(expression, _dict={}, **kw): """ # build execution namespace - args = ops.copy() + args: dict[str, Any] = ops.copy() + for k in list(_dict.keys()) + list(kw.keys()): + if "__" in k or hasattr(builtins, k): + msg = f"'{k}' not allowed" + raise ValueError(msg) + args.update(_dict) args.update(kw) for k, v in args.items(): @@ -242,7 +263,7 @@ def eval(expression, _dict={}, **kw): compiled_code = compile(expression, "", "eval") - def scan(code): + def scan(code: CodeType) -> None: for const in code.co_consts: if type(const) is type(compiled_code): scan(const) diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 54c3d01c4..0b31f6081 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -15,77 +15,82 @@ from __future__ import annotations import sys - -# mode descriptor cache -_modes = None +from functools import lru_cache class ModeDescriptor: """Wrapper for mode strings.""" - def __init__(self, mode, bands, basemode, basetype, typestr): + def __init__( + self, + mode: str, + bands: tuple[str, ...], + basemode: str, + basetype: str, + typestr: str, + ) -> None: self.mode = mode self.bands = bands self.basemode = basemode self.basetype = basetype self.typestr = typestr - def __str__(self): + def __str__(self) -> str: return self.mode -def getmode(mode): +@lru_cache +def getmode(mode: str) -> ModeDescriptor: """Gets a mode descriptor for the given mode.""" - global _modes - if not _modes: - # initialize mode cache - modes = {} - endian = "<" if sys.byteorder == "little" else ">" - for m, (basemode, basetype, bands, typestr) in { - # core modes - # Bits need to be extended to bytes - "1": ("L", "L", ("1",), "|b1"), - "L": ("L", "L", ("L",), "|u1"), - "I": ("L", "I", ("I",), endian + "i4"), - "F": ("L", "F", ("F",), endian + "f4"), - "P": ("P", "L", ("P",), "|u1"), - "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), - "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), - "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"), - # UNDONE - unsigned |u1i1i1 - "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"), - "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), - # extra experimental modes - "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), - "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), - "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), - "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), - "LA": ("L", "L", ("L", "A"), "|u1"), - "La": ("L", "L", ("L", "a"), "|u1"), - "PA": ("RGB", "L", ("P", "A"), "|u1"), - }.items(): - modes[m] = ModeDescriptor(m, bands, basemode, basetype, typestr) - # mapping modes - for i16mode, typestr in { - # I;16 == I;16L, and I;32 == I;32L - "I;16": "u2", - "I;16BS": ">i2", - "I;16N": endian + "u2", - "I;16NS": endian + "i2", - "I;32": "u4", - "I;32L": "i4", - "I;32LS": "" + + modes = { + # core modes + # Bits need to be extended to bytes + "1": ("L", "L", ("1",), "|b1"), + "L": ("L", "L", ("L",), "|u1"), + "I": ("L", "I", ("I",), endian + "i4"), + "F": ("L", "F", ("F",), endian + "f4"), + "P": ("P", "L", ("P",), "|u1"), + "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"), + "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"), + "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"), + "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"), + "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"), + # UNDONE - unsigned |u1i1i1 + "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"), + "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), + # extra experimental modes + "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), + "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), + "LA": ("L", "L", ("L", "A"), "|u1"), + "La": ("L", "L", ("L", "a"), "|u1"), + "PA": ("RGB", "L", ("P", "A"), "|u1"), + } + if mode in modes: + base_mode, base_type, bands, type_str = modes[mode] + return ModeDescriptor(mode, bands, base_mode, base_type, type_str) + + mapping_modes = { + # I;16 == I;16L, and I;32 == I;32L + "I;16": "u2", + "I;16BS": ">i2", + "I;16N": endian + "u2", + "I;16NS": endian + "i2", + "I;32": "u4", + "I;32L": "i4", + "I;32LS": " None: if patterns is not None: self.patterns = patterns else: self.patterns = [] - self.lut = None + self.lut: bytearray | None = None if op_name is not None: known_patterns = { "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"], @@ -87,25 +89,27 @@ class LutBuilder: self.patterns = known_patterns[op_name] - def add_patterns(self, patterns): + def add_patterns(self, patterns: list[str]) -> None: self.patterns += patterns - def build_default_lut(self): + def build_default_lut(self) -> None: symbols = [0, 1] m = 1 << 4 # pos of current pixel self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) - def get_lut(self): + def get_lut(self) -> bytearray | None: return self.lut - def _string_permute(self, pattern, permutation): + def _string_permute(self, pattern: str, permutation: list[int]) -> str: """string_permute takes a pattern and a permutation and returns the string permuted according to the permutation list. """ assert len(permutation) == 9 return "".join(pattern[p] for p in permutation) - def _pattern_permute(self, basic_pattern, options, basic_result): + def _pattern_permute( + self, basic_pattern: str, options: str, basic_result: int + ) -> list[tuple[str, int]]: """pattern_permute takes a basic pattern and its result and clones the pattern according to the modifications described in the $options parameter. It returns a list of all cloned patterns.""" @@ -135,12 +139,13 @@ class LutBuilder: return patterns - def build_lut(self): + def build_lut(self) -> bytearray: """Compile all patterns into a morphology lut. TBD :Build based on (file) morphlut:modify_lut """ self.build_default_lut() + assert self.lut is not None patterns = [] # Parse and create symmetries of the patterns strings @@ -159,10 +164,10 @@ class LutBuilder: patterns += self._pattern_permute(pattern, options, result) # compile the patterns into regular expressions for speed - for i, pattern in enumerate(patterns): + compiled_patterns = [] + for pattern in patterns: p = pattern[0].replace(".", "X").replace("X", "[01]") - p = re.compile(p) - patterns[i] = (p, pattern[1]) + compiled_patterns.append((re.compile(p), pattern[1])) # Step through table and find patterns that match. # Note that all the patterns are searched. The last one @@ -172,8 +177,8 @@ class LutBuilder: bitpattern = bin(i)[2:] bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1] - for p, r in patterns: - if p.match(bitpattern): + for pattern, r in compiled_patterns: + if pattern.match(bitpattern): self.lut[i] = [0, 1][r] return self.lut @@ -182,7 +187,12 @@ class LutBuilder: class MorphOp: """A class for binary morphological operators""" - def __init__(self, lut=None, op_name=None, patterns=None): + def __init__( + self, + lut: bytearray | None = None, + op_name: str | None = None, + patterns: list[str] | None = None, + ) -> None: """Create a binary morphological operator""" self.lut = lut if op_name is not None: @@ -190,7 +200,7 @@ class MorphOp: elif patterns is not None: self.lut = LutBuilder(patterns=patterns).build_lut() - def apply(self, image): + def apply(self, image: Image.Image): """Run a single morphological operation on an image Returns a tuple of the number of changed pixels and the @@ -206,7 +216,7 @@ class MorphOp: count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage - def match(self, image): + def match(self, image: Image.Image): """Get a list of coordinates matching the morphological operation on an image. @@ -221,7 +231,7 @@ class MorphOp: raise ValueError(msg) return _imagingmorph.match(bytes(self.lut), image.im.id) - def get_on_pixels(self, image): + def get_on_pixels(self, image: Image.Image): """Get a list of all turned on pixels in a binary image Returns a list of tuples of (x,y) coordinates @@ -232,7 +242,7 @@ class MorphOp: raise ValueError(msg) return _imagingmorph.get_on_pixels(image.im.id) - def load_lut(self, filename): + def load_lut(self, filename: str) -> None: """Load an operator from an mrl file""" with open(filename, "rb") as f: self.lut = bytearray(f.read()) @@ -242,7 +252,7 @@ class MorphOp: msg = "Wrong size operator file!" raise Exception(msg) - def save_lut(self, filename): + def save_lut(self, filename: str) -> None: """Save an operator to an mrl file""" if self.lut is None: msg = "No operator loaded" @@ -250,6 +260,6 @@ class MorphOp: with open(filename, "wb") as f: f.write(self.lut) - def set_lut(self, lut): + def set_lut(self, lut: bytearray | None) -> None: """Set the lut from an external source""" self.lut = lut diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index a9e626b2b..6218c723f 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,6 +21,7 @@ from __future__ import annotations import functools import operator import re +from typing import Protocol, Sequence, cast from . import ExifTags, Image, ImagePalette @@ -28,7 +29,7 @@ from . import ExifTags, Image, ImagePalette # helpers -def _border(border): +def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]: if isinstance(border, tuple): if len(border) == 2: left, top = right, bottom = border @@ -39,7 +40,7 @@ def _border(border): return left, top, right, bottom -def _color(color, mode): +def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]: if isinstance(color, str): from . import ImageColor @@ -47,7 +48,7 @@ def _color(color, mode): return color -def _lut(image, lut): +def _lut(image: Image.Image, lut: list[int]) -> Image.Image: if image.mode == "P": # FIXME: apply to lookup table, not image data msg = "mode P support coming soon" @@ -65,7 +66,13 @@ def _lut(image, lut): # actions -def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): +def autocontrast( + image: Image.Image, + cutoff: float | tuple[float, float] = 0, + ignore: int | Sequence[int] | None = None, + mask: Image.Image | None = None, + preserve_tone: bool = False, +) -> Image.Image: """ Maximize (normalize) image contrast. This function calculates a histogram of the input image (or mask region), removes ``cutoff`` percent of the @@ -97,10 +104,9 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): h = histogram[layer : layer + 256] if ignore is not None: # get rid of outliers - try: + if isinstance(ignore, int): h[ignore] = 0 - except TypeError: - # assume sequence + else: for ix in ignore: h[ix] = 0 if cutoff: @@ -112,7 +118,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): for ix in range(256): n = n + h[ix] # remove cutoff% pixels from the low end - cut = n * cutoff[0] // 100 + cut = int(n * cutoff[0] // 100) for lo in range(256): if cut > h[lo]: cut = cut - h[lo] @@ -123,7 +129,7 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): if cut <= 0: break # remove cutoff% samples from the high end - cut = n * cutoff[1] // 100 + cut = int(n * cutoff[1] // 100) for hi in range(255, -1, -1): if cut > h[hi]: cut = cut - h[hi] @@ -156,7 +162,15 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): return _lut(image, lut) -def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoint=127): +def colorize( + image: Image.Image, + black: str | tuple[int, ...], + white: str | tuple[int, ...], + mid: str | int | tuple[int, ...] | None = None, + blackpoint: int = 0, + whitepoint: int = 255, + midpoint: int = 127, +) -> Image.Image: """ Colorize grayscale image. This function calculates a color wedge which maps all black pixels in @@ -188,10 +202,9 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 # Define colors from arguments - black = _color(black, "RGB") - white = _color(white, "RGB") - if mid is not None: - mid = _color(mid, "RGB") + rgb_black = cast(Sequence[int], _color(black, "RGB")) + rgb_white = cast(Sequence[int], _color(white, "RGB")) + rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None # Empty lists for the mapping red = [] @@ -200,18 +213,24 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the low-end values for i in range(0, blackpoint): - red.append(black[0]) - green.append(black[1]) - blue.append(black[2]) + red.append(rgb_black[0]) + green.append(rgb_black[1]) + blue.append(rgb_black[2]) # Create the mapping (2-color) - if mid is None: + if rgb_mid is None: range_map = range(0, whitepoint - blackpoint) for i in range_map: - red.append(black[0] + i * (white[0] - black[0]) // len(range_map)) - green.append(black[1] + i * (white[1] - black[1]) // len(range_map)) - blue.append(black[2] + i * (white[2] - black[2]) // len(range_map)) + red.append( + rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map) + ) + green.append( + rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map) + ) + blue.append( + rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map) + ) # Create the mapping (3-color) else: @@ -219,26 +238,36 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi range_map2 = range(0, whitepoint - midpoint) for i in range_map1: - red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1)) - green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1)) - blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1)) + red.append( + rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1) + ) + green.append( + rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1) + ) + blue.append( + rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1) + ) for i in range_map2: - red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2)) - green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2)) - blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2)) + red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2)) + green.append( + rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2) + ) + blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2)) # Create the high-end values for i in range(0, 256 - whitepoint): - red.append(white[0]) - green.append(white[1]) - blue.append(white[2]) + red.append(rgb_white[0]) + green.append(rgb_white[1]) + blue.append(rgb_white[2]) # Return converted image image = image.convert("RGB") return _lut(image, red + green + blue) -def contain(image, size, method=Image.Resampling.BICUBIC): +def contain( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a resized version of the image, set to the maximum width and height within the requested size, while maintaining the original aspect ratio. @@ -267,7 +296,9 @@ def contain(image, size, method=Image.Resampling.BICUBIC): return image.resize(size, resample=method) -def cover(image, size, method=Image.Resampling.BICUBIC): +def cover( + image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a resized version of the image, so that the requested size is covered, while maintaining the original aspect ratio. @@ -296,7 +327,13 @@ def cover(image, size, method=Image.Resampling.BICUBIC): return image.resize(size, resample=method) -def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5, 0.5)): +def pad( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + color: str | int | tuple[int, ...] | None = None, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: """ Returns a resized and padded version of the image, expanded to fill the requested aspect ratio and size. @@ -334,7 +371,7 @@ def pad(image, size, method=Image.Resampling.BICUBIC, color=None, centering=(0.5 return out -def crop(image, border=0): +def crop(image: Image.Image, border: int = 0) -> Image.Image: """ Remove border from image. The same amount of pixels are removed from all four sides. This function works on all image modes. @@ -349,7 +386,9 @@ def crop(image, border=0): return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) -def scale(image, factor, resample=Image.Resampling.BICUBIC): +def scale( + image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC +) -> Image.Image: """ Returns a rescaled image by a specific factor given in parameter. A factor greater than 1 expands the image, between 0 and 1 contracts the @@ -372,7 +411,19 @@ def scale(image, factor, resample=Image.Resampling.BICUBIC): return image.resize(size, resample) -def deform(image, deformer, resample=Image.Resampling.BILINEAR): +class _SupportsGetMesh(Protocol): + def getmesh( + self, image: Image.Image + ) -> list[ + tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]] + ]: ... + + +def deform( + image: Image.Image, + deformer: _SupportsGetMesh, + resample: int = Image.Resampling.BILINEAR, +) -> Image.Image: """ Deform the image. @@ -388,7 +439,7 @@ def deform(image, deformer, resample=Image.Resampling.BILINEAR): ) -def equalize(image, mask=None): +def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image: """ Equalize the image histogram. This function applies a non-linear mapping to the input image, in order to create a uniform @@ -419,7 +470,11 @@ def equalize(image, mask=None): return _lut(image, lut) -def expand(image, border=0, fill=0): +def expand( + image: Image.Image, + border: int | tuple[int, ...] = 0, + fill: str | int | tuple[int, ...] = 0, +) -> Image.Image: """ Add border to the image @@ -445,7 +500,13 @@ def expand(image, border=0, fill=0): return out -def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, 0.5)): +def fit( + image: Image.Image, + size: tuple[int, int], + method: int = Image.Resampling.BICUBIC, + bleed: float = 0.0, + centering: tuple[float, float] = (0.5, 0.5), +) -> Image.Image: """ Returns a resized and cropped version of the image, cropped to the requested aspect ratio and size. @@ -479,13 +540,12 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, # kevin@cazabon.com # https://www.cazabon.com - # ensure centering is mutable - centering = list(centering) + centering_x, centering_y = centering - if not 0.0 <= centering[0] <= 1.0: - centering[0] = 0.5 - if not 0.0 <= centering[1] <= 1.0: - centering[1] = 0.5 + if not 0.0 <= centering_x <= 1.0: + centering_x = 0.5 + if not 0.0 <= centering_y <= 1.0: + centering_y = 0.5 if not 0.0 <= bleed < 0.5: bleed = 0.0 @@ -522,8 +582,8 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, crop_height = live_size[0] / output_ratio # make the crop - crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering[0] - crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering[1] + crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x + crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height) @@ -531,7 +591,7 @@ def fit(image, size, method=Image.Resampling.BICUBIC, bleed=0.0, centering=(0.5, return image.resize(size, method, box=crop) -def flip(image): +def flip(image: Image.Image) -> Image.Image: """ Flip the image vertically (top to bottom). @@ -541,7 +601,7 @@ def flip(image): return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) -def grayscale(image): +def grayscale(image: Image.Image) -> Image.Image: """ Convert the image to grayscale. @@ -551,7 +611,7 @@ def grayscale(image): return image.convert("L") -def invert(image): +def invert(image: Image.Image) -> Image.Image: """ Invert (negate) the image. @@ -562,7 +622,7 @@ def invert(image): return image.point(lut) if image.mode == "1" else _lut(image, lut) -def mirror(image): +def mirror(image: Image.Image) -> Image.Image: """ Flip image horizontally (left to right). @@ -572,7 +632,7 @@ def mirror(image): return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) -def posterize(image, bits): +def posterize(image: Image.Image, bits: int) -> Image.Image: """ Reduce the number of bits for each color channel. @@ -585,7 +645,7 @@ def posterize(image, bits): return _lut(image, lut) -def solarize(image, threshold=128): +def solarize(image: Image.Image, threshold: int = 128) -> Image.Image: """ Invert all pixel values above a threshold. @@ -602,7 +662,7 @@ def solarize(image, threshold=128): return _lut(image, lut) -def exif_transpose(image, *, in_place=False): +def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None: """ If an image has an EXIF Orientation tag, other than 1, transpose the image accordingly, and remove the orientation data. @@ -616,7 +676,7 @@ def exif_transpose(image, *, in_place=False): """ image.load() image_exif = image.getexif() - orientation = image_exif.get(ExifTags.Base.Orientation) + orientation = image_exif.get(ExifTags.Base.Orientation, 1) method = { 2: Image.Transpose.FLIP_LEFT_RIGHT, 3: Image.Transpose.ROTATE_180, @@ -653,3 +713,4 @@ def exif_transpose(image, *, in_place=False): return transposed_image elif not in_place: return image.copy() + return None diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index fbcfa309d..770d10025 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -18,6 +18,7 @@ from __future__ import annotations import array +from typing import Sequence from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile @@ -34,11 +35,11 @@ class ImagePalette: Defaults to an empty palette. """ - def __init__(self, mode="RGB", palette=None): + def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None: self.mode = mode self.rawmode = None # if set, palette contains raw data self.palette = palette or bytearray() - self.dirty = None + self.dirty: int | None = None @property def palette(self): @@ -127,7 +128,7 @@ class ImagePalette: raise ValueError(msg) from e return index - def getcolor(self, color, image=None): + def getcolor(self, color, image=None) -> int: """Given an rgb tuple, allocate palette entry. .. warning:: This method is experimental. @@ -192,7 +193,7 @@ class ImagePalette: # Internal -def raw(rawmode, data): +def raw(rawmode, data) -> ImagePalette: palette = ImagePalette() palette.rawmode = rawmode palette.palette = data diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 6377c7501..293ba4941 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,19 +19,26 @@ from __future__ import annotations import sys from io import BytesIO +from typing import Callable from . import Image from ._util import is_path +qt_version: str | None qt_versions = [ ["6", "PyQt6"], ["side6", "PySide6"], ] # If a version has already been imported, attempt it first -qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True) -for qt_version, qt_module in qt_versions: +qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) +for version, qt_module in qt_versions: try: + QBuffer: type + QIODevice: type + QImage: type + QPixmap: type + qRgba: Callable[[int, int, int, int], int] if qt_module == "PyQt6": from PyQt6.QtCore import QBuffer, QIODevice from PyQt6.QtGui import QImage, QPixmap, qRgba @@ -41,6 +48,7 @@ for qt_version, qt_module in qt_versions: except (ImportError, RuntimeError): continue qt_is_installed = True + qt_version = version break else: qt_is_installed = False diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index e09b001e8..2c1850276 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -16,6 +16,10 @@ ## from __future__ import annotations +from typing import Callable + +from . import Image + class Iterator: """ @@ -29,14 +33,14 @@ class Iterator: :param im: An image object. """ - def __init__(self, im): + def __init__(self, im: Image.Image): if not hasattr(im, "seek"): msg = "im must have seek method" raise AttributeError(msg) self.im = im self.position = getattr(self.im, "_min_frame", 0) - def __getitem__(self, ix): + def __getitem__(self, ix: int) -> Image.Image: try: self.im.seek(ix) return self.im @@ -44,10 +48,10 @@ class Iterator: msg = "end of sequence" raise IndexError(msg) from e - def __iter__(self): + def __iter__(self) -> Iterator: return self - def __next__(self): + def __next__(self) -> Image.Image: try: self.im.seek(self.position) self.position += 1 @@ -57,7 +61,10 @@ class Iterator: raise StopIteration(msg) from e -def all_frames(im, func=None): +def all_frames( + im: Image.Image | list[Image.Image], + func: Callable[[Image.Image], Image.Image] | None = None, +) -> list[Image.Image]: """ Applies a given function to all frames in an image or a list of images. The frames are returned as a list of separate images. diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index fad3e0980..d90545e92 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -13,18 +13,20 @@ # from __future__ import annotations +import abc import os import shutil import subprocess import sys from shlex import quote +from typing import Any from . import Image _viewers = [] -def register(viewer, order=1): +def register(viewer, order: int = 1) -> None: """ The :py:func:`register` function is used to register additional viewers:: @@ -49,7 +51,7 @@ def register(viewer, order=1): _viewers.insert(0, viewer) -def show(image, title=None, **options): +def show(image: Image.Image, title: str | None = None, **options: Any) -> bool: r""" Display a given image. @@ -69,7 +71,7 @@ class Viewer: # main api - def show(self, image, **options): + def show(self, image: Image.Image, **options: Any) -> int: """ The main function for displaying an image. Converts the given image to the target format and displays it. @@ -87,16 +89,16 @@ class Viewer: # hook methods - format = None + format: str | None = None """The format to convert the image into.""" - options = {} + options: dict[str, Any] = {} """Additional options used to convert the image.""" - def get_format(self, image): + def get_format(self, image: Image.Image) -> str | None: """Return format name, or ``None`` to save as PGM/PPM.""" return self.format - def get_command(self, file, **options): + def get_command(self, file: str, **options: Any) -> str: """ Returns the command used to display the file. Not implemented in the base class. @@ -104,15 +106,15 @@ class Viewer: msg = "unavailable in base viewer" raise NotImplementedError(msg) - def save_image(self, image): + def save_image(self, image: Image.Image) -> str: """Save to temporary file and return filename.""" return image._dump(format=self.get_format(image), **self.options) - def show_image(self, image, **options): + def show_image(self, image: Image.Image, **options: Any) -> int: """Display the given image.""" return self.show_file(self.save_image(image), **options) - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -129,7 +131,7 @@ class WindowsViewer(Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} - def get_command(self, file, **options): + def get_command(self, file: str, **options: Any) -> str: return ( f'start "Pillow" /WAIT "{file}" ' "&& ping -n 4 127.0.0.1 >NUL " @@ -147,14 +149,14 @@ class MacViewer(Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} - def get_command(self, file, **options): + def get_command(self, file: str, **options: Any) -> str: # on darwin open returns immediately resulting in the temp # file removal while app is opening command = "open -a Preview.app" command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" return command - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -180,7 +182,11 @@ class UnixViewer(Viewer): format = "PNG" options = {"compress_level": 1, "save_all": True} - def get_command(self, file, **options): + @abc.abstractmethod + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: + pass + + def get_command(self, file: str, **options: Any) -> str: command = self.get_command_ex(file, **options)[0] return f"({command} {quote(file)}" @@ -190,11 +196,11 @@ class XDGViewer(UnixViewer): The freedesktop.org ``xdg-open`` command. """ - def get_command_ex(self, file, **options): + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: command = executable = "xdg-open" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -208,13 +214,15 @@ class DisplayViewer(UnixViewer): This viewer supports the ``title`` parameter. """ - def get_command_ex(self, file, title=None, **options): + def get_command_ex( + self, file: str, title: str | None = None, **options: Any + ) -> tuple[str, str]: command = executable = "display" if title: command += f" -title {quote(title)}" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -231,12 +239,12 @@ class DisplayViewer(UnixViewer): class GmDisplayViewer(UnixViewer): """The GraphicsMagick ``gm display`` command.""" - def get_command_ex(self, file, **options): + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: executable = "gm" command = "gm display" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -247,12 +255,12 @@ class GmDisplayViewer(UnixViewer): class EogViewer(UnixViewer): """The GNOME Image Viewer ``eog`` command.""" - def get_command_ex(self, file, **options): + def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: executable = "eog" command = "eog -n" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -266,7 +274,9 @@ class XVViewer(UnixViewer): This viewer supports the ``title`` parameter. """ - def get_command_ex(self, file, title=None, **options): + def get_command_ex( + self, file: str, title: str | None = None, **options: Any + ) -> tuple[str, str]: # note: xv is pretty outdated. most modern systems have # imagemagick's display command instead. command = executable = "xv" @@ -274,7 +284,7 @@ class XVViewer(UnixViewer): command += f" -name {quote(title)}" return command, executable - def show_file(self, path, **options): + def show_file(self, path: str, **options: Any) -> int: """ Display given file. """ @@ -304,7 +314,7 @@ if sys.platform not in ("win32", "darwin"): # unixoids class IPythonViewer(Viewer): """The viewer for IPython frontends.""" - def show_image(self, image, **options): + def show_image(self, image: Image.Image, **options: Any) -> int: ipython_display(image) return 1 diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 1fdaa9140..6aa82dadd 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -14,17 +14,29 @@ # from __future__ import annotations +from typing import Sequence + from . import Image class Transform(Image.ImageTransformHandler): - def __init__(self, data): + """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`.""" + + method: Image.Transform + + def __init__(self, data: Sequence[int]) -> None: self.data = data - def getdata(self): + def getdata(self) -> tuple[Image.Transform, Sequence[int]]: return self.method, self.data - def transform(self, size, image, **options): + def transform( + self, + size: tuple[int, int], + image: Image.Image, + **options: dict[str, str | int | tuple[int, ...] | list[int]], + ) -> Image.Image: + """Perform the transform. Called from :py:meth:`.Image.transform`.""" # can be overridden method, data = self.getdata() return image.transform(size, method, data, **options) @@ -42,7 +54,7 @@ class AffineTransform(Transform): This function can be used to scale, translate, rotate, and shear the original image. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows from an affine transform matrix. @@ -51,6 +63,26 @@ class AffineTransform(Transform): method = Image.Transform.AFFINE +class PerspectiveTransform(Transform): + """ + Define a perspective image transform. + + This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel + (x, y) in the output image, the new value is taken from a position + ((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in + the input image, rounded to nearest pixel. + + This function can be used to scale, translate, rotate, and shear the + original image. + + See :py:meth:`.Image.transform` + + :param matrix: An 8-tuple (a, b, c, d, e, f, g, h). + """ + + method = Image.Transform.PERSPECTIVE + + class ExtentTransform(Transform): """ Define a transform to extract a subregion from an image. @@ -64,7 +96,7 @@ class ExtentTransform(Transform): rectangle in the current image. It is slightly slower than crop, but about as fast as a corresponding resize operation. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the input image's coordinate system. See :ref:`coordinate-system`. @@ -80,7 +112,7 @@ class QuadTransform(Transform): Maps a quadrilateral (a region defined by four corners) from the image to a rectangle of the given size. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the upper left, lower left, lower right, and upper right corner of the @@ -95,7 +127,7 @@ class MeshTransform(Transform): Define a mesh image transform. A mesh transform consists of one or more individual quad transforms. - See :py:meth:`~PIL.Image.Image.transform` + See :py:meth:`.Image.transform` :param data: A list of (bbox, quad) tuples. """ diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 7469c592d..abb3fb762 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -33,10 +33,12 @@ class ImtImageFile(ImageFile.ImageFile): format = "IMT" format_description = "IM Tools" - def _open(self): + def _open(self) -> None: # Quick rejection: if there's not a LF among the first # 100 bytes, this is (probably) not a text header. + assert self.fp is not None + buffer = self.fp.read(100) if b"\n" not in buffer: msg = "not an IM file" diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index e7dc3e4e4..409609434 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -16,30 +16,48 @@ # from __future__ import annotations -import os -import tempfile +from io import BytesIO +from typing import Sequence from . import Image, ImageFile -from ._binary import i8, o8 from ._binary import i16be as i16 from ._binary import i32be as i32 +from ._deprecate import deprecate COMPRESSION = {1: "raw", 5: "jpeg"} -PAD = o8(0) * 4 + +def __getattr__(name: str) -> bytes: + if name == "PAD": + deprecate("IptcImagePlugin.PAD", 12) + return b"\0\0\0\0" + msg = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(msg) # # Helpers -def i(c): - return i32((PAD + c)[-4:]) +def _i(c: bytes) -> int: + return i32((b"\0\0\0\0" + c)[-4:]) -def dump(c): +def _i8(c: int | bytes) -> int: + return c if isinstance(c, int) else c[0] + + +def i(c: bytes) -> int: + """.. deprecated:: 10.2.0""" + deprecate("IptcImagePlugin.i", 12) + return _i(c) + + +def dump(c: Sequence[int | bytes]) -> None: + """.. deprecated:: 10.2.0""" + deprecate("IptcImagePlugin.dump", 12) for i in c: - print("%02x" % i8(i), end=" ") + print("%02x" % _i8(i), end=" ") print() @@ -52,10 +70,10 @@ class IptcImageFile(ImageFile.ImageFile): format = "IPTC" format_description = "IPTC/NAA" - def getint(self, key): - return i(self.info[key]) + def getint(self, key: tuple[int, int]) -> int: + return _i(self.info[key]) - def field(self): + def field(self) -> tuple[tuple[int, int] | None, int]: # # get a IPTC field header s = self.fp.read(5) @@ -77,13 +95,13 @@ class IptcImageFile(ImageFile.ImageFile): elif size == 128: size = 0 elif size > 128: - size = i(self.fp.read(size - 128)) + size = _i(self.fp.read(size - 128)) else: size = i16(s, 3) return tag, size - def _open(self): + def _open(self) -> None: # load descriptive fields while True: offset = self.fp.tell() @@ -103,10 +121,10 @@ class IptcImageFile(ImageFile.ImageFile): self.info[tag] = tagdata # mode - layers = i8(self.info[(3, 60)][0]) - component = i8(self.info[(3, 60)][1]) + layers = self.info[(3, 60)][0] + component = self.info[(3, 60)][1] if (3, 65) in self.info: - id = i8(self.info[(3, 65)][0]) - 1 + id = self.info[(3, 65)][0] - 1 else: id = 0 if layers == 1 and not component: @@ -128,27 +146,22 @@ class IptcImageFile(ImageFile.ImageFile): # tile if tag == (8, 10): - self.tile = [ - ("iptc", (compression, offset), (0, 0, self.size[0], self.size[1])) - ] + self.tile = [("iptc", (0, 0) + self.size, offset, compression)] def load(self): if len(self.tile) != 1 or self.tile[0][0] != "iptc": return ImageFile.ImageFile.load(self) - type, tile, box = self.tile[0] - - encoding, offset = tile + offset, compression = self.tile[0][2:] self.fp.seek(offset) # Copy image data to temporary file - o_fd, outfile = tempfile.mkstemp(text=False) - o = os.fdopen(o_fd) - if encoding == "raw": + o = BytesIO() + if compression == "raw": # To simplify access to the extracted file, # prepend a PPM header - o.write("P5\n%d %d\n255\n" % self.size) + o.write(b"P5\n%d %d\n255\n" % self.size) while True: type, size = self.field() if type != (8, 10): @@ -159,17 +172,10 @@ class IptcImageFile(ImageFile.ImageFile): break o.write(s) size -= len(s) - o.close() - try: - with Image.open(outfile) as _im: - _im.load() - self.im = _im.im - finally: - try: - os.unlink(outfile) - except OSError: - pass + with Image.open(o) as _im: + _im.load() + self.im = _im.im Image.register_open(IptcImageFile.format, IptcImageFile) @@ -185,8 +191,6 @@ def getiptcinfo(im): :returns: A dictionary containing IPTC information, or None if no IPTC information block was found. """ - import io - from . import JpegImagePlugin, TiffImagePlugin data = None @@ -221,7 +225,7 @@ def getiptcinfo(im): # parse the IPTC information chunk im.info = {} - im.fp = io.BytesIO(data) + im.fp = BytesIO(data) try: im._open() diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 59bade303..81b8749a3 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -87,10 +87,12 @@ def APP(self, marker): self.info["dpi"] = jfif_density self.info["jfif_unit"] = jfif_unit self.info["jfif_density"] = jfif_density - elif marker == 0xFFE1 and s[:5] == b"Exif\0": - if "exif" not in self.info: - # extract EXIF information (incomplete) - self.info["exif"] = s # FIXME: value will change + elif marker == 0xFFE1 and s[:6] == b"Exif\0\0": + # extract EXIF information + if "exif" in self.info: + self.info["exif"] += s[6:] + else: + self.info["exif"] = s self._exif_offset = self.fp.tell() - n + 6 elif marker == 0xFFE2 and s[:5] == b"FPXR\0": # extract FlashPix information (incomplete) @@ -783,6 +785,7 @@ def _save(im, fp, filename): progressive, info.get("smooth", 0), optimize, + info.get("keep_rgb", False), info.get("streamtype", 0), dpi[0], dpi[1], diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index 9ecfdb259..d0e64a35e 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -62,6 +62,7 @@ Libjpeg ref.: https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html """ + from __future__ import annotations # fmt: off diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 9a85c0d15..27972236c 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -22,8 +22,8 @@ import struct from . import Image, ImageFile -def _accept(s): - return s[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" +def _accept(prefix: bytes) -> bool: + return prefix[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" ## @@ -34,8 +34,10 @@ class McIdasImageFile(ImageFile.ImageFile): format = "MCIDAS" format_description = "McIdas area file" - def _open(self): + def _open(self) -> None: # parse area file directory + assert self.fp is not None + s = self.fp.read(256) if not _accept(s) or len(s) != 256: msg = "not an McIdas area file" diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index f4e598ca3..1565612f8 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -16,21 +16,22 @@ from __future__ import annotations from . import Image, ImageFile from ._binary import i8 +from ._typing import SupportsRead # # Bitstream parser class BitStream: - def __init__(self, fp): + def __init__(self, fp: SupportsRead[bytes]) -> None: self.fp = fp self.bits = 0 self.bitbuffer = 0 - def next(self): + def next(self) -> int: return i8(self.fp.read(1)) - def peek(self, bits): + def peek(self, bits: int) -> int: while self.bits < bits: c = self.next() if c < 0: @@ -40,13 +41,13 @@ class BitStream: self.bits += 8 return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 - def skip(self, bits): + def skip(self, bits: int) -> None: while self.bits < bits: self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1)) self.bits += 8 self.bits = self.bits - bits - def read(self, bits): + def read(self, bits: int) -> int: v = self.peek(bits) self.bits = self.bits - bits return v @@ -61,9 +62,10 @@ class MpegImageFile(ImageFile.ImageFile): format = "MPEG" format_description = "MPEG" - def _open(self): - s = BitStream(self.fp) + def _open(self) -> None: + assert self.fp is not None + s = BitStream(self.fp) if s.read(32) != 0x1B3: msg = "not an MPEG file" raise SyntaxError(msg) diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 77dac65b6..65cc70624 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -26,6 +26,7 @@ from __future__ import annotations import io import struct +from typing import IO from . import Image, ImageFile from ._binary import i16le as i16 @@ -35,7 +36,7 @@ from ._binary import o16le as o16 # read MSP files -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] in [b"DanM", b"LinS"] @@ -48,8 +49,10 @@ class MspImageFile(ImageFile.ImageFile): format = "MSP" format_description = "Windows Paint" - def _open(self): + def _open(self) -> None: # Header + assert self.fp is not None + s = self.fp.read(32) if not _accept(s): msg = "not an MSP file" @@ -109,7 +112,9 @@ class MspDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + img = io.BytesIO() blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) try: @@ -159,7 +164,7 @@ Image.register_decoder("MSP", MspDecoder) # write MSP files (uncompressed only) -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index a0515b302..1cd5c4a9d 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -27,8 +27,10 @@ class PcdImageFile(ImageFile.ImageFile): format = "PCD" format_description = "Kodak PhotoCD" - def _open(self): + def _open(self) -> None: # rough + assert self.fp is not None + self.fp.seek(2048) s = self.fp.read(2048) @@ -47,9 +49,11 @@ class PcdImageFile(ImageFile.ImageFile): self._size = 768, 512 # FIXME: not correct for rotated images! self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)] - def load_end(self): + def load_end(self) -> None: if self.tile_post_rotate: # Handle rotated PCDs + assert self.im is not None + self.im = self.im.rotate(self.tile_post_rotate) self._size = self.im.size diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index d602a1633..0d1968b14 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -18,6 +18,7 @@ from __future__ import annotations import io +from typing import BinaryIO, Callable from . import FontFile, Image from ._binary import i8 @@ -41,7 +42,7 @@ PCF_SWIDTHS = 1 << 6 PCF_GLYPH_NAMES = 1 << 7 PCF_BDF_ACCELERATORS = 1 << 8 -BYTES_PER_ROW = [ +BYTES_PER_ROW: list[Callable[[int], int]] = [ lambda bits: ((bits + 7) >> 3), lambda bits: ((bits + 15) >> 3) & ~1, lambda bits: ((bits + 31) >> 3) & ~3, @@ -49,7 +50,7 @@ BYTES_PER_ROW = [ ] -def sz(s, o): +def sz(s: bytes, o: int) -> bytes: return s[o : s.index(b"\0", o)] @@ -58,7 +59,7 @@ class PcfFontFile(FontFile.FontFile): name = "name" - def __init__(self, fp, charset_encoding="iso8859-1"): + def __init__(self, fp: BinaryIO, charset_encoding: str = "iso8859-1"): self.charset_encoding = charset_encoding magic = l32(fp.read(4)) @@ -104,7 +105,9 @@ class PcfFontFile(FontFile.FontFile): bitmaps[ix], ) - def _getformat(self, tag): + def _getformat( + self, tag: int + ) -> tuple[BinaryIO, int, Callable[[bytes], int], Callable[[bytes], int]]: format, size, offset = self.toc[tag] fp = self.fp @@ -119,7 +122,7 @@ class PcfFontFile(FontFile.FontFile): return fp, format, i16, i32 - def _load_properties(self): + def _load_properties(self) -> dict[bytes, bytes | int]: # # font properties @@ -138,18 +141,16 @@ class PcfFontFile(FontFile.FontFile): data = fp.read(i32(fp.read(4))) for k, s, v in p: - k = sz(data, k) - if s: - v = sz(data, v) - properties[k] = v + property_value: bytes | int = sz(data, v) if s else v + properties[sz(data, k)] = property_value return properties - def _load_metrics(self): + def _load_metrics(self) -> list[tuple[int, int, int, int, int, int, int, int]]: # # font metrics - metrics = [] + metrics: list[tuple[int, int, int, int, int, int, int, int]] = [] fp, format, i16, i32 = self._getformat(PCF_METRICS) @@ -182,7 +183,9 @@ class PcfFontFile(FontFile.FontFile): return metrics - def _load_bitmaps(self, metrics): + def _load_bitmaps( + self, metrics: list[tuple[int, int, int, int, int, int, int, int]] + ) -> list[Image.Image]: # # bitmap data @@ -222,7 +225,7 @@ class PcfFontFile(FontFile.FontFile): return bitmaps - def _load_encoding(self): + def _load_encoding(self) -> list[int | None]: fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS) first_col, last_col = i16(fp.read(2)), i16(fp.read(2)) @@ -233,7 +236,7 @@ class PcfFontFile(FontFile.FontFile): nencoding = (last_col - first_col + 1) * (last_row - first_row + 1) # map character code to bitmap index - encoding = [None] * min(256, nencoding) + encoding: list[int | None] = [None] * min(256, nencoding) encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)] diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 98ecefd05..026bfd9a0 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -28,6 +28,7 @@ from __future__ import annotations import io import logging +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -37,7 +38,7 @@ from ._binary import o16le as o16 logger = logging.getLogger(__name__) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[0] == 10 and prefix[1] in [0, 2, 3, 5] @@ -49,8 +50,10 @@ class PcxImageFile(ImageFile.ImageFile): format = "PCX" format_description = "Paintbrush" - def _open(self): + def _open(self) -> None: # header + assert self.fp is not None + s = self.fp.read(128) if not _accept(s): msg = "not a PCX file" @@ -141,7 +144,7 @@ SAVE = { } -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: @@ -199,6 +202,8 @@ def _save(im, fp, filename): if im.mode == "P": # colour palette + assert im.im is not None + fp.write(o8(12)) palette = im.im.getpalette("RGB", "RGB") palette += b"\x00" * (768 - len(palette)) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 3506aadce..1777f1f20 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -188,9 +188,9 @@ def _save(im, fp, filename, save_all=False): x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) info = { - "title": None - if is_appending - else os.path.splitext(os.path.basename(filename))[0], + "title": ( + None if is_appending else os.path.splitext(os.path.basename(filename))[0] + ), "author": None, "subject": None, "keywords": None, diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 014460006..4c5101738 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,6 +8,7 @@ import os import re import time import zlib +from typing import TYPE_CHECKING, Any, List, Union # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -239,12 +240,18 @@ class PdfName: return bytes(result) -class PdfArray(list): +class PdfArray(List[Any]): def __bytes__(self): return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" -class PdfDict(collections.UserDict): +if TYPE_CHECKING: + _DictBase = collections.UserDict[Union[str, bytes], Any] +else: + _DictBase = collections.UserDict + + +class PdfDict(_DictBase): def __setattr__(self, key, value): if key == "data": collections.UserDict.__setattr__(self, key, value) diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index af866feb3..887b6568b 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -27,7 +27,7 @@ from ._binary import i16le as i16 # helpers -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"\200\350\000\000" @@ -39,8 +39,10 @@ class PixarImageFile(ImageFile.ImageFile): format = "PIXAR" format_description = "PIXAR raster image" - def _open(self): + def _open(self) -> None: # assuming a 4-byte magic label + assert self.fp is not None + s = self.fp.read(4) if not _accept(s): msg = "not a PIXAR file" diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index e4ed93880..823f12492 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -378,7 +378,7 @@ class PngStream(ChunkStream): } def rewind(self): - self.im_info = self.rewind_state["info"] + self.im_info = self.rewind_state["info"].copy() self.im_tile = self.rewind_state["tile"] self._seq_num = self.rewind_state["seq_num"] diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 25dbfa5b0..6ac7a9bbc 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -15,6 +15,9 @@ # from __future__ import annotations +import math +from typing import IO + from . import Image, ImageFile from ._binary import i16be as i16 from ._binary import o8 @@ -35,6 +38,7 @@ MODES = { b"P6": "RGB", # extensions b"P0CMYK": "CMYK", + b"Pf": "F", # PIL extensions (for test purposes only) b"PyP": "P", b"PyRGBA": "RGBA", @@ -42,8 +46,8 @@ MODES = { } -def _accept(prefix): - return prefix[0:1] == b"P" and prefix[1] in b"0123456y" +def _accept(prefix: bytes) -> bool: + return prefix[0:1] == b"P" and prefix[1] in b"0123456fy" ## @@ -54,7 +58,9 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" - def _read_magic(self): + def _read_magic(self) -> bytes: + assert self.fp is not None + magic = b"" # read until whitespace or longest available magic number for _ in range(6): @@ -64,7 +70,9 @@ class PpmImageFile(ImageFile.ImageFile): magic += c return magic - def _read_token(self): + def _read_token(self) -> bytes: + assert self.fp is not None + token = b"" while len(token) <= 10: # read until next whitespace or limit of 10 characters c = self.fp.read(1) @@ -90,13 +98,16 @@ class PpmImageFile(ImageFile.ImageFile): raise ValueError(msg) return token - def _open(self): + def _open(self) -> None: + assert self.fp is not None + magic_number = self._read_magic() try: mode = MODES[magic_number] except KeyError: msg = "not a PPM file" raise SyntaxError(msg) + self._mode = mode if magic_number in (b"P1", b"P4"): self.custom_mimetype = "image/x-portable-bitmap" @@ -105,40 +116,42 @@ class PpmImageFile(ImageFile.ImageFile): elif magic_number in (b"P3", b"P6"): self.custom_mimetype = "image/x-portable-pixmap" - maxval = None + self._size = int(self._read_token()), int(self._read_token()) + decoder_name = "raw" if magic_number in (b"P1", b"P2", b"P3"): decoder_name = "ppm_plain" - for ix in range(3): - token = int(self._read_token()) - if ix == 0: # token is the x size - xsize = token - elif ix == 1: # token is the y size - ysize = token - if mode == "1": - self._mode = "1" - rawmode = "1;I" - break - else: - self._mode = rawmode = mode - elif ix == 2: # token is maxval - maxval = token - if not 0 < maxval < 65536: - msg = "maxval must be greater than 0 and less than 65536" - raise ValueError(msg) - if maxval > 255 and mode == "L": - self._mode = "I" - if decoder_name != "ppm_plain": - # If maxval matches a bit depth, use the raw decoder directly - if maxval == 65535 and mode == "L": - rawmode = "I;16B" - elif maxval != 255: - decoder_name = "ppm" + args: str | tuple[str | int, ...] + if mode == "1": + args = "1;I" + elif mode == "F": + scale = float(self._read_token()) + if scale == 0.0 or not math.isfinite(scale): + msg = "scale must be finite and non-zero" + raise ValueError(msg) + self.info["scale"] = abs(scale) - args = (rawmode, 0, 1) if decoder_name == "raw" else (rawmode, maxval) - self._size = xsize, ysize - self.tile = [(decoder_name, (0, 0, xsize, ysize), self.fp.tell(), args)] + rawmode = "F;32F" if scale < 0 else "F;32BF" + args = (rawmode, 0, -1) + else: + maxval = int(self._read_token()) + if not 0 < maxval < 65536: + msg = "maxval must be greater than 0 and less than 65536" + raise ValueError(msg) + if maxval > 255 and mode == "L": + self._mode = "I" + + rawmode = mode + if decoder_name != "ppm_plain": + # If maxval matches a bit depth, use the raw decoder directly + if maxval == 65535 and mode == "L": + rawmode = "I;16B" + elif maxval != 255: + decoder_name = "ppm" + + args = rawmode if decoder_name == "raw" else (rawmode, maxval) + self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)] # @@ -147,16 +160,19 @@ class PpmImageFile(ImageFile.ImageFile): class PpmPlainDecoder(ImageFile.PyDecoder): _pulls_fd = True + _comment_spans: bool + + def _read_block(self) -> bytes: + assert self.fd is not None - def _read_block(self): return self.fd.read(ImageFile.SAFEBLOCK) - def _find_comment_end(self, block, start=0): + def _find_comment_end(self, block: bytes, start: int = 0) -> int: a = block.find(b"\n", start) b = block.find(b"\r", start) return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1) - def _ignore_comments(self, block): + def _ignore_comments(self, block: bytes) -> bytes: if self._comment_spans: # Finish current comment while block: @@ -190,7 +206,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): break return block - def _decode_bitonal(self): + def _decode_bitonal(self) -> bytearray: """ This is a separate method because in the plain PBM format, all data tokens are exactly one byte, so the inter-token whitespace is optional. @@ -215,7 +231,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): invert = bytes.maketrans(b"01", b"\xFF\x00") return data.translate(invert) - def _decode_blocks(self, maxval): + def _decode_blocks(self, maxval: int) -> bytearray: data = bytearray() max_len = 10 out_byte_count = 4 if self.mode == "I" else 1 @@ -223,7 +239,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): bands = Image.getmodebands(self.mode) total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count - half_token = False + half_token = b"" while len(data) != total_bytes: block = self._read_block() # read next block if not block: @@ -237,7 +253,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): if half_token: block = half_token + block # stitch half_token to new block - half_token = False + half_token = b"" tokens = block.split() @@ -255,15 +271,15 @@ class PpmPlainDecoder(ImageFile.PyDecoder): raise ValueError(msg) value = int(token) if value > maxval: - msg = f"Channel value too large for this mode: {value}" - raise ValueError(msg) + msg_str = f"Channel value too large for this mode: {value}" + raise ValueError(msg_str) value = round(value / maxval * out_max) data += o32(value) if self.mode == "I" else o8(value) if len(data) == total_bytes: # finished! break return data - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: self._comment_spans = False if self.mode == "1": data = self._decode_bitonal() @@ -279,7 +295,9 @@ class PpmPlainDecoder(ImageFile.PyDecoder): class PpmDecoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + data = bytearray() maxval = self.args[-1] in_byte_count = 1 if maxval < 256 else 2 @@ -306,7 +324,7 @@ class PpmDecoder(ImageFile.PyDecoder): # -------------------------------------------------------------------- -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode == "1": rawmode, head = "1;I", b"P4" elif im.mode == "L": @@ -315,6 +333,8 @@ def _save(im, fp, filename): rawmode, head = "I;16B", b"P5" elif im.mode in ("RGB", "RGBA"): rawmode, head = "RGB", b"P6" + elif im.mode == "F": + rawmode, head = "F;32F", b"Pf" else: msg = f"cannot write mode {im.mode} as PPM" raise OSError(msg) @@ -326,7 +346,10 @@ def _save(im, fp, filename): fp.write(b"255\n") else: fp.write(b"65535\n") - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) + elif head == b"Pf": + fp.write(b"-1.0\n") + row_order = -1 if im.mode == "F" else 1 + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) # @@ -339,6 +362,6 @@ Image.register_save(PpmImageFile.format, _save) Image.register_decoder("ppm", PpmDecoder) Image.register_decoder("ppm_plain", PpmPlainDecoder) -Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"]) +Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"]) Image.register_mime(PpmImageFile.format, "image/x-portable-anymap") diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 23ff154f6..2c831913d 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -25,6 +25,7 @@ import sys from ._deprecate import deprecate +FFI: type try: from cffi import FFI @@ -43,7 +44,7 @@ except ImportError as ex: # anything in core. from ._util import DeferredError - FFI = ffi = DeferredError(ex) + FFI = ffi = DeferredError.new(ex) logger = logging.getLogger(__name__) diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index f9a10f610..7bd84ebd4 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -24,13 +24,14 @@ from __future__ import annotations import os import struct +from typing import IO from . import Image, ImageFile from ._binary import i16be as i16 from ._binary import o8 -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return len(prefix) >= 2 and i16(prefix) == 474 @@ -52,8 +53,10 @@ class SgiImageFile(ImageFile.ImageFile): format = "SGI" format_description = "SGI Image File Format" - def _open(self): + def _open(self) -> None: # HEAD + assert self.fp is not None + headlen = 512 s = self.fp.read(headlen) @@ -122,7 +125,7 @@ class SgiImageFile(ImageFile.ImageFile): ] -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode not in {"RGB", "RGBA", "L"}: msg = "Unsupported SGI image mode" raise ValueError(msg) @@ -168,8 +171,8 @@ def _save(im, fp, filename): # Maximum Byte value (255 = 8bits per pixel) pinmax = 255 # Image name (79 characters max, truncated below in write) - img_name = os.path.splitext(os.path.basename(filename))[0] - img_name = img_name.encode("ascii", "ignore") + filename = os.path.basename(filename) + img_name = os.path.splitext(filename)[0].encode("ascii", "ignore") # Standard representation of pixel in the file colormap = 0 fp.write(struct.pack(">h", magic_number)) @@ -201,7 +204,10 @@ def _save(im, fp, filename): class SGI16Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None + assert self.im is not None + rawmode, stride, orientation = self.args pagesize = self.state.xsize * self.state.ysize zsize = len(self.mode) diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index 11ce3dfef..4e098474a 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -21,7 +21,7 @@ from . import Image, ImageFile, ImagePalette from ._binary import i32be as i32 -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return len(prefix) >= 4 and i32(prefix) == 0x59A66A95 @@ -33,7 +33,7 @@ class SunImageFile(ImageFile.ImageFile): format = "SUN" format_description = "Sun Raster File" - def _open(self): + def _open(self) -> None: # The Sun Raster file header is 32 bytes in length # and has the following format: @@ -49,6 +49,8 @@ class SunImageFile(ImageFile.ImageFile): # DWORD ColorMapLength; /* Size of the color map in bytes */ # } SUNRASTER; + assert self.fp is not None + # HEAD s = self.fp.read(32) if not _accept(s): diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 26522d93f..7470663b4 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -16,14 +16,15 @@ from __future__ import annotations import io +from types import TracebackType from . import ContainerIO -class TarIO(ContainerIO.ContainerIO): +class TarIO(ContainerIO.ContainerIO[bytes]): """A file object that provides read access to a given member of a TAR file.""" - def __init__(self, tarfile, file): + def __init__(self, tarfile: str, file: str) -> None: """ Create file object. @@ -57,11 +58,16 @@ class TarIO(ContainerIO.ContainerIO): super().__init__(self.fh, self.fh.tell(), size) # Context manager support - def __enter__(self): + def __enter__(self) -> TarIO: return self - def __exit__(self, *args): + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: self.close() - def close(self): + def close(self) -> None: self.fh.close() diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 65c7484f7..828701342 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -18,6 +18,7 @@ from __future__ import annotations import warnings +from typing import IO from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 @@ -49,8 +50,10 @@ class TgaImageFile(ImageFile.ImageFile): format = "TGA" format_description = "Targa" - def _open(self): + def _open(self) -> None: # process header + assert self.fp is not None + s = self.fp.read(18) id_len = s[0] @@ -151,8 +154,9 @@ class TgaImageFile(ImageFile.ImageFile): except KeyError: pass # cannot decode - def load_end(self): + def load_end(self) -> None: if self._flip_horizontally: + assert self.im is not None self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) @@ -171,7 +175,7 @@ SAVE = { } -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: @@ -194,6 +198,7 @@ def _save(im, fp, filename): warnings.warn("id_section has been trimmed to 255 characters") if colormaptype: + assert im.im is not None palette = im.im.getpalette("RGB", "BGR") colormaplength, colormapentry = len(palette) // 3, 24 else: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index fc242ca64..3ba4de9d1 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -50,6 +50,7 @@ import warnings from collections.abc import MutableMapping from fractions import Fraction from numbers import Number, Rational +from typing import TYPE_CHECKING, Any, Callable from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 @@ -306,6 +307,13 @@ _load_dispatch = {} _write_dispatch = {} +def _delegate(op): + def delegate(self, *args): + return getattr(self._val, op)(*args) + + return delegate + + class IFDRational(Rational): """Implements a rational class where 0/0 is a legal value to match the in the wild use of exif rationals. @@ -391,12 +399,6 @@ class IFDRational(Rational): self._numerator = _numerator self._denominator = _denominator - def _delegate(op): - def delegate(self, *args): - return getattr(self._val, op)(*args) - - return delegate - """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', 'mod','rmod', 'pow','rpow', 'pos', 'neg', @@ -436,7 +438,50 @@ class IFDRational(Rational): __int__ = _delegate("__int__") -class ImageFileDirectory_v2(MutableMapping): +def _register_loader(idx, size): + def decorator(func): + from .TiffTags import TYPES + + if func.__name__.startswith("load_"): + TYPES[idx] = func.__name__[5:].replace("_", " ") + _load_dispatch[idx] = size, func # noqa: F821 + return func + + return decorator + + +def _register_writer(idx): + def decorator(func): + _write_dispatch[idx] = func # noqa: F821 + return func + + return decorator + + +def _register_basic(idx_fmt_name): + from .TiffTags import TYPES + + idx, fmt, name = idx_fmt_name + TYPES[idx] = name + size = struct.calcsize("=" + fmt) + _load_dispatch[idx] = ( # noqa: F821 + size, + lambda self, data, legacy_api=True: ( + self._unpack(f"{len(data) // size}{fmt}", data) + ), + ) + _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 + b"".join(self._pack(fmt, value) for value in values) + ) + + +if TYPE_CHECKING: + _IFDv2Base = MutableMapping[int, Any] +else: + _IFDv2Base = MutableMapping + + +class ImageFileDirectory_v2(_IFDv2Base): """This class represents a TIFF tag directory. To speed things up, we don't decode tags unless they're asked for. @@ -497,6 +542,9 @@ class ImageFileDirectory_v2(MutableMapping): """ + _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} + _write_dispatch: dict[int, Callable[..., Any]] = {} + def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): """Initialize an ImageFileDirectory. @@ -531,7 +579,10 @@ class ImageFileDirectory_v2(MutableMapping): prefix = property(lambda self: self._prefix) offset = property(lambda self: self._offset) - legacy_api = property(lambda self: self._legacy_api) + + @property + def legacy_api(self): + return self._legacy_api @legacy_api.setter def legacy_api(self, value): @@ -674,40 +725,6 @@ class ImageFileDirectory_v2(MutableMapping): def _pack(self, fmt, *values): return struct.pack(self._endian + fmt, *values) - def _register_loader(idx, size): - def decorator(func): - from .TiffTags import TYPES - - if func.__name__.startswith("load_"): - TYPES[idx] = func.__name__[5:].replace("_", " ") - _load_dispatch[idx] = size, func # noqa: F821 - return func - - return decorator - - def _register_writer(idx): - def decorator(func): - _write_dispatch[idx] = func # noqa: F821 - return func - - return decorator - - def _register_basic(idx_fmt_name): - from .TiffTags import TYPES - - idx, fmt, name = idx_fmt_name - TYPES[idx] = name - size = struct.calcsize("=" + fmt) - _load_dispatch[idx] = ( # noqa: F821 - size, - lambda self, data, legacy_api=True: ( - self._unpack(f"{len(data) // size}{fmt}", data) - ), - ) - _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 - b"".join(self._pack(fmt, value) for value in values) - ) - list( map( _register_basic, @@ -995,7 +1012,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): tagdata = property(lambda self: self._tagdata) # defined in ImageFileDirectory_v2 - tagtype: dict + tagtype: dict[int, int] """Dictionary of tag types""" @classmethod @@ -1704,25 +1721,27 @@ def _save(im, fp, filename): colormap += [0] * (256 - colors) ifd[COLORMAP] = colormap # data orientation - stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) - # aim for given strip size (64 KB by default) when using libtiff writer - if libtiff: - im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) - rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1]) - # JPEG encoder expects multiple of 8 rows - if compression == "jpeg": - rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1]) - else: - rows_per_strip = im.size[1] - if rows_per_strip == 0: - rows_per_strip = 1 - strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip - strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip - ifd[ROWSPERSTRIP] = rows_per_strip + w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH] + stride = len(bits) * ((w * bits[0] + 7) // 8) + if ROWSPERSTRIP not in ifd: + # aim for given strip size (64 KB by default) when using libtiff writer + if libtiff: + im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) + rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h) + # JPEG encoder expects multiple of 8 rows + if compression == "jpeg": + rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h) + else: + rows_per_strip = h + if rows_per_strip == 0: + rows_per_strip = 1 + ifd[ROWSPERSTRIP] = rows_per_strip + strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP] + strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP] if strip_byte_counts >= 2**16: ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( - stride * im.size[1] - strip_byte_counts * (strips_per_image - 1), + stride * h - strip_byte_counts * (strips_per_image - 1), ) ifd[STRIPOFFSETS] = tuple( range(0, strip_byte_counts * strips_per_image, strip_byte_counts) @@ -1833,11 +1852,11 @@ def _save(im, fp, filename): tags = list(atts.items()) tags.sort() a = (rawmode, compression, _fp, filename, tags, types) - e = Image._getencoder(im.mode, "libtiff", a, encoderconfig) - e.setimage(im.im, (0, 0) + im.size) + encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) + encoder.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - errcode, data = e.encode(16 * 1024)[1:] + errcode, data = encoder.encode(16 * 1024)[1:] if not _fp: fp.write(data) if errcode: diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 88ff2f4fc..b94193931 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -22,7 +22,7 @@ from collections import namedtuple class TagInfo(namedtuple("_TagInfo", "value name type length enum")): - __slots__ = [] + __slots__: list[str] = [] def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): return super().__new__(cls, value, name, type, length, enum or {}) @@ -437,7 +437,7 @@ _populate() ## # Map type numbers to type names -- defined in ImageFileDirectory. -TYPES = {} +TYPES: dict[int, str] = {} # # These tags are handled by default in libtiff, without diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 47ba1c548..c84adaca2 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -33,7 +33,7 @@ for r in range(8): ) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:6] == _MAGIC @@ -45,8 +45,10 @@ class XVThumbImageFile(ImageFile.ImageFile): format = "XVThumb" format_description = "XV thumbnail image" - def _open(self): + def _open(self) -> None: # check magic + assert self.fp is not None + if not _accept(self.fp.read(6)): msg = "not an XV thumbnail file" raise SyntaxError(msg) diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 566acbfe5..eee727436 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -21,6 +21,7 @@ from __future__ import annotations import re +from typing import IO from . import Image, ImageFile @@ -36,7 +37,7 @@ xbm_head = re.compile( ) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix.lstrip()[:7] == b"#define" @@ -48,7 +49,9 @@ class XbmImageFile(ImageFile.ImageFile): format = "XBM" format_description = "X11 Bitmap" - def _open(self): + def _open(self) -> None: + assert self.fp is not None + m = xbm_head.match(self.fp.read(512)) if not m: @@ -67,7 +70,7 @@ class XbmImageFile(ImageFile.ImageFile): self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] -def _save(im, fp, filename): +def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 3fcac8643..63a45769b 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -12,6 +12,7 @@ Use PIL.__version__ for this Pillow version. ;-) """ + from __future__ import annotations from . import _version diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index c60c9cec1..0a07e8d0e 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -18,16 +18,16 @@ from __future__ import annotations from struct import pack, unpack_from -def i8(c): - return c if c.__class__ is int else c[0] +def i8(c: bytes) -> int: + return c[0] -def o8(i): +def o8(i: int) -> bytes: return bytes((i & 255,)) # Input, le = little endian, be = big endian -def i16le(c, o=0): +def i16le(c: bytes, o: int = 0) -> int: """ Converts a 2-bytes (16 bits) string to an unsigned integer. @@ -37,7 +37,7 @@ def i16le(c, o=0): return unpack_from(" int: """ Converts a 2-bytes (16 bits) string to a signed integer. @@ -47,7 +47,7 @@ def si16le(c, o=0): return unpack_from(" int: """ Converts a 2-bytes (16 bits) string to a signed integer, big endian. @@ -57,7 +57,7 @@ def si16be(c, o=0): return unpack_from(">h", c, o)[0] -def i32le(c, o=0): +def i32le(c: bytes, o: int = 0) -> int: """ Converts a 4-bytes (32 bits) string to an unsigned integer. @@ -67,7 +67,7 @@ def i32le(c, o=0): return unpack_from(" int: """ Converts a 4-bytes (32 bits) string to a signed integer. @@ -77,26 +77,26 @@ def si32le(c, o=0): return unpack_from(" int: return unpack_from(">H", c, o)[0] -def i32be(c, o=0): +def i32be(c: bytes, o: int = 0) -> int: return unpack_from(">I", c, o)[0] # Output, le = little endian, be = big endian -def o16le(i): +def o16le(i: int) -> bytes: return pack(" bytes: return pack(" bytes: return pack(">H", i) -def o32be(i): +def o32be(i: int) -> bytes: return pack(">I", i) diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_imaging.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_imagingcms.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_imagingft.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_imagingmath.pyi b/src/PIL/_imagingmath.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_imagingmath.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_imagingmorph.pyi b/src/PIL/_imagingmorph.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_imagingmorph.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 03a6eba44..beddfb062 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -1,10 +1,12 @@ """ Find compiled module linking to Tcl / Tk libraries """ + from __future__ import annotations import sys import tkinter -from tkinter import _tkinter as tk + +tk = getattr(tkinter, "_tkinter") try: if hasattr(sys, "pypy_find_executable"): diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py new file mode 100644 index 000000000..7075e8672 --- /dev/null +++ b/src/PIL/_typing.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import os +import sys +from typing import Protocol, Sequence, TypeVar, Union + +if sys.version_info >= (3, 10): + from typing import TypeGuard +else: + try: + from typing_extensions import TypeGuard + except ImportError: + from typing import Any + + class TypeGuard: # type: ignore[no-redef] + def __class_getitem__(cls, item: Any) -> type[bool]: + return bool + + +Coords = Union[Sequence[float], Sequence[Sequence[float]]] + + +_T_co = TypeVar("_T_co", covariant=True) + + +class SupportsRead(Protocol[_T_co]): + def read(self, __length: int = ...) -> _T_co: ... + + +StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] + + +__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"] diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 4634d335b..6bc762816 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,21 +1,31 @@ from __future__ import annotations import os -from pathlib import Path +from typing import Any, NoReturn + +from ._typing import StrOrBytesPath, TypeGuard -def is_path(f): - return isinstance(f, (bytes, str, Path)) +def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: + return isinstance(f, (bytes, str, os.PathLike)) -def is_directory(f): +def is_directory(f: Any) -> TypeGuard[StrOrBytesPath]: """Checks if an object is a string, and that it points to a directory.""" return is_path(f) and os.path.isdir(f) class DeferredError: - def __init__(self, ex): + def __init__(self, ex: BaseException): self.ex = ex - def __getattr__(self, elt): + def __getattr__(self, elt: str) -> NoReturn: raise self.ex + + @staticmethod + def new(ex: BaseException) -> Any: + """ + Creates an object that raises the wrapped exception ``ex`` when used, + and casts it to :py:obj:`~typing.Any` type. + """ + return DeferredError(ex) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 7d994caf4..0568943b5 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,4 +1,4 @@ # Master version for Pillow from __future__ import annotations -__version__ = "10.2.0.dev0" +__version__ = "10.3.0.dev0" diff --git a/src/PIL/_webp.pyi b/src/PIL/_webp.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_webp.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/py.typed b/src/PIL/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/_imaging.c b/src/_imaging.c index 2270c77fe..59f80a354 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2649,6 +2649,26 @@ _font_new(PyObject *self_, PyObject *args) { self->glyphs[i].sy0 = S16(B16(glyphdata, 14)); self->glyphs[i].sx1 = S16(B16(glyphdata, 16)); self->glyphs[i].sy1 = S16(B16(glyphdata, 18)); + + // Do not allow glyphs to extend beyond bitmap image + // Helps prevent DOS by stopping cropped images being larger than the original + if (self->glyphs[i].sx0 < 0) { + self->glyphs[i].dx0 -= self->glyphs[i].sx0; + self->glyphs[i].sx0 = 0; + } + if (self->glyphs[i].sy0 < 0) { + self->glyphs[i].dy0 -= self->glyphs[i].sy0; + self->glyphs[i].sy0 = 0; + } + if (self->glyphs[i].sx1 > self->bitmap->xsize) { + self->glyphs[i].dx1 -= self->glyphs[i].sx1 - self->bitmap->xsize; + self->glyphs[i].sx1 = self->bitmap->xsize; + } + if (self->glyphs[i].sy1 > self->bitmap->ysize) { + self->glyphs[i].dy1 -= self->glyphs[i].sy1 - self->bitmap->ysize; + self->glyphs[i].sy1 = self->bitmap->ysize; + } + if (self->glyphs[i].dy0 < y0) { y0 = self->glyphs[i].dy0; } @@ -2721,7 +2741,7 @@ _font_text_asBytes(PyObject *encoded_string, unsigned char **text) { static PyObject * _font_getmask(ImagingFontObject *self, PyObject *args) { Imaging im; - Imaging bitmap; + Imaging bitmap = NULL; int x, b; int i = 0; int status; @@ -2730,7 +2750,7 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { PyObject *encoded_string; unsigned char *text; - char *mode = ""; + char *mode; if (!PyArg_ParseTuple(args, "O|s:getmask", &encoded_string, &mode)) { return NULL; @@ -2753,10 +2773,13 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { b = self->baseline; for (x = 0; text[i]; i++) { glyph = &self->glyphs[text[i]]; - bitmap = - ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); - if (!bitmap) { - goto failed; + if (i == 0 || text[i] != text[i - 1]) { + ImagingDelete(bitmap); + bitmap = + ImagingCrop(self->bitmap, glyph->sx0, glyph->sy0, glyph->sx1, glyph->sy1); + if (!bitmap) { + goto failed; + } } status = ImagingPaste( im, @@ -2766,17 +2789,18 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { glyph->dy0 + b, glyph->dx1 + x, glyph->dy1 + b); - ImagingDelete(bitmap); if (status < 0) { goto failed; } x = x + glyph->dx; b = b + glyph->dy; } + ImagingDelete(bitmap); free(text); return PyImagingNew(im); failed: + ImagingDelete(bitmap); free(text); ImagingDelete(im); Py_RETURN_NONE; diff --git a/src/_imagingft.c b/src/_imagingft.c index 4925dc233..6e24fcf95 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) { image = PyObject_CallFunction(fill, "ii", width, height); if (image == Py_None) { PyMem_Del(glyph_info); - return Py_BuildValue("ii", 0, 0); + return Py_BuildValue("N(ii)", image, 0, 0); } else if (image == NULL) { PyMem_Del(glyph_info); return NULL; @@ -894,7 +894,7 @@ font_render(FontObject *self, PyObject *args) { y_offset -= stroke_width; if (count == 0 || width == 0 || height == 0) { PyMem_Del(glyph_info); - return Py_BuildValue("ii", x_offset, y_offset); + return Py_BuildValue("N(ii)", image, x_offset, y_offset); } if (stroke_width) { @@ -1049,8 +1049,8 @@ font_render(FontObject *self, PyObject *args) { if (yy >= 0 && yy < im->ysize) { /* blend this glyph into the buffer */ int k; - unsigned char v; unsigned char *target; + unsigned int tmp; if (color) { /* target[RGB] returns the color, target[A] returns the mask */ /* target bands get split again in ImageDraw.text */ @@ -1061,34 +1061,55 @@ font_render(FontObject *self, PyObject *args) { if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { /* paste color glyph */ for (k = x0; k < x1; k++) { - if (target[k * 4 + 3] < source[k * 4 + 3]) { - /* unpremultiply BGRa to RGBA */ - target[k * 4 + 0] = CLIP8( - (255 * (int)source[k * 4 + 2]) / source[k * 4 + 3]); - target[k * 4 + 1] = CLIP8( - (255 * (int)source[k * 4 + 1]) / source[k * 4 + 3]); - target[k * 4 + 2] = CLIP8( - (255 * (int)source[k * 4 + 0]) / source[k * 4 + 3]); - target[k * 4 + 3] = source[k * 4 + 3]; + unsigned int src_alpha = source[k * 4 + 3]; + + /* paste only if source has data */ + if (src_alpha > 0) { + /* unpremultiply BGRa */ + int src_red = CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha); + int src_green = CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha); + int src_blue = CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha); + + /* blend required if target has data */ + if (target[k * 4 + 3] > 0) { + /* blend RGBA colors */ + target[k * 4 + 0] = BLEND(src_alpha, target[k * 4 + 0], src_red, tmp); + target[k * 4 + 1] = BLEND(src_alpha, target[k * 4 + 1], src_green, tmp); + target[k * 4 + 2] = BLEND(src_alpha, target[k * 4 + 2], src_blue, tmp); + target[k * 4 + 3] = CLIP8(src_alpha + MULDIV255(target[k * 4 + 3], (255 - src_alpha), tmp)); + } else { + /* paste unpremultiplied RGBA values */ + target[k * 4 + 0] = src_red; + target[k * 4 + 1] = src_green; + target[k * 4 + 2] = src_blue; + target[k * 4 + 3] = src_alpha; + } } } } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { if (color) { unsigned char *ink = (unsigned char *)&foreground_ink; for (k = x0; k < x1; k++) { - v = source[k] * convert_scale; - if (target[k * 4 + 3] < v) { - target[k * 4 + 0] = ink[0]; - target[k * 4 + 1] = ink[1]; - target[k * 4 + 2] = ink[2]; - target[k * 4 + 3] = v; + unsigned int src_alpha = source[k] * convert_scale; + if (src_alpha > 0) { + if (target[k * 4 + 3] > 0) { + target[k * 4 + 0] = BLEND(src_alpha, target[k * 4 + 0], ink[0], tmp); + target[k * 4 + 1] = BLEND(src_alpha, target[k * 4 + 1], ink[1], tmp); + target[k * 4 + 2] = BLEND(src_alpha, target[k * 4 + 2], ink[2], tmp); + target[k * 4 + 3] = CLIP8(src_alpha + MULDIV255(target[k * 4 + 3], (255 - src_alpha), tmp)); + } else { + target[k * 4 + 0] = ink[0]; + target[k * 4 + 1] = ink[1]; + target[k * 4 + 2] = ink[2]; + target[k * 4 + 3] = src_alpha; + } } } } else { for (k = x0; k < x1; k++) { - v = source[k] * convert_scale; - if (target[k] < v) { - target[k] = v; + unsigned int src_alpha = source[k] * convert_scale; + if (src_alpha > 0) { + target[k] = target[k] > 0 ? CLIP8(src_alpha + MULDIV255(target[k], (255 - src_alpha), tmp)) : src_alpha; } } } @@ -1109,18 +1130,12 @@ font_render(FontObject *self, PyObject *args) { if (bitmap_converted_ready) { FT_Bitmap_Done(library, &bitmap_converted); } - Py_DECREF(image); FT_Stroker_Done(stroker); PyMem_Del(glyph_info); - return Py_BuildValue("ii", x_offset, y_offset); + return Py_BuildValue("N(ii)", image, x_offset, y_offset); glyph_error: - if (im->destroy) { - im->destroy(im); - } - if (im->image) { - free(im->image); - } + Py_DECREF(image); if (stroker != NULL) { FT_Done_Glyph(glyph); } diff --git a/src/_webp.c b/src/_webp.c index a1b4dbc1a..47592547c 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -448,11 +448,16 @@ PyObject * _anim_decoder_get_next(PyObject *self) { uint8_t *buf; int timestamp; + int ok; PyObject *bytes; PyObject *ret; + ImagingSectionCookie cookie; WebPAnimDecoderObject *decp = (WebPAnimDecoderObject *)self; - if (!WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp)) { + ImagingSectionEnter(&cookie); + ok = WebPAnimDecoderGetNext(decp->dec, &buf, ×tamp); + ImagingSectionLeave(&cookie); + if (!ok) { PyErr_SetString(PyExc_OSError, "failed to read next frame"); return NULL; } diff --git a/src/encode.c b/src/encode.c index 4664ad0f3..c7dd51015 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1042,6 +1042,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { Py_ssize_t progressive = 0; Py_ssize_t smooth = 0; Py_ssize_t optimize = 0; + int keep_rgb = 0; Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */ Py_ssize_t xdpi = 0, ydpi = 0; Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ @@ -1059,13 +1060,14 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple( args, - "ss|nnnnnnnnnnOz#y#y#", + "ss|nnnnpnnnnnnOz#y#y#", &mode, &rawmode, &quality, &progressive, &smooth, &optimize, + &keep_rgb, &streamtype, &xdpi, &ydpi, @@ -1150,6 +1152,7 @@ PyImaging_JpegEncoderNew(PyObject *self, PyObject *args) { strncpy(((JPEGENCODERSTATE *)encoder->state.context)->rawmode, rawmode, 8); + ((JPEGENCODERSTATE *)encoder->state.context)->keep_rgb = keep_rgb; ((JPEGENCODERSTATE *)encoder->state.context)->quality = quality; ((JPEGENCODERSTATE *)encoder->state.context)->qtables = qarrays; ((JPEGENCODERSTATE *)encoder->state.context)->qtablesLen = qtablesLen; diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index f23245405..e37301df7 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -105,7 +105,7 @@ encode_loop: st->head = st->codes[st->probe] >> 20; goto encode_loop; } else { - /* Reprobe decrement must be nonzero and relatively prime to table + /* Reprobe decrement must be non-zero and relatively prime to table * size. So, any odd positive number for power-of-2 size. */ if ((st->probe -= ((st->tail << 2) | 1)) < 0) { st->probe += TABLE_SIZE; diff --git a/src/libImaging/Jpeg.h b/src/libImaging/Jpeg.h index 5cc74e69b..7cdba9022 100644 --- a/src/libImaging/Jpeg.h +++ b/src/libImaging/Jpeg.h @@ -74,6 +74,9 @@ typedef struct { /* Optimize Huffman tables (slow) */ int optimize; + /* Disable automatic conversion of RGB images to YCbCr if non-zero */ + int keep_rgb; + /* Stream type (0=full, 1=tables only, 2=image only) */ int streamtype; diff --git a/src/libImaging/JpegEncode.c b/src/libImaging/JpegEncode.c index 9da830b18..00f3d5f74 100644 --- a/src/libImaging/JpegEncode.c +++ b/src/libImaging/JpegEncode.c @@ -137,6 +137,30 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { /* Compressor configuration */ jpeg_set_defaults(&context->cinfo); + /* Prevent RGB -> YCbCr conversion */ + if (context->keep_rgb) { + switch (context->cinfo.in_color_space) { + case JCS_RGB: +#ifdef JCS_EXTENSIONS + case JCS_EXT_RGBX: +#endif + switch (context->subsampling) { + case -1: /* Default */ + case 0: /* No subsampling */ + break; + default: + /* Would subsample the green and blue + channels, which doesn't make sense */ + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + jpeg_set_colorspace(&context->cinfo, JCS_RGB); + break; + default: + break; + } + } + /* Use custom quantization tables */ if (context->qtables) { int i; diff --git a/tox.ini b/tox.ini index 034d89372..85a2020d6 100644 --- a/tox.ini +++ b/tox.ini @@ -33,7 +33,16 @@ commands = [testenv:mypy] skip_install = true deps = - mypy==1.7.1 + -r .ci/requirements-mypy.txt + IceSpringPySideStubs-PyQt6 + IceSpringPySideStubs-PySide6 + ipython numpy + packaging + types-cffi + types-defusedxml + types-olefile +extras = + typing commands = mypy src {posargs} diff --git a/winbuild/build.rst b/winbuild/build.rst index a8e4ebaa6..cd3b559e7 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -27,7 +27,7 @@ Download and install: * `Ninja `_ (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) -* x86/x64: `Netwide Assembler (NASM) `_ +* x86/AMD64: `Netwide Assembler (NASM) `_ Any version of Visual Studio 2017 or newer should be supported, including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. @@ -42,7 +42,7 @@ Run ``build_prepare.py`` to configure the build:: usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] [--depends PILLOW_DEPS] - [--architecture {x86,x64,ARM64}] [--nmake] + [--architecture {x86,AMD64,ARM64}] [--nmake] [--no-imagequant] [--no-fribidi] Download and generate build scripts for Pillow dependencies. @@ -55,7 +55,7 @@ Run ``build_prepare.py`` to configure the build:: --depends PILLOW_DEPS directory used to store cached dependencies (default: 'winbuild\depends') - --architecture {x86,x64,ARM64} + --architecture {x86,AMD64,ARM64} build architecture (default: same as host Python) --nmake build dependencies using NMake instead of Ninja --no-imagequant skip GPL-licensed optional dependency libimagequant diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f7e145fb9..df33ea493 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -56,7 +56,9 @@ def cmd_nmake( ) -def cmds_cmake(target: str | tuple[str, ...] | list[str], *params) -> list[str]: +def cmds_cmake( + target: str | tuple[str, ...] | list[str], *params, build_dir: str = "." +) -> list[str]: if not isinstance(target, str): target = " ".join(target) @@ -73,10 +75,11 @@ def cmds_cmake(target: str | tuple[str, ...] | list[str], *params) -> list[str]: "-DCMAKE_CXX_FLAGS=-nologo", *params, '-G "{cmake_generator}"', - ".", + f'-B "{build_dir}"', + "-S .", ] ), - f"{{cmake}} --build . --clean-first --parallel --target {target}", + f'{{cmake}} --build "{build_dir}" --clean-first --parallel --target {target}', ] @@ -102,7 +105,7 @@ SF_PROJECTS = "https://sourceforge.net/projects" ARCHITECTURES = { "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, - "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, + "AMD64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } @@ -171,23 +174,22 @@ DEPS = { "filename": "libwebp-1.3.2.tar.gz", "dir": "libwebp-1.3.2", "license": "COPYING", + "patch": { + r"src\enc\picture_csp_enc.c": { + # link against libsharpyuv.lib + '#include "sharpyuv/sharpyuv.h"': '#include "sharpyuv/sharpyuv.h"\n#pragma comment(lib, "libsharpyuv.lib")', # noqa: E501 + } + }, "build": [ - cmd_rmdir(r"output\release-static"), # clean - cmd_nmake( - "Makefile.vc", - "all", - [ - "CFG=release-static", - "RTLIBCFG=dynamic", - "OBJDIR=output", - "ARCH={architecture}", - "LIBWEBP_BASENAME=webp", - ], + *cmds_cmake( + "webp webpdemux webpmux", + "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DWEBP_LINK_STATIC:BOOL=OFF", ), cmd_mkdir(r"{inc_dir}\webp"), cmd_copy(r"src\webp\*.h", r"{inc_dir}\webp"), ], - "libs": [r"output\release-static\{architecture}\lib\*.lib"], + "libs": [r"libsharpyuv.lib", r"libwebp*.lib"], }, "libtiff": { "url": "https://download.osgeo.org/libtiff/tiff-4.6.0.tar.gz", @@ -200,8 +202,8 @@ DEPS = { "#ifdef LZMA_SUPPORT": '#ifdef LZMA_SUPPORT\n#pragma comment(lib, "liblzma.lib")', # noqa: E501 }, r"libtiff\tif_webp.c": { - # link against webp.lib - "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501 + # link against libwebp.lib + "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "libwebp.lib")', # noqa: E501 }, r"test\CMakeLists.txt": { "add_executable(test_write_read_tags ../placeholder.h)": "", @@ -214,6 +216,7 @@ DEPS = { *cmds_cmake( "tiff", "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DWebP_LIBRARY=libwebp", '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', ) ], @@ -367,7 +370,14 @@ DEPS = { "build": [ cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.13-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), - *cmds_cmake("fribidi"), + # generated tab.i files cannot be cross-compiled + " ^&^& ".join( + [ + "if {architecture}==ARM64 cmd /c call {vcvarsall} x86", + *cmds_cmake("fribidi-gen", "-DARCH=x86", build_dir="build_x86"), + ] + ), + *cmds_cmake("fribidi", "-DARCH={architecture}"), ], "bins": [r"*.dll"], }, @@ -381,10 +391,9 @@ def find_msvs(architecture: str) -> dict[str, str] | None: print("Program Files not found") return None + requires = ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"] if architecture == "ARM64": - tools = "Microsoft.VisualStudio.Component.VC.Tools.ARM64" - else: - tools = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64" + requires += ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.ARM64"] try: vspath = ( @@ -395,8 +404,7 @@ def find_msvs(architecture: str) -> dict[str, str] | None: ), "-latest", "-prerelease", - "-requires", - tools, + *requires, "-property", "installationPath", "-products", @@ -643,7 +651,7 @@ if __name__ == "__main__": ( "ARM64" if platform.machine() == "ARM64" - else ("x86" if struct.calcsize("P") == 4 else "x64") + else ("x86" if struct.calcsize("P") == 4 else "AMD64") ), ), help="build architecture (default: same as host Python)", @@ -707,11 +715,6 @@ if __name__ == "__main__": disabled += ["libimagequant"] if args.no_fribidi: disabled += ["fribidi"] - elif args.architecture == "ARM64" and platform.machine() != "ARM64": - import warnings - - warnings.warn("Cross-compiling FriBiDi is currently not supported, disabling") - disabled += ["fribidi"] prefs = { "architecture": args.architecture, diff --git a/winbuild/fribidi.cmake b/winbuild/fribidi.cmake index 27b8d17a8..b16e0784c 100644 --- a/winbuild/fribidi.cmake +++ b/winbuild/fribidi.cmake @@ -2,9 +2,9 @@ cmake_minimum_required(VERSION 3.12) project(fribidi) + add_definitions(-D_CRT_SECURE_NO_WARNINGS) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) include_directories(lib) function(extract_regex_1 var text regex) @@ -27,12 +27,20 @@ function(fribidi_conf) set(PACKAGE_BUGREPORT "https://github.com/fribidi/fribidi/issues/new") set(SIZEOF_INT 4) set(FRIBIDI_MSVC_BUILD_PLACEHOLDER "#define FRIBIDI_BUILT_WITH_MSVC") - message("detected ${PACKAGE_NAME} version ${FRIBIDI_VERSION}") - configure_file(lib/fribidi-config.h.in lib/fribidi-config.h @ONLY) + message("Detected ${PACKAGE_NAME} version ${FRIBIDI_VERSION}") + configure_file(lib/fribidi-config.h.in ${CMAKE_CURRENT_SOURCE_DIR}/lib/fribidi-config.h @ONLY) endfunction() fribidi_conf() +option(ARCH "Target architecture") +if(${ARCH} STREQUAL ARM64) + set(GEN FALSE) +else() + set(GEN TRUE) +endif() +message("Generate tab.i files: " ${GEN}) + function(prepend var prefix) set(out "") foreach(f ${ARGN}) @@ -56,18 +64,20 @@ macro(fribidi_definitions _TGT) endmacro() function(fribidi_gen _NAME _OUTNAME _PARAM) - set(_OUT lib/${_OUTNAME}) - prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN}) - add_executable(gen-${_NAME} - gen.tab/gen-${_NAME}.c - gen.tab/packtab.c) - fribidi_definitions(gen-${_NAME}) - target_compile_definitions(gen-${_NAME} - PUBLIC DONT_HAVE_FRIBIDI_CONFIG_H) - add_custom_command( - COMMAND gen-${_NAME} ${_PARAM} ${_DEP} > ${_OUT} - DEPENDS ${_DEP} - OUTPUT ${_OUT}) + set(_OUT ${CMAKE_CURRENT_SOURCE_DIR}/lib/${_OUTNAME}) + if(GEN) + prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN}) + add_executable(gen-${_NAME} + gen.tab/gen-${_NAME}.c + gen.tab/packtab.c) + fribidi_definitions(gen-${_NAME}) + target_compile_definitions(gen-${_NAME} + PUBLIC DONT_HAVE_FRIBIDI_CONFIG_H) + add_custom_command( + COMMAND gen-${_NAME} ${_PARAM} ${_DEP} > ${_OUT} + DEPENDS ${_DEP} + OUTPUT ${_OUT}) + endif(GEN) list(APPEND FRIBIDI_SOURCES_GENERATED "${_OUT}") set(FRIBIDI_SOURCES_GENERATED ${FRIBIDI_SOURCES_GENERATED} PARENT_SCOPE) endfunction() @@ -78,8 +88,10 @@ fribidi_gen(unicode-version fribidi-unicode-version.h "" macro(fribidi_tab _NAME) fribidi_gen(${_NAME}-tab ${_NAME}.tab.i 2 ${ARGN}) - target_sources(gen-${_NAME}-tab - PRIVATE lib/fribidi-unicode-version.h) + if(GEN) + target_sources(gen-${_NAME}-tab + PRIVATE lib/fribidi-unicode-version.h) + endif(GEN) endmacro() fribidi_tab(bidi-type unidata/UnicodeData.txt) @@ -89,14 +101,16 @@ fribidi_tab(mirroring unidata/BidiMirroring.txt) fribidi_tab(brackets unidata/BidiBrackets.txt unidata/UnicodeData.txt) fribidi_tab(brackets-type unidata/BidiBrackets.txt) +add_custom_target(fribidi-gen DEPENDS ${FRIBIDI_SOURCES_GENERATED}) + file(GLOB FRIBIDI_SOURCES lib/*.c) file(GLOB FRIBIDI_HEADERS lib/*.h) add_library(fribidi SHARED - ${FRIBIDI_SOURCES} - ${FRIBIDI_HEADERS} - ${FRIBIDI_SOURCES_GENERATED}) + ${FRIBIDI_SOURCES} + ${FRIBIDI_HEADERS} + ${FRIBIDI_SOURCES_GENERATED}) fribidi_definitions(fribidi) target_compile_definitions(fribidi - PUBLIC "-DFRIBIDI_BUILD") + PUBLIC "-DFRIBIDI_BUILD")